-
Notifications
You must be signed in to change notification settings - Fork 1
226 lines (197 loc) · 9.61 KB
/
release.yml
File metadata and controls
226 lines (197 loc) · 9.61 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
name: Release
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run only (no git push, no tags, no npm publish)'
required: false
default: true
type: boolean
permissions:
contents: write
id-token: write
issues: write
pull-requests: write
# Allow GitHub Actions to bypass branch protection
# This is required for semantic-release to push version updates
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Verify admin permissions
run: |
# Use the repository's permission endpoint which works for both personal and org repos
RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission")
# Extract permission using jq if available, otherwise use grep
if command -v jq &> /dev/null; then
PERMISSION=$(echo "$RESPONSE" | jq -r '.permission // empty')
else
PERMISSION=$(echo "$RESPONSE" | grep -o '"permission":"[^"]*"' | head -1 | cut -d'"' -f4)
fi
if [ -z "$PERMISSION" ]; then
echo "Warning: Could not determine permission level. Response: $RESPONSE"
echo "Note: workflow_dispatch requires write access, proceeding..."
exit 0
fi
if [ "$PERMISSION" != "admin" ]; then
echo "Error: Only repository admins can trigger releases. Current permission: $PERMISSION"
exit 1
fi
echo "✓ Verified admin permission for ${{ github.actor }}"
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Setup git branch
run: |
git fetch --all --tags --force
git fetch origin '+refs/notes/*:refs/notes/*' || true
git checkout -B main
git branch --set-upstream-to=origin/main main
- name: Ensure master branch exists (for semantic-release validation)
run: |
# semantic-release requires at least one release branch that exists; repo uses main, we declare "master"
if ! git ls-remote --heads origin master 2>/dev/null | grep -q .; then
git checkout -b master
git push origin master
git checkout main
fi
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
always-auth: true
- run: npm ci
- run: npm test --if-present
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Ensure v1.0.0-beta.1 is in main history
run: |
set -e
# semantic-release uses "git tag --merged main"; if the tag points to a commit not in main, it is ignored.
if ! git rev-parse --verify v1.0.0-beta.1 >/dev/null 2>&1; then exit 0; fi
if git tag --merged main | grep -qx v1.0.0-beta.1; then
echo "v1.0.0-beta.1 is already in main history."
exit 0
fi
# Tag exists but is not merged into main; move it to the release commit so semantic-release sees it.
RELEASE_COMMIT=$(git log main -1 --format=%H --grep="chore(release): 1.0.0-beta.1" 2>/dev/null || true)
if [ -z "$RELEASE_COMMIT" ]; then
RELEASE_COMMIT=$(git merge-base main origin/master 2>/dev/null || git rev-parse HEAD~1)
fi
git tag -d v1.0.0-beta.1 2>/dev/null || true
git tag -a v1.0.0-beta.1 "$RELEASE_COMMIT" -m "chore: 1.0.0-beta.1"
if [ "${{ github.event.inputs.dry_run }}" != "true" ]; then
git push origin v1.0.0-beta.1 --force
fi
echo "Moved v1.0.0-beta.1 to $RELEASE_COMMIT so it is in main history."
- name: Add semantic-release note to v1.0.0-beta.1 if missing
run: |
set -e
# Existing v1.0.0-beta.1 tag has no git note, so semantic-release ignores it and tries to re-release it.
# Add the note so it is treated as last release and the next version is 1.0.0-beta.2.
if ! git rev-parse --verify v1.0.0-beta.1 >/dev/null 2>&1; then
echo "Tag v1.0.0-beta.1 not found, skipping note step."
exit 0
fi
# Always ensure the note exists (-f overwrites); required for get-last-release to see this tag on main.
git notes --ref semantic-release-v1.0.0-beta.1 add -f -m '{"channels":["beta"]}' v1.0.0-beta.1
if [ "${{ github.event.inputs.dry_run }}" != "true" ]; then
git push origin refs/notes/semantic-release-v1.0.0-beta.1
fi
echo "Added semantic-release note to v1.0.0-beta.1"
- name: Create initial tag if needed
if: github.event.inputs.dry_run != 'true'
run: |
if ! git rev-parse --verify "v1.0.0-beta.1" >/dev/null 2>&1; then
echo "Creating initial tag v1.0.0-beta.1"
git tag -a "v1.0.0-beta.1" -m "chore: initial beta release"
git push origin "v1.0.0-beta.1" || echo "Tag push failed (may not have permission or tag exists)"
else
echo "Tag v1.0.0-beta.1 already exists"
fi
- name: Dry run mode notice
if: github.event.inputs.dry_run == 'true'
run: |
echo "=============================================="
echo " DRY RUN MODE - No commits, tags, or publish"
echo "=============================================="
echo "Semantic-release will show what WOULD happen."
echo "To perform a real release, run again and uncheck 'Dry run only'."
echo ""
- name: Ensure v1.0.0-beta.1 exists locally (dry run only)
if: github.event.inputs.dry_run == 'true'
run: |
if ! git rev-parse --verify "v1.0.0-beta.1" >/dev/null 2>&1; then
# So semantic-release sees a "previous release" and suggests 1.0.0-beta.2 for new commits
PARENT=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
git tag -a "v1.0.0-beta.1" "$PARENT" -m "chore: initial beta release (dry-run placeholder)"
echo "Created local tag v1.0.0-beta.1 at $PARENT so semantic-release can compute next version (1.0.0-beta.2)."
else
echo "Tag v1.0.0-beta.1 already exists."
fi
- name: Get version before semantic-release
id: version-before
run: |
VERSION_BEFORE=$(node -p "require('./package.json').version")
echo "version=$VERSION_BEFORE" >> $GITHUB_OUTPUT
echo "Current version: $VERSION_BEFORE"
- name: Release with semantic-release
id: release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "Running semantic-release in DRY RUN mode..."
npx semantic-release --dry-run 2>&1 | tee semantic-release.log || true
# Extract "The next release version is X" for use in summary step
grep -oE "The next release version is [^[:space:]]+" semantic-release.log 2>/dev/null | sed 's/The next release version is //' > next-version.txt || echo "" > next-version.txt
else
echo "Running semantic-release (REAL release)..."
npx semantic-release
fi
- name: Publish to npm using trusted publishing
if: github.event.inputs.dry_run != 'true'
run: |
echo "=== Publishing to npm with trusted publishing (OIDC) ==="
# Ensure .npmrc is available (setup-node should have created it)
if [ -f "$NPM_CONFIG_USERCONFIG" ]; then
cp "$NPM_CONFIG_USERCONFIG" ~/.npmrc
echo "✓ Using .npmrc for authentication"
fi
# Get versions
VERSION_BEFORE="${{ steps.version-before.outputs.version }}"
VERSION_AFTER=$(node -p "require('./package.json').version")
echo "Version before: $VERSION_BEFORE"
echo "Version after: $VERSION_AFTER"
# Only publish if semantic-release created a new version
if [ "$VERSION_BEFORE" != "$VERSION_AFTER" ]; then
echo "✓ New version detected: $VERSION_AFTER"
echo "Publishing to npm..."
# Publish using npm publish which supports OIDC/trusted publishing
npm publish --provenance --access public
echo "✓ Published $VERSION_AFTER to npm"
else
echo "No version change detected (version: $VERSION_AFTER)"
echo "Skipping npm publish - no new release was created"
fi
- name: Dry run - skip npm publish
if: github.event.inputs.dry_run == 'true'
run: |
echo "=============================================="
echo " DRY RUN - Skipping npm publish"
echo "=============================================="
NEXT_VERSION=$(cat next-version.txt 2>/dev/null || echo "")
if [ -n "$NEXT_VERSION" ]; then
echo "Version that WOULD have been published: $NEXT_VERSION"
else
echo "Version that WOULD have been published: (check semantic-release output above; might be no new release)"
echo "Current package.json: $(node -p "require('./package.json').version")"
fi
echo ""
echo "To publish for real, run the workflow again with 'Dry run only' unchecked."