Skip to content
Merged
Show file tree
Hide file tree
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
49 changes: 49 additions & 0 deletions .github/workflows/changeset-release-pr.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const indexOf = (text) => {
return index;
};

test("version PR mutations use latest-run branch concurrency", () => {
assert.match(
workflow,
/concurrency:\n {6}group: changeset-release-pr-\$\{\{ github\.ref \}\}\n {6}cancel-in-progress: true/,
);
});

test("Rust Wasm preparation is explicit and disabled by default", () => {
const input = workflow.match(
/ {6}prepare-rust-wasm:\n[\s\S]+?(?= {4}secrets:)/,
Expand Down Expand Up @@ -50,3 +57,45 @@ test("tool installation precedes the write-capable release credential", () => {
const token = indexOf("name: Mint version PR token");
assert.ok(preparation < token);
});

test("stale source revisions cannot mint credentials or mutate release PRs", () => {
const freshness = indexOf("name: Check release source is current");
const token = indexOf("name: Mint version PR token");
const release = indexOf("name: Create or update version packages PR");

assert.ok(freshness < token);
assert.ok(freshness < release);
assert.match(workflow, /GH_TOKEN: \$\{\{ github\.token \}\}/);
assert.match(
workflow,
/git\/ref\/heads\/\$GITHUB_REF_NAME[\s\S]+?if \[\[ "\$current_sha" == "\$GITHUB_SHA" \]\]/,
);
assert.match(
workflow,
/if: steps\.source\.outputs\.current == 'true'\n {8}name: Mint version PR token/,
);
assert.match(
workflow,
/if: steps\.source\.outputs\.current == 'true'\n {8}name: Create or update version packages PR/,
);
});

test("a current no-op run removes the stale release PR and branch", () => {
const cleanup = workflow.match(
/ {6}- if: >-\n {10}steps\.source\.outputs\.current == 'true'[\s\S]+?(?=\n {6}- |\n\S|$)/,
)?.[0];

assert.ok(cleanup, "missing stale release cleanup step");
assert.match(
cleanup,
/steps\.changesets\.outputs\.pullRequestNumber == ''/,
);
assert.match(cleanup, /name: Remove stale version packages PR/);
assert.match(cleanup, /current_sha="\$\(\n {12}gh api/);
assert.match(cleanup, /if \[\[ "\$current_sha" != "\$GITHUB_SHA" \]\]/);
assert.match(cleanup, /-f base="\$BASE_BRANCH"/);
assert.match(cleanup, /-f head="\$owner:\$RELEASE_BRANCH"/);
assert.match(cleanup, /-f per_page=1/);
assert.match(cleanup, /-f state=closed/);
assert.match(cleanup, /git\/refs\/heads\/\$RELEASE_BRANCH/);
});
81 changes: 79 additions & 2 deletions .github/workflows/changeset-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
name: Maintain version packages PR
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency:
group: changeset-release-pr-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -135,8 +138,29 @@ jobs:
chmod 0700 "$version_command"
printf 'path=%s\n' "$version_command" >> "$GITHUB_OUTPUT"

- name: Check release source is current
id: source
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
current_sha="$(
gh api \
"repos/$GITHUB_REPOSITORY/git/ref/heads/$GITHUB_REF_NAME" \
--jq '.object.sha'
)"
if [[ "$current_sha" == "$GITHUB_SHA" ]]; then
echo "current=true" >> "$GITHUB_OUTPUT"
Comment thread
jan-kubica marked this conversation as resolved.
exit 0
fi

echo "current=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping version PR update because $GITHUB_SHA is no longer the head of $GITHUB_REF_NAME."

# Install hooks run before a write-capable credential exists in the job.
- name: Mint version PR token
- if: steps.source.outputs.current == 'true'
name: Mint version PR token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
Expand All @@ -147,7 +171,9 @@ jobs:
permission-contents: write
permission-pull-requests: write

- name: Create or update version packages PR
- if: steps.source.outputs.current == 'true'
name: Create or update version packages PR
id: changesets
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
version: bash ${{ steps.version-command.outputs.path }}
Expand All @@ -159,3 +185,54 @@ jobs:
CARGO_SYNC_SCRIPT: ${{ steps.cargo-sync.outputs.script }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
SYNC_CARGO_INHERITED_LOCK: ${{ inputs.sync-cargo-inherited-lock }}

- if: >-
steps.source.outputs.current == 'true' &&
steps.changesets.outputs.pullRequestNumber == ''
name: Remove stale version packages PR
shell: bash
env:
BASE_BRANCH: ${{ github.ref_name }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RELEASE_BRANCH: changeset-release/${{ github.ref_name }}
run: |
set -euo pipefail
current_sha="$(
gh api \
"repos/$GITHUB_REPOSITORY/git/ref/heads/$BASE_BRANCH" \
--jq '.object.sha'
)"
if [[ "$current_sha" != "$GITHUB_SHA" ]]; then
echo "::notice::Skipping stale version PR cleanup because $GITHUB_SHA is no longer the head of $BASE_BRANCH."
exit 0
fi

owner="${GITHUB_REPOSITORY%%/*}"
pr_number="$(
gh api \
--method GET \
"repos/$GITHUB_REPOSITORY/pulls" \
-f base="$BASE_BRANCH" \
-f head="$owner:$RELEASE_BRANCH" \
-f per_page=1 \
-f state=open \
--jq '.[0].number // empty'
)"
if [[ -n "$pr_number" ]]; then
gh api \
--method PATCH \
"repos/$GITHUB_REPOSITORY/pulls/$pr_number" \
-f state=closed \
--silent
echo "::notice::Closed stale version PR #$pr_number."
fi

if gh api \
"repos/$GITHUB_REPOSITORY/git/ref/heads/$RELEASE_BRANCH" \
--silent >/dev/null 2>&1; then
gh api \
--method DELETE \
"repos/$GITHUB_REPOSITORY/git/refs/heads/$RELEASE_BRANCH" \
--silent
echo "::notice::Deleted stale branch $RELEASE_BRANCH."
fi
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ jobs:
```

The caller must provide `changeset`, `changeset:version`, and a `.changeset/config.json`.
The shared workflow serializes each target branch with latest-run concurrency and
skips write-capable steps when its trigger commit is no longer the branch head. A
current no-op run closes any stale version PR and deletes its generated branch, so
interrupted runs converge without a duplicate release PR. Callers may use matching
workflow-level concurrency to cancel obsolete setup work before the reusable job
starts.

For hybrid repositories, `changeset:version` must synchronize the selected package
version into every npm, Cargo manifest, Python, and central `VERSION` surface. With
`sync-cargo-inherited-lock` enabled, the shared workflow validates the incoming
Expand Down
Loading