Skip to content
Merged
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
92 changes: 92 additions & 0 deletions .github/workflows/auto-merge-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: auto-merge-release

# Releases demand on a cadence rather than on every merge: once a day this looks
# for release-plz's open release PR and merges it, but only if the last release
# is at least seven days old and something user-visible has landed since.
#
# Merging that PR is what triggers release-plz.yml's release job, so this is the
# whole release trigger — nothing else merges it.

on:
schedule:
- cron: "0 10 * * *"
workflow_dispatch:

permissions: {}

concurrency:
group: auto-merge-release
cancel-in-progress: false

jobs:
merge:
if: github.repository == 'jdx/demand'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false

- name: Merge the release PR
env:
GH_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
run: |
set -euo pipefail

if [ "${GITHUB_EVENT_NAME:-}" != "workflow_dispatch" ]; then
latest_tag=$(git describe --tags --match 'v[0-9]*' --abbrev=0 || true)
if [ -n "$latest_tag" ]; then
range="${latest_tag}..HEAD"
tag_time=$(git for-each-ref --format='%(creatordate:unix)' "refs/tags/$latest_tag")
now=$(date +%s)
min_age=$((7 * 24 * 60 * 60))
age=$((now - tag_time))
if [ "$age" -lt "$min_age" ]; then
echo "Latest release ${latest_tag} is less than 7 days old; not auto-merging release PRs"
exit 0
fi
else
# Nothing to measure a cadence against yet — let the first
# release through rather than deadlocking on a missing tag.
echo "No v[0-9]* release tag found; allowing bootstrap release"
range=""
fi
# Docs, CI and chore commits do not move a version anyone installs.
if [ -n "$range" ]; then
commit_subjects=$(git log --format=%s "$range")
if ! grep -Eq '^(fix|feat)(\([^)]+\))?!?: ' <<<"$commit_subjects"; then
echo "No pending fix/feat commits since the latest release; not auto-merging release PRs"
exit 0
fi
fi
else
echo "Manual dispatch; skipping release cadence guards"
fi

# release-plz opens its PR from `release-plz-<base>`. Match on that
# prefix and on the PR coming from this repository, so a fork branch
# of the same name can never be what gets merged. release-plz titles
# the PR `chore(<crate>): release v<version>` when one crate is
# released and `chore: release` when several are, so accept both.
PR_NUMBER=$(gh pr list -R jdx/demand --base main --state open --limit 100 --json number,headRefName,headRepositoryOwner,title,updatedAt \
--jq '[.[] | select(.headRefName | startswith("release-plz-")) | select(.headRepositoryOwner.login == "jdx") | select(.title | test("^chore(\\([^)]+\\))?: release( |$)"))] | sort_by(.updatedAt) | reverse | .[0].number // empty')
if [ -z "$PR_NUMBER" ]; then
echo "No open release-plz release PR to main"
exit 0
fi

# An empty body means release-plz was still writing the PR when this
# ran. The changelog would be wrong; wait for tomorrow instead.
BODY=$(gh pr view "$PR_NUMBER" -R jdx/demand --json body --jq '.body // ""')
TRIMMED_BODY=$(echo "$BODY" | tr -d '[:space:]')
if [ -z "$TRIMMED_BODY" ]; then
echo "Skipping merge: PR #$PR_NUMBER has an empty body; will retry on the next scheduled run"
exit 0
fi

echo "PR #$PR_NUMBER is ready for the daily release window"
gh pr merge "$PR_NUMBER" -R jdx/demand --squash --auto
Loading