-
-
Notifications
You must be signed in to change notification settings - Fork 0
199 lines (186 loc) · 9.48 KB
/
Copy pathgitflow-release.yml
File metadata and controls
199 lines (186 loc) · 9.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
name: Gitflow Release
# Triggered when a PR (typically `release/vX.Y.Z` or `hotfix/vX.Y.Z`) is merged
# to master. Reads the version from CHANGELOG.md, creates and pushes a vX.Y.Z
# tag using REPO_WRITE_PAT, then opens a merge-back PR to develop.
#
# The actual release (build, artifacts, GitHub Release) is handled by
# release.yml, which fires on the tag push. We must use a PAT (not GITHUB_TOKEN)
# to push the tag — tags pushed by GITHUB_TOKEN do NOT trigger downstream
# workflows. See:
# https://docs.github.com/en/actions/security-for-github-actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow
#
# Required repo configuration:
# - Secret REPO_WRITE_PAT: fine-grained PAT scoped to this repo with:
# Contents: read/write (push the tag, push the meta/merge-back-* branch)
# Pull requests: read/write (create the merge-back PR, enable auto-merge)
# Metadata: read (mandatory baseline on fine-grained PATs)
# Both jobs use this PAT — not GITHUB_TOKEN — because PRs and tags created
# by GITHUB_TOKEN do NOT trigger downstream workflows. Without PAT-driven
# events, the merge-back PR's required status checks (Format, Check & Clippy,
# Test, Package smoke test, Validate branch prefix vs base) would never
# fire, and auto-merge would wait forever.
# - Settings → Actions → General → "Allow GitHub Actions to create and approve
# pull requests" — recommended on for general Actions hygiene, but NOT
# required for this workflow specifically. The merge-back step uses the
# PAT (authenticated as a real user), not GITHUB_TOKEN, so the bot-PR
# setting doesn't gate it.
on:
push:
branches:
- master
# Manual recovery path: re-run if a previous attempt failed mid-flight
# (e.g., transient gh API error during merge-back PR creation). Safe to
# invoke repeatedly — the tag and merge-back steps are both idempotent.
workflow_dispatch:
# Serialize release runs; never cancel a tag/PR operation mid-flight.
concurrency:
group: gitflow-release
cancel-in-progress: false
# Workflow-level permissions are the floor. Both jobs authenticate via
# REPO_WRITE_PAT, not GITHUB_TOKEN, so these read-only defaults are intentional —
# GITHUB_TOKEN is unused for any write here.
permissions:
contents: read
jobs:
tag:
name: Tag release
runs-on: ubuntu-latest
# GITHUB_TOKEN here is unused for writes — REPO_WRITE_PAT supplies the
# tag-push credential. Keep the floor at read.
permissions:
contents: read
outputs:
tag: ${{ steps.extract.outputs.tag }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ secrets.REPO_WRITE_PAT }}
- name: Extract version from CHANGELOG.md
id: extract
run: |
set -euo pipefail
if [ ! -f CHANGELOG.md ]; then
echo "::error::CHANGELOG.md not found"
exit 1
fi
# First versioned entry header: ## [X.Y.Z] - YYYY-MM-DD
VERSION=$(awk '/^## \[[0-9]/ { gsub(/[\[\]]/, "", $2); print $2; exit }' CHANGELOG.md)
if [ -z "${VERSION}" ]; then
# No versioned entry → nothing to release. This is the expected
# state on bootstrap (`git push origin develop:master` to create
# the master branch for the first time) and on any push to master
# that doesn't include a CHANGELOG promotion. We exit successfully
# without setting outputs.tag, so the merge-back job's
# `if: needs.tag.outputs.tag != ''` short-circuits it.
#
# Loud signal preservation: this still surfaces in the run summary
# as a notice — it's just not a red ❌. If you intended to cut a
# release, the missing tag (and missing GitHub Release) is the
# signal to look here.
echo "::notice title=No release cut::CHANGELOG.md has no [X.Y.Z] entry — skipping tag and merge-back. To release, promote [Unreleased] to [X.Y.Z] - YYYY-MM-DD on a release/* branch and merge to master."
exit 0
fi
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
echo "tag=v${VERSION}" >> "${GITHUB_OUTPUT}"
echo "Detected version: ${VERSION}"
- name: Create and push tag
id: tag
# Skip if extract found no versioned entry (bootstrap / non-release
# push to master). Without this guard, the step would fall through
# with TAG="" and fail at `git tag -a ""`.
if: steps.extract.outputs.tag != ''
env:
TAG: ${{ steps.extract.outputs.tag }}
run: |
set -euo pipefail
if git rev-parse "${TAG}" >/dev/null 2>&1; then
echo "::warning title=No new release::CHANGELOG.md still references already-released ${TAG}. If you intended to release a new version, promote [Unreleased] to [X.Y.Z] - YYYY-MM-DD on a release/* branch and merge it to master."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "${TAG}" -m "Release ${TAG}"
git push origin "${TAG}"
merge-back:
name: Open merge-back PR (master → develop)
needs: tag
# Always run as long as we know which version we're targeting. The job
# itself is idempotent: it short-circuits if the merge-back PR for this
# tag is already open. This handles recovery cases where the tag was
# pushed by a prior run but the PR step failed.
if: needs.tag.outputs.tag != ''
runs-on: ubuntu-latest
# GITHUB_TOKEN is intentionally read-only here. All writes (branch push,
# PR create, auto-merge enable) go through REPO_WRITE_PAT so the
# resulting events trigger downstream workflows (ci.yml, branch-policy.yml,
# labeler.yml). PRs created by GITHUB_TOKEN do NOT fire pull_request
# events, which would leave the merge-back PR's required checks unreported
# and auto-merge stuck forever.
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# Use PAT so the meta/merge-back-* branch push is by the PAT user.
# The push itself doesn't need to trigger a workflow, but the
# subsequent gh-CLI PR creation must use the same identity for
# consistency (and gh inherits its credential from this checkout
# unless GH_TOKEN is set explicitly).
token: ${{ secrets.REPO_WRITE_PAT }}
- name: Check for existing merge-back PR
id: check
env:
# PAT here so the gh CLI authenticates as the PAT user, matching
# the credential used to create the PR in the next step.
GH_TOKEN: ${{ secrets.REPO_WRITE_PAT }}
TAG: ${{ needs.tag.outputs.tag }}
run: |
set -euo pipefail
branch="meta/merge-back-${TAG}"
# `--head OWNER:branch` is the safe form; without the owner prefix
# gh treats `--head` as a contains-filter rather than an exact match.
existing=$(gh pr list \
--base develop \
--head "${branch}" \
--state open \
--json number,url \
--jq '.[0].url // empty')
if [ -n "${existing}" ]; then
echo "Merge-back PR already open: ${existing}"
echo "exists=true" >> "${GITHUB_OUTPUT}"
else
echo "exists=false" >> "${GITHUB_OUTPUT}"
fi
- name: Open merge-back PR
if: steps.check.outputs.exists == 'false'
env:
# PAT — see job-level permissions block. Using GITHUB_TOKEN here
# would create the PR as github-actions[bot], which suppresses all
# pull_request workflow triggers and strands the PR.
GH_TOKEN: ${{ secrets.REPO_WRITE_PAT }}
TAG: ${{ needs.tag.outputs.tag }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
branch="meta/merge-back-${TAG}"
git fetch origin master develop
git checkout -B "${branch}" "origin/master"
git commit --allow-empty -m "meta: merge-back ${TAG}"
git push --force-with-lease -u origin "${branch}"
pr_url=$(gh pr create \
--base develop \
--head "${branch}" \
--title "meta: merge-back ${TAG} from master to develop" \
--body "Automated merge-back PR after the ${TAG} release. Auto-merge is enabled — if there are conflicts, resolve them and the PR will merge once checks pass.")
# Enable auto-merge so the PR merges itself once required checks pass
# and there are no conflicts. Conflicting back-merges still need
# manual resolution; auto-merge will sit and wait until they're fixed.
# Uses --squash because the repo disables merge commits and rebase merges
# (allow_merge_commit: false, allow_rebase_merge: false). Squashing the
# empty merge-back commit is fine — its only purpose is to trigger the
# PR; the actual code already exists on master via the release PR.
gh pr merge --auto --squash "${pr_url}" || \
echo "::warning::Auto-merge could not be enabled for ${pr_url}. Repo setting 'Allow auto-merge' may be off, or the PR has conflicts. Merge manually."