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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:
- "flake.nix"
- "flake.lock"
- ".github/workflows/ci.yml"
- ".github/workflows/release-pr.yml"
pull_request:
branches: [main]
paths:
Expand All @@ -29,6 +30,7 @@ on:
- "flake.nix"
- "flake.lock"
- ".github/workflows/ci.yml"
- ".github/workflows/release-pr.yml"

jobs:
fmt:
Expand Down
68 changes: 62 additions & 6 deletions .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.repository.default_branch }}
token: ${{ steps.app-token.outputs.token }}

- name: Skip release commits
Expand Down Expand Up @@ -139,16 +140,71 @@ jobs:
if: steps.skip.outputs.skip == 'false' && steps.changes.outputs.ahead != '0' && steps.version-check.outputs.skip == 'false'
id: release-branch
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
RELEASE_TAG: ${{ steps.next-version.outputs.content }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
branch="release/next"
source_commit="$(git rev-parse HEAD)"

git fetch origin "${DEFAULT_BRANCH}"
current_source_commit="$(git rev-parse FETCH_HEAD)"
if [ "${source_commit}" != "${current_source_commit}" ]; then
echo "${DEFAULT_BRANCH} advanced during this run; a newer workflow will update the release PR"
exit 0
fi

git checkout -B "${branch}"
bot_name='${{ steps.app-token.outputs.app-slug }}[bot]'
bot_id="$(gh api "/users/${bot_name}" --jq .id)"
git config user.name "${bot_name}"
git config user.email "${bot_id}+${bot_name}@users.noreply.github.com"

generated_dir="$(mktemp -d)"
trap 'rm -rf "${generated_dir}"' EXIT
cp CHANGELOG.md Cargo.toml Cargo.lock RELEASE_NOTES.md "${generated_dir}/"
git restore CHANGELOG.md Cargo.toml Cargo.lock
rm RELEASE_NOTES.md

if git ls-remote --exit-code --heads origin "${branch}" >/dev/null; then
git fetch origin "${branch}"
git checkout -B "${branch}" FETCH_HEAD
else
previous_pr="$(gh pr list \
--base "${DEFAULT_BRANCH}" \
--head "${branch}" \
--state all \
--limit 1 \
--json number,updatedAt \
--jq 'sort_by(.updatedAt) | last | .number // empty')"

if [ -n "${previous_pr}" ]; then
echo "Recovering ${branch} from pull request #${previous_pr}"
git fetch origin "refs/pull/${previous_pr}/head"
git checkout -b "${branch}" FETCH_HEAD
else
git checkout -b "${branch}" "${source_commit}"
fi
fi

if ! git merge --no-edit "${source_commit}"; then
if ! git rev-parse --verify --quiet MERGE_HEAD >/dev/null; then
exit 1
fi

conflicts="$(git diff --name-only --diff-filter=U)"
unexpected_conflicts="$(printf '%s\n' "${conflicts}" | grep -Ev '^(CHANGELOG\.md|Cargo\.toml|Cargo\.lock)$' || true)"
if [ -n "${unexpected_conflicts}" ]; then
echo "Unexpected conflicts while merging ${source_commit}:"
printf '%s\n' "${unexpected_conflicts}"
exit 1
fi

git checkout "${source_commit}" -- CHANGELOG.md Cargo.toml Cargo.lock
git add CHANGELOG.md Cargo.toml Cargo.lock
git commit --no-edit
fi

cp "${generated_dir}"/* .
git add CHANGELOG.md Cargo.toml Cargo.lock

if git diff --cached --quiet; then
Expand All @@ -157,7 +213,7 @@ jobs:
git commit -m "chore(release): ${RELEASE_TAG}"
fi

git push --force-with-lease origin "${branch}"
git push origin "${branch}"
echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Mark release build as pending
Expand All @@ -174,7 +230,7 @@ jobs:
-f description='Building installable release packages'

- name: Create PR body
if: steps.skip.outputs.skip == 'false' && steps.changes.outputs.ahead != '0' && steps.version-check.outputs.skip == 'false'
if: steps.release-branch.outputs.commit != ''
env:
RELEASE_TAG: ${{ steps.next-version.outputs.content }}
AHEAD: ${{ steps.changes.outputs.ahead }}
Expand All @@ -195,7 +251,7 @@ jobs:
EOF

- name: Find existing release PR
if: steps.skip.outputs.skip == 'false' && steps.changes.outputs.ahead != '0' && steps.version-check.outputs.skip == 'false'
if: steps.release-branch.outputs.commit != ''
id: existing-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
Expand All @@ -210,7 +266,7 @@ jobs:
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"

- name: Create release PR
if: steps.skip.outputs.skip == 'false' && steps.changes.outputs.ahead != '0' && steps.version-check.outputs.skip == 'false' && steps.existing-pr.outputs.pr_number == ''
if: steps.release-branch.outputs.commit != '' && steps.existing-pr.outputs.pr_number == ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RELEASE_TAG: ${{ steps.next-version.outputs.content }}
Expand All @@ -222,7 +278,7 @@ jobs:
--body-file /tmp/release-pr-body.md

- name: Update existing release PR
if: steps.skip.outputs.skip == 'false' && steps.changes.outputs.ahead != '0' && steps.version-check.outputs.skip == 'false' && steps.existing-pr.outputs.pr_number != ''
if: steps.release-branch.outputs.commit != '' && steps.existing-pr.outputs.pr_number != ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RELEASE_TAG: ${{ steps.next-version.outputs.content }}
Expand Down
4 changes: 2 additions & 2 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Shosai releases are prepared from Conventional Commit titles and reviewed throug
## Release flow

1. Each pull request title must use an allowed Conventional Commit type. The repository must use squash merges with the pull request title as the resulting commit subject.
2. A push to `main` updates `release/next`. Git-cliff calculates the next semantic version, updates the workspace version and lockfile, regenerates `CHANGELOG.md`, and creates or refreshes the release pull request.
2. A push to `main` updates `release/next`. The workflow merges the new `main` revision into the existing release branch, then appends a release commit instead of rewriting branch history. If the branch was deleted after an earlier pull request, the workflow recovers its last pull-request head before appending. Git-cliff calculates the next semantic version, updates the workspace version and lockfile, regenerates `CHANGELOG.md`, and creates or refreshes the release pull request.
3. The exact `release/next` commit is dry-run through the full three-platform package matrix with publishing disabled. The release PR receives a `Release build dry run` commit status, so it cannot be mistaken for a releasable revision while packages are pending or failing. Ordinary feature pull requests do not run this release matrix.
4. Merging that pull request into the default branch validates that its `chore(release): vX.Y.Z` title matches the workspace version and creates the tag on the merge commit.
5. Release jobs repeat the same builds, bundle PDFium, and attach installable packages and checksums to the GitHub release. A manually pushed `vX.Y.Z` tag runs the same publisher after validating the tag against the workspace version.
Expand Down Expand Up @@ -49,7 +49,7 @@ The workflows rely on these GitHub settings:
- Require release pull requests to be up to date before merging, or use a merge queue, so the reviewed changelog covers every commit in the tagged merge.
- Install the private `chaba2-bot` GitHub App on this repository with `Contents: write`, `Pull requests: write`, and `Commit statuses: write` repository permissions.
- Create the `RELEASE` Actions environment, store the App ID in its `RELEASE_APP_ID` variable, and store the PEM private key in its `RELEASE_APP_SECRET` secret.
- Allow `chaba2-bot` to update the `release/next` branch with force-with-lease.
- Allow `chaba2-bot` to push updates to the `release/next` branch.

Only mutation jobs enter the `RELEASE` environment and receive its credentials. Release package builds, including dry runs, remain credential-free. Mutation jobs exchange the credentials for short-lived, repository-scoped `chaba2-bot` installation tokens: release commits and branch pushes, release pull-request creation and updates, dry-run statuses, tags, and GitHub releases. The built-in `GITHUB_TOKEN` is read-only. App-authored branch and tag events are not suppressed by GitHub's recursive-workflow protection, so the normal PR checks and tag workflow can run.

Expand Down
Loading