Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 118 additions & 28 deletions .github/workflows/nightly-changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
# Nightly auto-compile: rolls accumulated fragments under
# ``source/<pkg>/changelog.d/`` into per-package ``CHANGELOG.rst`` entries,
# bumps each ``extension.toml``, deletes consumed fragments, and pushes the
# result back to ``develop``. Keeps the develop branch's changelog current
# without requiring a maintainer to run ``compile`` by hand.
# result back to each branch in the configured list. Keeps every tracked
# branch's changelog current without requiring a maintainer to run
# ``compile`` by hand.
#
# Scheduled workflow — must live on the default branch (``main``) for the
# cron to register. See ``.github/workflows/README.md``.
#
# Adding a branch to the nightly set is a one-line edit to ``env.CRON_BRANCHES``
# below. Each target branch uses its own ``tools/changelog/cli.py`` (the
# same copy the PR gate already runs), so the nightly compile honours the
# same rules that validated the fragments.
#
# The push uses a short-lived GitHub App installation token minted from
# ``CHANGELOG_APP_ID`` + ``CHANGELOG_APP_PRIVATE_KEY`` (repo secrets). The
# App must be installed on this repository with ``contents: write`` and
# added to the bypass-actor list of ``develop``'s branch ruleset so the
# added to the bypass-actor list of each target branch's ruleset so the
# auto-commit can push directly without satisfying required-checks /
# required-approval gates.
# Commits signed by an App token (unlike ``GITHUB_TOKEN``) are treated as
Expand All @@ -24,42 +30,102 @@

name: Nightly Changelog Compilation

# Branches the nightly cron compiles. Single source of truth — append a
# ref here to extend the nightly set (the active release branch belongs
# here). Each branch must carry ``tools/changelog/cli.py`` and the
# isaaclab-bot App must be in its branch-ruleset bypass list.
# Surrounding whitespace per entry is stripped by the resolver below.
env:
CRON_BRANCHES: develop,release/3.0.0-beta2

on:
schedule:
# Run nightly at 5 AM UTC (one hour after daily-compatibility, so we
# don't compete for runner capacity).
- cron: '0 5 * * *'
workflow_dispatch:
inputs:
branch:
# Manual trigger is always a single branch. Free-text on purpose
# — scales to any branch (e.g. a new ``release/*`` cut from
# develop) without needing to update the workflow. A branch
# that lacks ``tools/changelog/cli.py`` fails the verify step
# below with a clear error, which is the desired failure mode.
description: 'Branch to compile (e.g. develop, release/3.0.0-beta2). Must carry tools/changelog/cli.py.'
required: true
type: string
dry_run:
description: 'Preview only — do not commit / push'
required: false
type: boolean
default: false

concurrency:
# Only one nightly compile may be in flight at a time. ``cancel-in-progress``
# is intentionally false: if a previous run is still finishing its push, we
# queue rather than abort it mid-commit.
group: nightly-changelog
cancel-in-progress: false

permissions:
# Reduced: the App installation token below carries its own write scope.
# GITHUB_TOKEN only needs read access for the standard checkout machinery.
contents: read

jobs:
resolve-branches:
# CSV → JSON array bridge. ``workflow_dispatch`` inputs can only be
# string / bool / choice, so the branch list arrives as a comma-
# separated string; the matrix below needs a JSON list to fan out.
# Mirrors the ``setup-versions`` job in ``daily-compatibility.yml``.
name: Resolve branch list
runs-on: ubuntu-latest
timeout-minutes: 1
outputs:
branches: ${{ steps.b.outputs.branches }}
steps:
- id: b
env:
# Schedule → the CRON_BRANCHES list. Manual → the single branch
# the maintainer entered. The two paths are intentionally
# asymmetric: cron is the configured set, manual is exactly one
# branch (required input).
BRANCHES: ${{ github.event_name == 'schedule' && env.CRON_BRANCHES || inputs.branch }}
# ``EVENT_NAME`` mirrors ``github.event_name`` so the guard below
# branches on it without re-interpolating into the shell.
EVENT_NAME: ${{ github.event_name }}
run: |
# CSV → JSON array, trimming surrounding whitespace per entry.
# Manual produces a 1-element array; cron produces N elements.
arr=$(echo "$BRANCHES" | tr ',' '\n' | xargs -n1 | jq -R . | jq -s -c .)
# Manual trigger contract: exactly one branch. A maintainer who
# pastes a comma-separated list into the dispatch form should
# see a clear error, not a silent multi-branch fan-out.
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$(echo "$arr" | jq 'length')" -ne 1 ]; then
echo "::error::Manual trigger accepts exactly one branch; got $arr. Fire the workflow separately per branch."
exit 1
fi
echo "branches=$arr" >> "$GITHUB_OUTPUT"
echo "Resolved branches: $arr"
Comment on lines +80 to +102
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Comma in inputs.branch silently fans out to multiple branches on a manual run

