Skip to content

ci + release: gate main on all five suites; one version, tag and draf… #1

ci + release: gate main on all five suites; one version, tag and draf…

ci + release: gate main on all five suites; one version, tag and draf… #1

Workflow file for this run

# ─────────────────────────────────────────────────────────────────────────────
# Every version that lands on main gets a tag and a draft release, and dev is
# brought up to date with main (the sync-dev job at the end).
#
# The version gate (ci.yml) makes a pull request that reaches users bump the
# version. When that bump merges, this workflow tags the merge commit v<version>
# and opens a DRAFT GitHub release with notes generated from the pull requests
# since the previous tag. Nothing is published here.
#
# Publishing is a person's decision: review the draft, then press Publish. That
# `release: published` event is what runs publish-npm.yml, which builds the
# engines, checks that the tag and every manifest agree, publishes to npm and
# attaches the tarballs to the release.
#
# A push that does not move the version finds its tag already present and does
# nothing, so this runs on every push to main without a path filter.
# ─────────────────────────────────────────────────────────────────────────────
name: release
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: release-main
cancel-in-progress: false
permissions:
contents: write
jobs:
tag:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: every manifest carries the same version
run: node .github/scripts/version.mjs check
- name: tag and draft the release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
version="$(node .github/scripts/version.mjs get)"
tag="v$version"
if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null; then
echo "$tag already exists — this push did not change the version"
exit 0
fi
prerelease=""
case "$version" in *-*) prerelease="--prerelease";; esac
# The previous tag bounds the generated notes; the first release has none.
prev="$(git tag --list 'v*' --sort=-v:refname | head -1)"
start=""; [ -n "$prev" ] && start="--notes-start-tag $prev"
# The tag is pushed here rather than left to the release: a DRAFT release
# does not create its tag until it is published, so the check above would
# never see it and every later push would draft again. A tag pushed with
# GITHUB_TOKEN starts no workflow, which is intended: publishing waits for
# a person to publish the draft.
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$tag" -m "$tag" "$GITHUB_SHA"
git push origin "refs/tags/$tag"
gh release create "$tag" --draft --verify-tag --title "$tag" \
--generate-notes $start $prerelease
echo "::notice::drafted $tag — review it under Releases and publish it to ship to npm"
# ── keep dev on top of main ─────────────────────────────────────────────────
# main only takes squash merges, so a promotion from dev lands on main as a NEW
# commit that dev does not have; without this, the next dev→main pull request
# shows the old work again. Merging main back into dev records that it is
# already there. A promotion merges cleanly (same content both sides); a hotfix
# that went straight to main and touches lines dev has since changed does not,
# and then nothing is pushed: an issue says how to resolve it by hand.
sync-dev:
runs-on: ubuntu-24.04
permissions:
contents: write
issues: write
steps:
- uses: actions/checkout@v4
with:
ref: dev
fetch-depth: 0
- name: merge main into dev
id: merge
run: |
set -uo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin main
if git merge-base --is-ancestor origin/main HEAD; then
echo "dev already contains main"; exit 0
fi
if git merge --no-edit -m "Merge main into dev (${GITHUB_SHA::8})" origin/main; then
git push origin HEAD:dev
echo "dev now contains main at ${GITHUB_SHA::8}"
else
git diff --name-only --diff-filter=U > /tmp/conflicts.txt
git merge --abort
echo "conflict=1" >> "$GITHUB_OUTPUT"
echo "::error::main does not merge cleanly into dev"
exit 1
fi
# One open issue at most: a later conflicting push comments on it.
- name: say how to resolve it
if: failure() && steps.merge.outputs.conflict == '1'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -uo pipefail
title="main does not merge cleanly into dev"
{
echo "Merging main (${GITHUB_SHA::8}) into dev conflicts in:"
echo; echo '```'; cat /tmp/conflicts.txt; echo '```'; echo
echo "Resolve it locally (dev takes direct pushes):"
echo; echo '```'
echo "git fetch origin && git checkout dev && git pull"
echo "git merge origin/main"
echo "# fix the files above, then"
echo "git add -A && git commit && git push origin dev"
echo '```'; echo
echo "Close this once dev is pushed; the next push to main retries on its own."
echo; echo "Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
} > /tmp/body.md
existing="$(gh issue list --state open --search "\"$title\" in:title" --json number --jq '.[0].number' || true)"
if [ -n "$existing" ]; then
gh issue comment "$existing" --body-file /tmp/body.md
else
gh issue create --title "$title" --body-file /tmp/body.md
fi