diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 7df2171..046e0f8 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -12,30 +12,23 @@ jobs: changelog: if: ${{ !endsWith(github.actor, '[bot]') }} runs-on: ubuntu-latest - permissions: {} # all repo access comes from the minted App token below + # Only the newest regeneration may land: an older one sees a stale tree. + # A cancel between mint and land leaves its one-hour token unrevoked. + concurrency: + group: changelog + cancel-in-progress: true + environment: soothfast-bot + permissions: + contents: read + id-token: write # soothfast-bot token via the broker, see bot/ steps: - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: audit - - name: Mint a soothfast-bot token - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - client-id: ${{ vars.CHANGELOG_APP_CLIENT_ID }} - private-key: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }} - permission-contents: write - permission-pull-requests: write - - name: Get soothfast-bot user id - id: app-user - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - run: echo "id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # zizmor: ignore[artipacked] + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 # surface diff + previous-tag lookup need history - ref: ${{ github.ref_name }} - token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: toolchain: stable @@ -59,22 +52,16 @@ jobs: # shellcheck disable=SC2086 # both are deliberate flag lists cargo run --release -p cargo-soothfast -- \ report changelog $PKGS --baseline self $AGAINST - - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - id: cpr - with: - token: ${{ steps.app-token.outputs.token }} - add-paths: CHANGELOG.md - commit-message: "docs: regenerate CHANGELOG.md" - title: "docs: regenerate CHANGELOG.md" - body: Automated CHANGELOG regeneration. - branch: bot/changelog-update - delete-branch: true - author: >- - ${{ steps.app-token.outputs.app-slug }}[bot] - <${{ steps.app-user.outputs.id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com> - - name: Merge pull request - if: steps.cpr.outputs.pull-request-number + # Minted after the build so no step that compiles the tree holds it. + - name: Mint a soothfast-bot token + id: bot + run: action/bot-token.sh + - name: Land the regenerated changelog env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }} - run: gh pr merge --auto --squash --delete-branch "$PR_NUMBER" + TOKEN: ${{ steps.bot.outputs.token }} + APP_SLUG: ${{ steps.bot.outputs.app_slug }} + BRANCH: bot/changelog-update + TITLE: "docs: regenerate CHANGELOG.md" + BODY: Automated CHANGELOG regeneration. + PATHS: CHANGELOG.md + run: action/land.sh diff --git a/action/land.sh b/action/land.sh new file mode 100755 index 0000000..68f2f7d --- /dev/null +++ b/action/land.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Commit PATHS as APP_SLUG[bot], push them to BRANCH, open or refresh its +# pull request against the default branch, and merge it: auto-merge when the +# default branch has a rule that blocks merging, immediately otherwise. The +# working tree keeps the regenerated files but HEAD is left where it was. +# TOKEN is revoked on every exit path. +# Inputs: TOKEN APP_SLUG BRANCH TITLE BODY PATHS (space separated pathspecs). +set -euo pipefail + +: "${TOKEN:?}" "${APP_SLUG:?}" "${BRANCH:?}" "${TITLE:?}" "${PATHS:?}" +export GH_TOKEN="$TOKEN" +trap 'gh api -X DELETE /installation/token >/dev/null || true' EXIT + +read -ra paths <<<"$PATHS" +git add -- "${paths[@]}" +if git diff --cached --quiet; then + echo "land: nothing to commit" + exit 0 +fi + +bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) +head=$(git rev-parse HEAD) +git -c "user.name=${APP_SLUG}[bot]" \ + -c "user.email=${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com" \ + commit -q -m "$TITLE" +# shellcheck disable=SC2016 # git expands $TOKEN when it runs the helper +git -c credential.helper= \ + -c credential.helper='!f() { echo "username=x-access-token"; echo "password=$TOKEN"; }; f' \ + push --force "https://github.com/${GITHUB_REPOSITORY}" "HEAD:refs/heads/${BRANCH}" +git reset -q --soft "$head" + +default=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) +pr=$(gh pr list --head "$BRANCH" --base "$default" --state open --json number --jq '.[0].number // empty') +if [ -z "$pr" ]; then + url=$(gh pr create --head "$BRANCH" --base "$default" --title "$TITLE" --body "${BODY:-}") + pr=${url##*/} +else + gh pr edit "$pr" --title "$TITLE" --body "${BODY:-}" >/dev/null +fi + +# History-only rules (deletion, linear history) leave the PR clean, and +# --auto refuses a PR with nothing to wait for. +blocking=$(gh api "repos/${GITHUB_REPOSITORY}/rules/branches/${default}" \ + --jq '[.[] | select(.type == "required_status_checks" or .type == "pull_request" or .type == "merge_queue")] | length') +if [ "$blocking" -gt 0 ]; then + gh pr merge --auto --squash --delete-branch "$pr" +else + gh pr merge --squash --delete-branch "$pr" +fi