The PR description says "Manual trigger is always a single branch", and the branch input description reinforces this, but the resolve-branches step applies the same CSV-split-and-trim logic regardless of event type. A maintainer who types develop,release/3.0.0-beta2 in the dispatch form gets a two-branch matrix run instead of a clear error. If the intent is strictly one branch for manual runs, adding a guard — e.g., checking that the resolved array length is 1 when github.event_name == 'workflow_dispatch' — would surface the mistake early rather than silently expanding the run.


compile-changelog:
name: Compile changelog fragments
name: Compile changelog fragments (${{ matrix.branch }})
needs: resolve-branches
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
# Independent branches: one failing shouldn't cancel the others.
# Each matrix entry shows up as a separate job tile in the Actions
# UI, so ``release/3.0.0-beta2`` failing doesn't hide ``develop``'s
# success (and ``gh run rerun --failed`` re-runs only the failed
# entry). Mirrors ``daily-compatibility.yml``'s matrix style.
fail-fast: false
matrix:
branch: ${{ fromJson(needs.resolve-branches.outputs.branches) }}
concurrency:
# Per-branch group: two runs against the same ref queue, but
# different refs (develop and a release branch) compile in parallel.
group: nightly-changelog-${{ matrix.branch }}
cancel-in-progress: false

steps:
# Mint a short-lived (1 h) installation access token for the
# ``isaaclab-bot`` GitHub App. The App is on develop's branch-ruleset
# bypass list, so its push lands without needing the standard
# required-checks / required-approval pipeline.
# ``isaaclab-bot`` GitHub App. The App is on each target branch's
# ruleset bypass list, so its push lands without needing the
# standard required-checks / required-approval pipeline.
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
id: app-token
with:
Expand All @@ -74,12 +140,12 @@ jobs:

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# Operate on develop, not the repo's default branch. Scheduled
# workflows fire from the default branch's workflow file by
# default, but we want the *checkout* to be develop so the
# compile sees develop's accumulated fragments and the push
# writes back to develop.
ref: develop
# Operate on the target branch, not the repo's default branch.
# Scheduled workflows fire from the default branch's workflow
# file, but the *checkout* is the branch we're compiling so the
# compile sees that branch's accumulated fragments and the push
# writes back to it.
ref: ${{ matrix.branch }}
# App token (vs. GITHUB_TOKEN) means the push is signed by
# ``isaaclab-bot`` — the bypass identity — and downstream CI
# workflows DO trigger on the resulting commit.
Expand All @@ -88,6 +154,20 @@ jobs:
# time via ``git log --diff-filter=A --first-parent``.
fetch-depth: 0

- name: Verify changelog tooling exists on target branch
env:
TARGET_BRANCH: ${{ matrix.branch }}
run: |
# Loud-fail this matrix entry (not the whole run — ``fail-fast:
# false`` keeps siblings going) when the target branch lacks
# ``tools/changelog/cli.py``. A red tile is the desired signal:
# a branch in the nightly set without the compile tooling is a
# configuration error, not a "no-op" condition.
if [ ! -f tools/changelog/cli.py ]; then
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."
exit 1
fi

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
Expand All @@ -103,6 +183,14 @@ jobs:

- name: Commit and push if fragments were compiled
if: ${{ !inputs.dry_run }}
env:
# Pass the matrix branch through an env var rather than
# interpolating ``${{ matrix.branch }}`` directly into ``run:``.
# The interpolation happens *before* shell quoting, so an
# adversarial input could escape the surrounding quotes; the
# env passthrough keeps the value inside the shell's variable
# space where standard quoting protects it.
TARGET_BRANCH: ${{ matrix.branch }}
run: |
# Author commits as the App's bot user so the GitHub UI attributes
# them correctly. ID 282401363 is isaaclab-bot[bot]'s user ID.
Expand Down Expand Up @@ -140,12 +228,14 @@ jobs:
done
} > "$MSG_FILE"
git commit -F "$MSG_FILE"
# Rebase onto develop's current tip in case a human commit
# landed during this run (~2 min window between checkout and
# push). Without this the push fails non-fast-forward and the
# batch waits for the next run.
git pull --rebase origin develop
# Push explicitly to develop so we don't accidentally write
# to the source ref of a workflow_dispatch run.
git push origin HEAD:develop
# Rebase onto the target branch's current tip in case a human
# commit landed during this run (~2 min window between
# checkout and push). Without this the push fails non-fast-
# forward and the batch waits for the next run. ``refs/heads/``
# is explicit so a same-named tag (if one ever exists) can't
# disambiguate the wrong way.
git pull --rebase origin "refs/heads/$TARGET_BRANCH"
# Push explicitly to the target branch so we don't accidentally
# write to the source ref of a workflow_dispatch run.
git push origin "HEAD:refs/heads/$TARGET_BRANCH"
fi
Loading