-
Notifications
You must be signed in to change notification settings - Fork 0
321 lines (288 loc) · 14.5 KB
/
Copy pathrelease.yml
File metadata and controls
321 lines (288 loc) · 14.5 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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
name: Release
# Step 2 of the single-branch release: take the stabilized release/v* or
# hotfix/v* branch, run tests, pin `authplane-sdk==X.Y.Z` in the two
# adapter pyprojects, commit, tag vX.Y.Z, build sdist+wheel for all three
# packages (hatch-vcs resolves the tag), validate with twine check,
# atomic-push branch + tag, create the GitHub Release, and delete the
# source branch on success.
#
# Branch conventions:
# - release/v<X.Y.Z>: current-line release off the default branch.
# - hotfix/v<X.Y.Z>: patch for an older minor line. The workflow refuses
# dispatch unless (branch.major, branch.minor) is strictly older than
# the default branch's latest v<X.Y.Z> tag — use release/v* for
# current-line patches.
#
# PyPI publication is handled separately by `publish-pypi.yml`, triggered
# by the tag push. This keeps the OIDC identity tied to the tag itself.
#
# Versions come from the git tag via hatch-vcs; no pyproject version edits.
# The only release-time file edit is the adapter->core pin in the adapters'
# dependencies lists.
#
# Trigger: maintainer dispatches from the Actions UI with a release/v* or
# hotfix/v* branch selected. Branch name is the source of truth for the
# version that will become the tag.
on:
workflow_dispatch:
inputs:
dryRun:
description: 'Dry run: pin adapter deps, test, build, twine check locally; skip atomic push, PyPI uploads, GitHub Release, and branch deletion.'
required: false
type: boolean
default: false
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
# Least-privilege default; the job re-grants the write scope it needs.
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Refuse if not dispatched from a release/v* or hotfix/v* branch
run: |
ref="${{ github.ref_name }}"
if [[ ! "$ref" =~ ^(release|hotfix)/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::This workflow must be dispatched from a release/v<X.Y.Z> or hotfix/v<X.Y.Z> branch (got: $ref)"
exit 1
fi
echo "Dispatched from $ref"
# Fail fast if the AuthPlane Release Bot App secrets aren't available
# (e.g. a fork or unconfigured clone). The token-mint step would
# otherwise fail with a generic actions/create-github-app-token error.
- name: Refuse if Release Bot secrets are missing
env:
APP_ID: ${{ secrets.RELEASE_BOT_APP_ID }}
PRIVATE_KEY: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
run: |
if [[ -z "$APP_ID" || -z "$PRIVATE_KEY" ]]; then
echo "::error::Release Bot App secrets (RELEASE_BOT_APP_ID / RELEASE_BOT_PRIVATE_KEY) are unavailable in this run. They live as AuthPlane org secrets scoped to the OSS release repo; forks and unconfigured clones cannot run release.yml end-to-end and must follow the manual tagging fallback."
exit 1
fi
# Tag/branch rulesets reject GHA bot pushes for v* tags. Mint a
# short-lived installation token for the AuthPlane Release Bot App
# (bypass actor on the ruleset) and pass it to checkout so the
# atomic push and source-branch delete use the App's identity.
- name: Mint release-bot token
id: app_token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- name: Check out repo with full history and all tags
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ github.ref }}
token: ${{ steps.app_token.outputs.token }}
# AuthPlane/conformance is the public sibling repo carrying the shared
# oauth-sdk-conformance-catalog.yaml. Cloned to $RUNNER_TEMP — outside
# $GITHUB_WORKSPACE — so the catalog checkout never pollutes the working
# tree of the commit we tag and publish. Plain git clone is enough:
# actions/checkout disallows paths outside the workspace, and we don't
# need its auth/persist-credentials features for a public read-only repo.
- name: Clone shared conformance catalog (out of tree)
run: |
git clone --depth 1 https://github.com/AuthPlane/conformance.git "$RUNNER_TEMP/conformance"
- name: Set up Python 3.11
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
- name: Install hatch-vcs, twine, build
run: |
python -m pip install --upgrade pip
pip install hatchling hatch-vcs twine build
- name: Derive version from branch name
id: version
run: |
ref="${{ github.ref_name }}"
version="${ref##*/v}"
{
echo "version=$version"
echo "tag=v$version"
} >> "$GITHUB_OUTPUT"
- name: Refuse if tag already exists
run: |
if git ls-remote --exit-code --tags origin "${{ steps.version.outputs.tag }}" >/dev/null; then
echo "::error::Tag ${{ steps.version.outputs.tag }} already exists. Aborting."
exit 1
fi
# hotfix/v* is reserved for patches to an older minor line. Refuse
# dispatch if the branch's (major, minor) is not strictly less than
# the default branch's latest released (major, minor). Current-line
# patches should use release/v*, not hotfix/v*.
- name: Refuse if hotfix branch is not strictly an older-line patch
if: startsWith(github.ref_name, 'hotfix/')
run: |
default="${{ github.event.repository.default_branch }}"
git fetch origin "$default" --tags --no-recurse-submodules
if ! main_tag=$(git describe --tags --abbrev=0 "origin/$default" 2>/dev/null); then
echo "::error::Cannot determine $default's latest tag — has a release ever shipped? If not, use release/v* for the first release."
exit 1
fi
echo "$default's latest tag: $main_tag"
if ! [[ "$main_tag" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::$default's latest tag '$main_tag' does not match vX.Y.Z."
exit 1
fi
main_major="${BASH_REMATCH[1]}"
main_minor="${BASH_REMATCH[2]}"
v="${{ steps.version.outputs.version }}"
[[ "$v" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]
br_major="${BASH_REMATCH[1]}"
br_minor="${BASH_REMATCH[2]}"
if (( br_major > main_major )) || { (( br_major == main_major )) && (( br_minor >= main_minor )); }; then
echo "::error::hotfix/v${v} is on line v${br_major}.${br_minor}, which is not strictly older than $default's line v${main_major}.${main_minor}."
echo "::error::Current-line patches must use release/v*, not hotfix/v*."
exit 1
fi
echo "Confirmed older-line patch: v${br_major}.${br_minor} < v${main_major}.${main_minor}"
- name: Verify CHANGELOG entry exists for this version
run: |
if ! grep -q "^## \[${{ steps.version.outputs.version }}\]" CHANGELOG.md 2>/dev/null; then
echo "::error::CHANGELOG.md has no entry for ## [${{ steps.version.outputs.version }}]"
exit 1
fi
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Tests run against the bare "authplane-sdk" adapter dependency with
# the core installed as a hatch-vcs dev version. The exact-version pin
# would be unsatisfiable until the release tag exists — it is applied
# below, as part of the single release commit.
- name: Install packages and run tests
env:
AUTHPLANE_CONFORMANCE_CATALOG: ${{ runner.temp }}/conformance/oauth-sdk-conformance-catalog.yaml
run: |
pip install -e ".[dev]"
pip install -e "./authplane-mcp[dev]"
pip install -e "./authplane-fastmcp[dev]"
pytest tests/ conformance-tests/ authplane-mcp/tests/ authplane-fastmcp/tests/
# Pin authplane-mcp and authplane-fastmcp to the exact version being
# released. Happens directly on the release branch; no merge to the
# default branch. Ordered after tests so the pin ships in the release
# commit but does not interfere with local install resolution.
- name: Pin adapter->core dependency to ==X.Y.Z
run: |
v="${{ steps.version.outputs.version }}"
for pkg in authplane-mcp authplane-fastmcp; do
f="$pkg/pyproject.toml"
sed -i -E "s/\"authplane-sdk([>=<~!][^\"]*)?\"/\"authplane-sdk==$v\"/" "$f"
if ! grep -q "\"authplane-sdk==$v\"" "$f"; then
echo "::error::Failed to pin authplane-sdk==$v in $f — no 'authplane-sdk' dependency line was found."
exit 1
fi
done
# If a maintainer pre-pinned the adapters on the release branch, sed
# produces an identical file and there is nothing to commit. Handle
# both cases: --allow-empty keeps the shape (always one release commit
# per version) and ensures the tag points at a distinct SHA.
- name: Commit adapter pins on the release branch
run: |
git add -A
git commit --allow-empty -m "release: v${{ steps.version.outputs.version }}"
- name: Capture release commit SHA
id: sha
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Create annotated tag locally
run: |
git tag -a "${{ steps.version.outputs.tag }}" \
-m "Release ${{ steps.version.outputs.tag }}"
- name: Build sdist and wheel for all three packages
run: |
(cd . && python -m build --sdist --wheel --outdir dist/)
(cd authplane-mcp && python -m build --sdist --wheel --outdir dist/)
(cd authplane-fastmcp && python -m build --sdist --wheel --outdir dist/)
echo "=== Root dist/ ==="
ls dist/
echo "=== authplane-mcp/dist/ ==="
ls authplane-mcp/dist/
echo "=== authplane-fastmcp/dist/ ==="
ls authplane-fastmcp/dist/
- name: Validate artifacts with twine check
run: |
python -m twine check dist/* authplane-mcp/dist/* authplane-fastmcp/dist/*
- name: Dry run short-circuit notice
if: ${{ inputs.dryRun }}
run: |
echo "::notice::Dry run — skipping atomic push, PyPI publish, GitHub Release, and branch deletion. Release commit SHA (would-be): ${{ steps.sha.outputs.sha }}"
- name: Atomic push of release branch and tag
if: ${{ !inputs.dryRun }}
run: |
git push --atomic origin "$GITHUB_REF_NAME" "${{ steps.version.outputs.tag }}"
echo "::notice::Released commit: ${{ steps.sha.outputs.sha }}"
- name: Extract CHANGELOG entry for release notes
if: ${{ !inputs.dryRun }}
run: |
version="${{ steps.version.outputs.version }}"
sha="${{ steps.sha.outputs.sha }}"
{
echo "_Released commit: \`$sha\`_"
echo ""
awk "/^## \[$version\]/{flag=1;next} /^## \[/{flag=0} flag" CHANGELOG.md
} > /tmp/release-notes.md
# If CHANGELOG extraction returned nothing beyond the SHA line, add a fallback.
if [[ $(wc -l < /tmp/release-notes.md) -le 2 ]]; then
echo "See CHANGELOG.md for details." >> /tmp/release-notes.md
fi
# GitHub Release creation goes through the API, not git push, so the
# default GITHUB_TOKEN is sufficient — the Release Bot App token is
# only needed for the tag/branch push and source-branch delete above.
- name: Create GitHub Release
if: ${{ !inputs.dryRun }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.version.outputs.tag }}" \
--title "${{ steps.version.outputs.tag }}" \
--notes-file /tmp/release-notes.md
# Delete the source branch (release/v* or hotfix/v*) on success. The
# branch ruleset must allow the bot to delete. If this fails, warn
# loudly but do not fail the workflow — the release is already live.
- name: Delete source branch on remote
if: ${{ !inputs.dryRun }}
continue-on-error: true
id: delete
run: |
git push origin --delete "$GITHUB_REF_NAME"
- name: Warn if delete failed
if: ${{ !inputs.dryRun && steps.delete.outcome == 'failure' }}
run: |
echo "::warning::Source branch delete failed. Run manually: git push origin --delete $GITHUB_REF_NAME"
- name: Summary
if: always()
run: |
v="${{ steps.version.outputs.version }}"
sha="${{ steps.sha.outputs.sha }}"
if [[ "${{ inputs.dryRun }}" == "true" ]]; then
{
echo "### Dry run complete"
echo ""
echo "- **Version**: \`$v\` (not published)"
echo "- **Would-be released commit**: \`$sha\`"
echo "- Pin, tests, build, twine check — all succeeded."
echo "- Atomic push, GitHub Release, PyPI publish, and branch delete were skipped."
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "### Release tag pushed"
echo ""
echo "- **Version**: \`$v\`"
echo "- **Released commit**: \`$sha\`"
echo "- **Tag**: \`v$v\` (pushed — triggers \`publish-pypi.yml\`)"
echo "- **GitHub Release**: ${{ github.server_url }}/${{ github.repository }}/releases/tag/v$v"
echo "- **PyPI (pending)**: [authplane-sdk](https://pypi.org/project/authplane-sdk/$v) · [authplane-mcp](https://pypi.org/project/authplane-mcp/$v) · [authplane-fastmcp](https://pypi.org/project/authplane-fastmcp/$v)"
if [[ "${{ steps.delete.outcome }}" != "success" ]]; then
echo ""
echo "⚠️ **Branch delete failed — run manually:** \`git push origin --delete $GITHUB_REF_NAME\`"
fi
echo ""
echo "**Follow-up:**"
echo "- If any commits on this branch should reach the default branch, dispatch **Backport fixes** with \`fromBranch=${{ steps.version.outputs.tag }}\` (the tag, not the branch — the branch is deleted)."
} >> "$GITHUB_STEP_SUMMARY"
fi