66# Nightly auto-compile: rolls accumulated fragments under
77# ``source/<pkg>/changelog.d/`` into per-package ``CHANGELOG.rst`` entries,
88# bumps each ``extension.toml``, deletes consumed fragments, and pushes the
9- # result back to ``develop``. Keeps the develop branch's changelog current
10- # without requiring a maintainer to run ``compile`` by hand.
9+ # result back to each branch in the configured list. Keeps every tracked
10+ # branch's changelog current without requiring a maintainer to run
11+ # ``compile`` by hand.
1112#
12- # The push uses ``CHANGELOG_PAT`` (a personal access token / fine-grained
13- # GitHub App token with ``contents:write`` on this repo) when it's
14- # available so downstream CI runs on the auto-commit. Falls back to
15- # ``GITHUB_TOKEN`` — sufficient for the push itself, but pushes signed
16- # with ``GITHUB_TOKEN`` do not trigger workflow runs on the resulting
17- # commit, which is by design (avoids infinite loops) but means the
18- # Docker / docs rebuild won't re-trigger off the nightly's auto-commit.
13+ # Scheduled workflow — must live on the default branch (``main``) for the
14+ # cron to register. See ``.github/workflows/README.md``.
15+ #
16+ # Adding a branch to the nightly set is a one-line edit to ``env.CRON_BRANCHES``
17+ # below. Each target branch uses its own ``tools/changelog/cli.py`` (the
18+ # same copy the PR gate already runs), so the nightly compile honours the
19+ # same rules that validated the fragments.
20+ #
21+ # The push uses a short-lived GitHub App installation token minted from
22+ # ``CHANGELOG_APP_ID`` + ``CHANGELOG_APP_PRIVATE_KEY`` (repo secrets). The
23+ # App must be installed on this repository with ``contents: write`` and
24+ # added to the bypass-actor list of each target branch's ruleset so the
25+ # auto-commit can push directly without satisfying required-checks /
26+ # required-approval gates.
27+ # Commits signed by an App token (unlike ``GITHUB_TOKEN``) are treated as
28+ # external pushes, so they DO trigger downstream workflow runs (Docker
29+ # rebuild, docs, etc.) without needing a separate PAT.
1930
2031name : Nightly Changelog Compilation
2132
33+ # Branches the nightly cron compiles. Single source of truth — append a
34+ # ref here to extend the nightly set (the active release branch belongs
35+ # here). Each branch must carry ``tools/changelog/cli.py`` and the
36+ # isaaclab-bot App must be in its branch-ruleset bypass list.
37+ # Surrounding whitespace per entry is stripped by the resolver below.
38+ env :
39+ CRON_BRANCHES : develop,release/3.0.0-beta2
40+
2241on :
2342 schedule :
2443 # Run nightly at 5 AM UTC (one hour after daily-compatibility, so we
2544 # don't compete for runner capacity).
2645 - cron : ' 0 5 * * *'
2746 workflow_dispatch :
2847 inputs :
48+ branch :
49+ # Manual trigger is always a single branch. Free-text on purpose
50+ # — scales to any branch (e.g. a new ``release/*`` cut from
51+ # develop) without needing to update the workflow. A branch
52+ # that lacks ``tools/changelog/cli.py`` fails the verify step
53+ # below with a clear error, which is the desired failure mode.
54+ description : ' Branch to compile (e.g. develop, release/3.0.0-beta2). Must carry tools/changelog/cli.py.'
55+ required : true
56+ type : string
2957 dry_run :
3058 description : ' Preview only — do not commit / push'
3159 required : false
3260 type : boolean
3361 default : false
3462
35- concurrency :
36- # Only one nightly compile may be in flight at a time. ``cancel-in-progress``
37- # is intentionally false: if a previous run is still finishing its push, we
38- # queue rather than abort it mid-commit.
39- group : nightly-changelog
40- cancel-in-progress : false
41-
4263permissions :
43- contents : write
64+ # Reduced: the App installation token below carries its own write scope.
65+ # GITHUB_TOKEN only needs read access for the standard checkout machinery.
66+ contents : read
4467
4568jobs :
69+ resolve-branches :
70+ # CSV → JSON array bridge. ``workflow_dispatch`` inputs can only be
71+ # string / bool / choice, so the branch list arrives as a comma-
72+ # separated string; the matrix below needs a JSON list to fan out.
73+ # Mirrors the ``setup-versions`` job in ``daily-compatibility.yml``.
74+ name : Resolve branch list
75+ runs-on : ubuntu-latest
76+ timeout-minutes : 1
77+ outputs :
78+ branches : ${{ steps.b.outputs.branches }}
79+ steps :
80+ - id : b
81+ env :
82+ # Schedule → the CRON_BRANCHES list. Manual → the single branch
83+ # the maintainer entered. The two paths are intentionally
84+ # asymmetric: cron is the configured set, manual is exactly one
85+ # branch (required input).
86+ BRANCHES : ${{ github.event_name == 'schedule' && env.CRON_BRANCHES || inputs.branch }}
87+ # ``EVENT_NAME`` mirrors ``github.event_name`` so the guard below
88+ # branches on it without re-interpolating into the shell.
89+ EVENT_NAME : ${{ github.event_name }}
90+ run : |
91+ # CSV → JSON array, trimming surrounding whitespace per entry.
92+ # Manual produces a 1-element array; cron produces N elements.
93+ arr=$(echo "$BRANCHES" | tr ',' '\n' | xargs -n1 | jq -R . | jq -s -c .)
94+ # Manual trigger contract: exactly one branch. A maintainer who
95+ # pastes a comma-separated list into the dispatch form should
96+ # see a clear error, not a silent multi-branch fan-out.
97+ if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$(echo "$arr" | jq 'length')" -ne 1 ]; then
98+ echo "::error::Manual trigger accepts exactly one branch; got $arr. Fire the workflow separately per branch."
99+ exit 1
100+ fi
101+ echo "branches=$arr" >> "$GITHUB_OUTPUT"
102+ echo "Resolved branches: $arr"
103+
46104 compile-changelog :
47- name : Compile changelog fragments
105+ name : Compile changelog fragments (${{ matrix.branch }})
106+ needs : resolve-branches
48107 runs-on : ubuntu-latest
108+ timeout-minutes : 10
109+ strategy :
110+ # Independent branches: one failing shouldn't cancel the others.
111+ # Each matrix entry shows up as a separate job tile in the Actions
112+ # UI, so ``release/3.0.0-beta2`` failing doesn't hide ``develop``'s
113+ # success (and ``gh run rerun --failed`` re-runs only the failed
114+ # entry). Mirrors ``daily-compatibility.yml``'s matrix style.
115+ fail-fast : false
116+ matrix :
117+ branch : ${{ fromJson(needs.resolve-branches.outputs.branches) }}
118+ concurrency :
119+ # Per-branch group: two runs against the same ref queue, but
120+ # different refs (develop and a release branch) compile in parallel.
121+ group : nightly-changelog-${{ matrix.branch }}
122+ cancel-in-progress : false
49123
50124 steps :
125+ # Mint a short-lived (1 h) installation access token for the
126+ # ``isaaclab-bot`` GitHub App. The App is on each target branch's
127+ # ruleset bypass list, so its push lands without needing the
128+ # standard required-checks / required-approval pipeline.
129+ - uses : actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
130+ id : app-token
131+ with :
132+ # ``client-id`` (the App's OAuth client ID, ``Iv23...``) supersedes
133+ # the deprecated ``app-id`` integer input as of v3.x.
134+ client-id : ${{ secrets.CHANGELOG_APP_CLIENT_ID }}
135+ private-key : ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
136+ # Declare the scope the App must have so token mint fails loudly
137+ # if the App is misconfigured, instead of failing silently at the
138+ # push step.
139+ permission-contents : write
140+
51141 - uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
52142 with :
53- # Operate on develop, not the repo's default branch. Scheduled
54- # workflows fire from the default branch's workflow file by
55- # default, but we want the *checkout* to be develop so the
56- # compile sees develop's accumulated fragments and the push
57- # writes back to develop.
58- ref : develop
59- # Use a PAT so the auto-commit triggers downstream CI; falls back
60- # to GITHUB_TOKEN which is sufficient for the push itself.
61- token : ${{ secrets.CHANGELOG_PAT || secrets.GITHUB_TOKEN }}
143+ # Operate on the target branch, not the repo's default branch.
144+ # Scheduled workflows fire from the default branch's workflow
145+ # file, but the *checkout* is the branch we're compiling so the
146+ # compile sees that branch's accumulated fragments and the push
147+ # writes back to it.
148+ ref : ${{ matrix.branch }}
149+ # App token (vs. GITHUB_TOKEN) means the push is signed by
150+ # ``isaaclab-bot`` — the bypass identity — and downstream CI
151+ # workflows DO trigger on the resulting commit.
152+ token : ${{ steps.app-token.outputs.token }}
62153 # Full history so the compiler can resolve each fragment's merge
63154 # time via ``git log --diff-filter=A --first-parent``.
64155 fetch-depth : 0
65156
66- - uses : actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
157+ - name : Verify changelog tooling exists on target branch
158+ env :
159+ TARGET_BRANCH : ${{ matrix.branch }}
160+ run : |
161+ # Loud-fail this matrix entry (not the whole run — ``fail-fast:
162+ # false`` keeps siblings going) when the target branch lacks
163+ # ``tools/changelog/cli.py``. A red tile is the desired signal:
164+ # a branch in the nightly set without the compile tooling is a
165+ # configuration error, not a "no-op" condition.
166+ if [ ! -f tools/changelog/cli.py ]; then
167+ echo "::error::Branch '$TARGET_BRANCH' is missing tools/changelog/cli.py — drop it from env.CRON_BRANCHES (or the dispatch input) or restore the tooling on that branch."
168+ exit 1
169+ fi
170+
171+ - uses : actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
67172 with :
68173 python-version : " 3.12"
69174
@@ -77,10 +182,20 @@ jobs:
77182 python3 tools/changelog/cli.py compile $ARGS
78183
79184 - name : Commit and push if fragments were compiled
80- if : inputs.dry_run != 'true'
185+ if : ${{ !inputs.dry_run }}
186+ env :
187+ # Pass the matrix branch through an env var rather than
188+ # interpolating ``${{ matrix.branch }}`` directly into ``run:``.
189+ # The interpolation happens *before* shell quoting, so an
190+ # adversarial input could escape the surrounding quotes; the
191+ # env passthrough keeps the value inside the shell's variable
192+ # space where standard quoting protects it.
193+ TARGET_BRANCH : ${{ matrix.branch }}
81194 run : |
82- git config user.name "github-actions[bot]"
83- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
195+ # Author commits as the App's bot user so the GitHub UI attributes
196+ # them correctly. ID 282401363 is isaaclab-bot[bot]'s user ID.
197+ git config user.name "isaaclab-bot[bot]"
198+ git config user.email "282401363+isaaclab-bot[bot]@users.noreply.github.com"
84199 git add source/*/changelog.d/ \
85200 source/*/docs/CHANGELOG.rst \
86201 source/*/config/extension.toml
@@ -113,7 +228,14 @@ jobs:
113228 done
114229 } > "$MSG_FILE"
115230 git commit -F "$MSG_FILE"
116- # Push explicitly to develop so we don't accidentally write
117- # to the source ref of a workflow_dispatch run.
118- git push origin HEAD:develop
231+ # Rebase onto the target branch's current tip in case a human
232+ # commit landed during this run (~2 min window between
233+ # checkout and push). Without this the push fails non-fast-
234+ # forward and the batch waits for the next run. ``refs/heads/``
235+ # is explicit so a same-named tag (if one ever exists) can't
236+ # disambiguate the wrong way.
237+ git pull --rebase origin "refs/heads/$TARGET_BRANCH"
238+ # Push explicitly to the target branch so we don't accidentally
239+ # write to the source ref of a workflow_dispatch run.
240+ git push origin "HEAD:refs/heads/$TARGET_BRANCH"
119241 fi
0 commit comments