Publish multiple articles from one repository
Use this configuration when one repository holds many articles. Conference proceedings, a journal issue and a lab’s report series are typical examples. One folder is nominated (e.g. papers/), and every subfolder inside it is an article that is previewed, submitted and published on its own. Authors usually contribute by opening a pull request from a fork, one article per pull request.
The SciPy Proceedings repository works this way, with one branch per year (see the 2025 and 2026 branches). Its workflows are the model for this page. Have your venue, kind, collection and CURVENOTE_TOKEN secret ready, as described in Before you start. Or let your coding agent do the setup with the agent prompt.
Repository layout¶
proceedings/
├── .github/
│ └── workflows/
│ ├── curvenote-preview.yml ← label "draft": preview + checks
│ ├── curvenote-submit.yml ← label "approved": submit the article
│ └── curvenote-publish-all.yml ← manual: publish every article
├── papers/
│ ├── papers.yml ← optional shared config (exports, theme)
│ ├── _template/ ← "_" folders are never built
│ │ ├── myst.yml
│ │ └── main.md
│ ├── ada_lovelace/
│ │ ├── myst.yml ← project.id: pyconf-2027-ada_lovelace
│ │ └── main.md
│ └── alan_turing/
│ ├── myst.yml ← project.id: pyconf-2027-alan_turing
│ └── main.md
└── README.md ← author instructionsEach article folder has its own myst.yml with a unique project.id. A naming convention, such as <venue>-<collection>-<folder>, makes IDs readable, and the actions can enforce the prefix with id-pattern-regex. Shared settings (exports, the site theme, PDF templates) can live in one file that each article pulls in with extends:
version: 1
extends: ../papers.yml
project:
id: pyconf-2027-ada_lovelace
title: A Numerical Perspective to Terraforming a DesertHow the process works¶
An author forks the repository, copies
papers/_templatetopapers/<their_name>, writes the article and opens a pull request.A maintainer reviews the change and adds the
draftlabel. The preview workflow builds the article, runs the site’s checks and comments on the pull request with links. Each later push updates the comment.Reviewers read the preview. The author pushes changes until it is ready.
An editor adds the
approvedlabel. The submit workflow submits the article to the site, where it appears in the site’s submissions.When the proceedings go live, an editor runs Publish all articles from the Actions tab to publish every article at once.
1. Create the labels¶
Create the two labels in the repository (Issues → Labels → New label) or with the GitHub CLI:
gh label create draft --description "Build a Curvenote preview"
gh label create approved --description "Submit this article to Curvenote"Only people with triage access or higher can add labels, so authors cannot trigger the workflows themselves.
2. Add the preview workflow¶
Create .github/workflows/curvenote-preview.yml:
name: Curvenote preview
on:
pull_request_target:
branches: ['main']
types: [labeled, opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
preview:
uses: curvenote/actions/.github/workflows/draft.yml@v1
with:
venue: '<VENUE>'
collection: '<COLLECTION>'
kind: '<KIND>'
path: papers/*
exclude: papers/example
id-pattern-regex: '^pyconf-2027-[a-zA-Z0-9_-]{3,40}$'
enforce-single-folder: true
label: draft
secrets:
CURVENOTE: ${{ secrets.CURVENOTE_TOKEN }}
GITHUB: ${{ secrets.GITHUB_TOKEN }}Use a quoted string for a numeric collection, such as '2027'. The options specific to this configuration are:
path: papers/*treats every immediate subfolder ofpapers/as an article. Folders starting with.or_are skipped. Only folders changed by the pull request are built.excludeskips other folders, given as repository-relative paths separated by commas.id-pattern-regexrequires every built article’sproject.idto match a pattern. The run fails with a clear message if one does not.enforce-single-folder: truefails the run when a pull request changes more than one article, or changes files outside the article folders.label: draftruns the workflow only when the pull request carries that label. Thelabeledtrigger type makes adding the label start a run.
3. Add the submit workflow¶
Create .github/workflows/curvenote-submit.yml. It is identical to the preview workflow except for the reusable workflow and the label:
name: Curvenote submit
on:
pull_request_target:
branches: ['main']
types: [labeled, opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
submit:
uses: curvenote/actions/.github/workflows/submit.yml@v1
with:
venue: '<VENUE>'
collection: '<COLLECTION>'
kind: '<KIND>'
path: papers/*
exclude: papers/example
id-pattern-regex: '^pyconf-2027-[a-zA-Z0-9_-]{3,40}$'
enforce-single-folder: true
label: approved
secrets:
CURVENOTE: ${{ secrets.CURVENOTE_TOKEN }}
GITHUB: ${{ secrets.GITHUB_TOKEN }}Both workflows update the same Curvenote Preview comment on the pull request. Adding approved replaces the draft links with the submission’s.
4. Add a publish-all workflow (optional)¶
Create .github/workflows/curvenote-publish-all.yml to publish every article at once, for example on the day the proceedings go live:
name: Publish all articles
on: workflow_dispatch
permissions:
contents: write
jobs:
publish:
uses: curvenote/actions/.github/workflows/submit.yml@v1
with:
venue: '<VENUE>'
collection: '<COLLECTION>'
kind: '<KIND>'
path: papers/*
exclude: papers/example
id-pattern-regex: '^pyconf-2027-[a-zA-Z0-9_-]{3,40}$'
include-unchanged: true
publish: true
secrets:
CURVENOTE: ${{ secrets.CURVENOTE_TOKEN }}
GITHUB: ${{ secrets.GITHUB_TOKEN }}include-unchanged: true builds every article folder, not only the changed ones. publish: true publishes each submission with no review step, so the token’s user needs permission to publish. Run it from the repository’s Actions tab, or with gh workflow run curvenote-publish-all.yml.
Security and forks¶
pull_request_target runs the workflow from your target branch with access to your secrets, against the code in the author’s pull request. That is what lets fork pull requests be previewed, and it is why both workflows are gated by labels:
Review the diff before adding
draftorapproved.New commits pushed to a labeled pull request trigger the workflows again (the
synchronizeevent). Keep reviewing, or remove the label while changes are in progress.Edits to these workflow files take effect only after they are merged into the target branch. A pull request cannot change the workflow that runs on it.
GitHub’s documentation on pull_request_target explains the trade-offs in detail.
Variant: merging means accepted¶
A lab or team repository may have no forks, and merging may itself mean “accepted”. In that case you can drop the labels:
Preview: trigger on
pull_requestand removelabel. Every pull request gets previews and checks.Submit: trigger on
pushtomainwithpermissions: contents: write, and removelabelandenforce-single-folder. Only the article folders changed by the merge are submitted.
Fork pull requests get no preview in this variant, because GitHub withholds secrets from pull_request runs from forks.
Running per year or per issue¶
SciPy keeps one branch per year (2025, 2026) and points every workflow’s branches: at it. It also sets a matching collection and ID prefix (scipy-2026-…). The next year starts from a new branch with updated values, and earlier years keep working unchanged.
Other options, including strict, comment-title and the fonts and image settings, are listed in the Curvenote Actions reference. For a repository that is a single paper, use the single article configuration instead.
- CLI
- Command Line Interface
- ID
- Identifier
- Portable Document Format