refactor(release): direct push to main instead of PR-and-merge dance

The prepare job was opening a release branch, creating a PR, and
immediately squash-merging it via gh CLI. That pattern was carried over
from the old release-pr.sh, but in this workflow it earns nothing:

- main has no branch protection, so direct push is allowed.
- The PR was force-merged in the same step (no `--auto`, no waiting for
  required checks), so it provided no pre-merge validation.
- Discoverability is fine without a PR — git tag, GitHub release, workflow
  run, and main commit are all visible.

The PR-and-merge sequence had ~5 GitHub API roundtrips, each a potential
failure point, plus added 30+ lines of YAML and required pull-requests:
write on the job. Replaced with a straight-line commit + tag + push:

  git fetch origin main && git checkout main && git reset --hard origin/main
  ... bump pkg.json, regen CHANGELOG, npm install ...
  git add ... && git commit && git tag -a && git push origin main "$TAG"

Job permissions tightened to contents: write only.

Race condition note: if main advances between fetch and push, the push
fails non-fast-forward — same outcome as the old PR pattern, where the
auto-merge would have failed. For workflow_dispatch releases with the
maintainer actively driving, the race is essentially never going to
happen, and the recovery (re-run the workflow) is the same.

The dry-run path still bumps + regenerates CHANGELOG inside the runner
so the maintainer can verify the computed values look right, just
without committing or pushing anything.
This commit is contained in:
José Almeida
2026-05-04 23:57:58 +01:00
parent 693969453d
commit 53c2e4503e
+35 -48
View File
@@ -44,11 +44,10 @@ permissions:
jobs:
prepare:
name: Prepare release (bump version, PR, tag)
name: Prepare release (bump version, commit, tag)
runs-on: ubuntu-latest
permissions:
contents: write # push release branch + tag
pull-requests: write # create + merge release PR
contents: write # commit + push to main, push tag
outputs:
version: ${{ steps.compute.outputs.version }}
tag: ${{ steps.compute.outputs.tag }}
@@ -152,65 +151,53 @@ jobs:
exit 1
fi
- name: Bump version
env:
NEXT: ${{ steps.compute.outputs.version }}
run: |
set -euo pipefail
# Bump only the main version. optionalDependencies stay pinned to
# 0.0.0-bootstrap in the committed source — the publish-npm step
# rewrites them to the real version in-flight before publishing,
# once the matching @kdlbs/runtime-* tarballs actually exist on npm.
(cd apps/cli && npm version --no-git-tag-version "$NEXT")
pnpm -C apps install --no-frozen-lockfile
- name: Generate CHANGELOG
env:
TAG: ${{ steps.compute.outputs.tag }}
run: git-cliff --tag "$TAG" -o CHANGELOG.md
- name: Create release PR + merge + tag (or dry-run)
- name: Bump version, generate CHANGELOG, commit, tag, push
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEXT: ${{ steps.compute.outputs.version }}
TAG: ${{ steps.compute.outputs.tag }}
run: |
set -euo pipefail
BRANCH="release/$TAG"
MSG="release: $NEXT"
git checkout -b "$BRANCH"
git add apps/cli/package.json CHANGELOG.md apps/pnpm-lock.yaml || true
if git diff --cached --quiet; then
echo "No changes to commit; aborting." >&2
exit 1
fi
git commit -m "$MSG"
git push -u origin "$BRANCH"
PR_URL="$(gh pr create \
--title "$MSG" \
--body "Automated release: \`$NEXT\`" \
--head "$BRANCH" \
--base main)"
echo "PR: $PR_URL"
gh pr merge "$BRANCH" --squash --delete-branch --subject "$MSG" --body ""
# Pull the squashed commit into local main, then tag from it.
# Refresh from main right before pushing so a concurrent merge
# doesn't get clobbered. main has no branch protection, so we can
# commit + push directly without a PR-and-merge dance.
git fetch origin main
git checkout main
git reset --hard origin/main
# Bump only the main package version. optionalDependencies stay
# pinned to 0.0.0-bootstrap in committed source — publish-npm.sh
# rewrites them to the real version in-flight before publishing,
# once the matching @kdlbs/runtime-* tarballs exist on npm.
(cd apps/cli && npm version --no-git-tag-version "$NEXT")
pnpm -C apps install --no-frozen-lockfile
git-cliff --tag "$TAG" -o CHANGELOG.md
git add apps/cli/package.json CHANGELOG.md apps/pnpm-lock.yaml
git commit -m "release: $NEXT"
git tag -a "$TAG" -m "release: $NEXT"
git push origin "$TAG"
# Push commit and tag together. If main advanced concurrently this
# fails non-fast-forward; the maintainer just re-runs.
git push origin main "$TAG"
- name: Dry-run summary
if: ${{ inputs.dry_run }}
env:
NEXT: ${{ steps.compute.outputs.version }}
TAG: ${{ steps.compute.outputs.tag }}
run: |
echo "DRY RUN: Would have created PR + tag ${{ steps.compute.outputs.tag }}"
echo "DRY RUN: Would have updated CHANGELOG.md"
echo "DRY RUN: Would have bumped apps/cli/package.json to ${{ steps.compute.outputs.version }}"
# In dry-run we still bump + run git-cliff locally so the maintainer
# can verify the computed version + changelog look right, but nothing
# is committed or pushed.
(cd apps/cli && npm version --no-git-tag-version "$NEXT")
pnpm -C apps install --no-frozen-lockfile
git-cliff --tag "$TAG" -o CHANGELOG.md
echo
echo "DRY RUN: Would have committed + tagged $TAG and pushed to origin/main."
echo "DRY RUN: apps/cli/package.json bumped to $NEXT (in-runner only)."
echo "DRY RUN: CHANGELOG.md regenerated (in-runner only)."
build-web:
name: Build web standalone