Skip to content

Commit 8575e80

Browse files
authored
ci: two-stage release with manual approval (2.x) (#815)
Brings the 2.x maintenance branch onto the same release flow as master (#814), so every line releases the same way. **Flow:** run **Prepare Release** (manual, with a version) -> it opens a `release/vX.Y.Z` PR -> merge it -> **Release** checks the version against npm, pushes an approval request to DingTalk, waits on the `release` environment gate, then publishes and creates the GitHub Release. **2.x specifics:** - Publishes under dist-tag `latest-2` (never `latest`). - Publishes `lib/` directly (no build step). - Replaces the previous shared `node-release` reusable workflow. - npm auth via OIDC trusted publishing (`id-token: write`). Requires the repo `release` environment (required reviewers) and npm trusted-publisher config to allow this branch's workflow. DingTalk secrets are already set repo-wide.
1 parent b54eda6 commit 8575e80

3 files changed

Lines changed: 216 additions & 13 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Prepare Release
2+
3+
permissions: {}
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: 'Version to release (without v prefix, e.g. 2.44.1 or 2.45.0-beta.0)'
10+
required: true
11+
type: string
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref_name }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
prepare:
19+
if: github.repository == 'node-modules/urllib'
20+
name: Prepare Release
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: write
24+
pull-requests: write
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
28+
with:
29+
ref: 2.x
30+
fetch-depth: 0
31+
32+
- name: Validate and bump version
33+
env:
34+
VERSION: ${{ inputs.version }}
35+
run: |
36+
set -euo pipefail
37+
# Require semver without a leading "v", e.g. 2.44.1 or 2.45.0-beta.0
38+
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
39+
echo "::error::Invalid version '$VERSION'. Expected semver without 'v' prefix, e.g. 2.44.1 or 2.45.0-beta.0"
40+
exit 1
41+
fi
42+
sed -i -E "s/^([[:space:]]*\"version\":[[:space:]]*)\"[^\"]+\"/\1\"$VERSION\"/" package.json
43+
grep -qF "\"version\": \"$VERSION\"" package.json || { echo "::error::Failed to update package.json"; exit 1; }
44+
echo "Updated package.json to $VERSION"
45+
46+
- name: Create pull request
47+
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
48+
with:
49+
commit-message: 'release: v${{ inputs.version }}'
50+
title: 'release: v${{ inputs.version }}'
51+
branch: release/v${{ inputs.version }}
52+
base: 2.x
53+
body: |
54+
Release urllib v${{ inputs.version }}.
55+
56+
Merging this PR updates the version on `2.x` and triggers the release
57+
workflow, which publishes to npm (dist-tag `latest-2`) and creates the
58+
GitHub Release after manual approval.
59+
assignees: fengmk2

.github/workflows/release-2.x.yml

Lines changed: 0 additions & 13 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [2.x]
6+
paths:
7+
- 'package.json'
8+
9+
permissions: {}
10+
11+
# Serialize releases per branch and cancel an older run still pending approval
12+
# when a newer version lands, so an approved-late stale run cannot publish
13+
# backwards. Scoped by ref so other release lines (master/3.x/4.x) are independent.
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref_name }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
check:
20+
if: github.repository == 'node-modules/urllib'
21+
name: Check version
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
outputs:
26+
version_changed: ${{ steps.version.outputs.changed }}
27+
version: ${{ steps.version.outputs.version }}
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
31+
32+
- name: Check whether version is already published
33+
id: version
34+
run: |
35+
set -euo pipefail
36+
# Compare the exact version against the registry so a prerelease
37+
# published under its own dist-tag is not treated as newer and re-released.
38+
VERSION=$(node -p "require('./package.json').version")
39+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
40+
STATUS=0
41+
OUTPUT=$(npm view "urllib@$VERSION" version 2>&1) || STATUS=$?
42+
if [ "$STATUS" -eq 0 ] && [ -n "$OUTPUT" ]; then
43+
echo "urllib@$VERSION is already published; nothing to release."
44+
echo "changed=false" >> "$GITHUB_OUTPUT"
45+
elif [ "$STATUS" -ne 0 ] && ! grep -q 'E404' <<<"$OUTPUT"; then
46+
# Not a "version missing" 404 -> auth/network/registry error; fail loudly.
47+
echo "::error::npm view failed for urllib@$VERSION (not a 404):"
48+
printf '%s\n' "$OUTPUT"
49+
exit 1
50+
else
51+
echo "urllib@$VERSION is not published yet; proceeding."
52+
echo "changed=true" >> "$GITHUB_OUTPUT"
53+
fi
54+
55+
request-approval:
56+
name: Request approval
57+
runs-on: ubuntu-latest
58+
needs: check
59+
if: needs.check.outputs.version_changed == 'true'
60+
permissions: {}
61+
env:
62+
DINGTALK_WEBHOOK_URL: ${{ secrets.DINGTALK_RELEASE_WEBHOOK_URL }}
63+
DINGTALK_WEBHOOK_SECRET: ${{ secrets.DINGTALK_RELEASE_WEBHOOK_SECRET }}
64+
VERSION: ${{ needs.check.outputs.version }}
65+
BRANCH: ${{ github.ref_name }}
66+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
67+
steps:
68+
- name: Notify DingTalk
69+
# Best-effort: a webhook failure must not block the manual approval gate.
70+
continue-on-error: true
71+
run: |
72+
set -euo pipefail
73+
# DingTalk signed webhook (加签): sign = urlencode(base64(HMAC-SHA256(secret, "timestamp\nsecret")))
74+
TIMESTAMP=$(date +%s%3N)
75+
SIGN=$(printf '%s\n%s' "$TIMESTAMP" "$DINGTALK_WEBHOOK_SECRET" \
76+
| openssl dgst -sha256 -hmac "$DINGTALK_WEBHOOK_SECRET" -binary \
77+
| base64 | tr -d '\n')
78+
SIGN_ENC=$(jq -rn --arg s "$SIGN" '$s | @uri')
79+
URL="${DINGTALK_WEBHOOK_URL}&timestamp=${TIMESTAMP}&sign=${SIGN_ENC}"
80+
TEXT=$(printf '### urllib release v%s (%s)\n\nAwaiting manual approval before publishing to npm.\n\n[Review and approve](%s)' "$VERSION" "$BRANCH" "$RUN_URL")
81+
PAYLOAD=$(jq -n --arg text "$TEXT" \
82+
'{msgtype: "markdown", markdown: {title: "urllib release approval", text: $text}}')
83+
curl -fsS --connect-timeout 10 --max-time 30 \
84+
--retry 3 --retry-delay 2 --retry-all-errors \
85+
-X POST "$URL" \
86+
-H 'Content-Type: application/json' \
87+
-d "$PAYLOAD"
88+
89+
release:
90+
name: Publish to npm
91+
runs-on: ubuntu-latest
92+
# Manual approval gate: configure an Environment named "release" with
93+
# required reviewers in repo settings. The job pauses here until approved.
94+
environment: release
95+
needs: [check, request-approval]
96+
if: needs.check.outputs.version_changed == 'true'
97+
permissions:
98+
contents: write
99+
id-token: write # OIDC trusted publishing to npm
100+
env:
101+
VERSION: ${{ needs.check.outputs.version }}
102+
steps:
103+
- name: Checkout repository
104+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
105+
106+
- name: Setup Node.js
107+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
108+
with:
109+
node-version: '22'
110+
111+
- name: Update npm (OIDC trusted publishing needs npm >= 11.5.1)
112+
run: npm install -g npm@latest
113+
114+
- name: Determine npm dist-tag
115+
id: dist-tag
116+
run: |
117+
set -euo pipefail
118+
# 2.x is a maintenance line: stable releases publish under `latest-2`,
119+
# never `latest`. Pre-releases use their first identifier (e.g. beta).
120+
CORE="${VERSION%%+*}"
121+
case "$CORE" in
122+
*-*)
123+
PRE="${CORE#*-}"
124+
echo "tag=${PRE%%.*}" >> "$GITHUB_OUTPUT"
125+
;;
126+
*)
127+
echo "tag=latest-2" >> "$GITHUB_OUTPUT"
128+
;;
129+
esac
130+
131+
- name: Re-check version before publish
132+
run: |
133+
set -euo pipefail
134+
# Guard against a stale run approved after a newer version was published.
135+
STATUS=0
136+
OUTPUT=$(npm view "urllib@$VERSION" version 2>&1) || STATUS=$?
137+
if [ "$STATUS" -eq 0 ] && [ -n "$OUTPUT" ]; then
138+
echo "::error::urllib@$VERSION is already published; aborting to avoid republishing a stale version."
139+
exit 1
140+
elif [ "$STATUS" -ne 0 ] && ! grep -q 'E404' <<<"$OUTPUT"; then
141+
echo "::error::npm view failed for urllib@$VERSION (not a 404); aborting:"
142+
printf '%s\n' "$OUTPUT"
143+
exit 1
144+
fi
145+
echo "urllib@$VERSION is not yet published; proceeding to publish."
146+
147+
- name: Publish to npm
148+
run: npm publish --access public --tag ${{ steps.dist-tag.outputs.tag }}
149+
150+
- name: Create GitHub Release
151+
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
152+
with:
153+
generate_release_notes: true
154+
name: v${{ env.VERSION }}
155+
tag_name: v${{ env.VERSION }}
156+
target_commitish: ${{ github.sha }}
157+
prerelease: ${{ steps.dist-tag.outputs.tag != 'latest-2' }}

0 commit comments

Comments
 (0)