Skip to content

fix(wrapper): anchor the server retry hint and never exit 0 without a… #190

fix(wrapper): anchor the server retry hint and never exit 0 without a…

fix(wrapper): anchor the server retry hint and never exit 0 without a… #190

name: Changelog append
on:
push:
branches: [main]
paths-ignore:
- 'CHANGELOG.md'
- 'LICENSE'
- '.gitignore'
- '.gitattributes'
- '.editorconfig'
workflow_dispatch:
inputs:
range:
description: 'Override commit range (default: HEAD~1..HEAD).'
required: false
default: ''
permissions:
contents: write
concurrency:
group: changelog-append
cancel-in-progress: false
jobs:
append:
if: ${{ github.event_name == 'workflow_dispatch' || (github.actor != 'github-actions[bot]' && !contains(github.event.head_commit.message, '[skip changelog]')) }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Compute push range
id: range
shell: bash
env:
DISPATCH_RANGE: ${{ inputs.range }}
run: |
set -euo pipefail
# workflow_dispatch path: honour the manual range input if set,
# otherwise default to HEAD~1..HEAD (last commit).
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [[ -n "$DISPATCH_RANGE" ]]; then
range="$DISPATCH_RANGE"
echo "workflow_dispatch with input range: $range"
else
range="HEAD~1..HEAD"
echo "workflow_dispatch with default range: $range"
fi
echo "range=$range" >> "$GITHUB_OUTPUT"
exit 0
fi
before='${{ github.event.before }}'
after='${{ github.event.after }}'
# First push to a branch arrives with before=000... -- fall back to
# the parent of the current commit so we still process the head.
if [[ "$before" =~ ^0+$ ]]; then
echo "First-push detection (before is zeros) -- using HEAD~1..HEAD."
range="HEAD~1..HEAD"
else
# Force-push edge case: if before isn't an ancestor, the simple
# before..after range can include unrelated commits. Detect and
# fall back to single-commit mode.
if git merge-base --is-ancestor "$before" "$after" 2>/dev/null; then
range="$before..$after"
else
echo "before $before is not an ancestor of $after (force-push?) -- using HEAD~1..HEAD."
range="HEAD~1..HEAD"
fi
fi
echo "range=$range" >> "$GITHUB_OUTPUT"
echo "Computed range: $range"
- name: Append commits to CHANGELOG
id: append
shell: pwsh
run: |
./.github/scripts/Update-Changelog.ps1 -Mode Append -Range '${{ steps.range.outputs.range }}'
# Mark whether anything actually changed so the next steps can skip.
$changed = & git status --porcelain CHANGELOG.md
if ([string]::IsNullOrWhiteSpace($changed)) {
"changed=false" >> $env:GITHUB_OUTPUT
Write-Host "No changelog updates produced."
} else {
"changed=true" >> $env:GITHUB_OUTPUT
Write-Host "Changelog updates ready to commit."
& git --no-pager diff --stat CHANGELOG.md
}
- name: Commit via createCommitOnBranch (verified)
if: steps.append.outputs.changed == 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF_NAME: ${{ github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
run: |
set -euo pipefail
# Use the GraphQL createCommitOnBranch mutation instead of a
# plain `git commit && git push`. Mutation-authored commits are
# signed server-side with GitHub's bot key, so the resulting
# commit is verified -- which is required by the protected-branch
# rule on main. Plain push of an unsigned commit gets GH006.
# The author + committer default to the actor of GITHUB_TOKEN
# (github-actions[bot]) so the commit identity matches the
# convention.
short_sha="${GITHUB_SHA::7}"
expected_oid=$(git rev-parse HEAD)
changelog_b64=$(base64 -w 0 CHANGELOG.md)
headline="docs(changelog): auto-append entries from ${short_sha} [skip changelog]"
body=$(printf '%s\n%s' \
'Maintained by .github/workflows/changelog-append.yml.' \
'See .github/scripts/Update-Changelog.ps1 for the parsing rules.')
payload=$(jq -n \
--arg repo "$GITHUB_REPOSITORY" \
--arg branch "$GITHUB_REF_NAME" \
--arg headline "$headline" \
--arg body "$body" \
--arg oid "$expected_oid" \
--arg changelog "$changelog_b64" \
'{
query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }",
variables: {
input: {
branch: { repositoryNameWithOwner: $repo, branchName: $branch },
message: { headline: $headline, body: $body },
fileChanges: {
additions: [
{ path: "CHANGELOG.md", contents: $changelog }
]
},
expectedHeadOid: $oid
}
}
}')
response=$(printf '%s' "$payload" | gh api graphql --input -)
echo "$response" | jq .
# gh api exits non-zero on transport errors but a successful 200
# with a GraphQL `errors` field passes through silently. Surface it.
if echo "$response" | jq -e '.errors // empty' >/dev/null; then
echo "::error::createCommitOnBranch returned GraphQL errors"
exit 1
fi
new_oid=$(echo "$response" | jq -r '.data.createCommitOnBranch.commit.oid')
new_url=$(echo "$response" | jq -r '.data.createCommitOnBranch.commit.url')
if [[ -z "$new_oid" || "$new_oid" == "null" ]]; then
echo "::error::createCommitOnBranch did not return a commit oid"
exit 1
fi
echo "Verified commit: $new_oid"
echo "URL: $new_url"
# Belt + braces: confirm GitHub recorded the commit as verified.
# Should always be true for a createCommitOnBranch output; if not,
# the protected-branch rule would fire on the next workflow run
# and we'd rather see it here.
verified=$(gh api "repos/$GITHUB_REPOSITORY/commits/$new_oid" --jq '.commit.verification.verified')
if [[ "$verified" != "true" ]]; then
echo "::error::Commit $new_oid is not verified (got: $verified)"
gh api "repos/$GITHUB_REPOSITORY/commits/$new_oid" --jq '.commit.verification'
exit 1
fi
echo "Verification: ok"