-
Notifications
You must be signed in to change notification settings - Fork 23
241 lines (211 loc) · 9.96 KB
/
Copy pathrelease.yml
File metadata and controls
241 lines (211 loc) · 9.96 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
name: Release
on:
push:
branches: [release]
permissions:
contents: write
jobs:
# The main -> release pull request already ran the complete suite on the very
# tree this push carries. Look for that green gate instead of paying for it
# twice; anything unverified (a direct push to release, a merge that changed
# the tree, an API failure) still falls through to the full run below.
verify-ci:
name: Check for an existing full CI run
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
permissions:
contents: read
checks: read
outputs:
verified: ${{ steps.check.outputs.verified }}
reason: ${{ steps.check.outputs.reason }}
steps:
# Depth 2 so both parents of the release merge commit — and their trees —
# are available to compare against the tree being released.
- uses: actions/checkout@v7
with:
fetch-depth: 2
- name: Look for a passing Full CI Gate on this tree
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node scripts/verify-ci-status.js
- name: Publish verification summary
env:
VERIFIED: ${{ steps.check.outputs.verified }}
REASON: ${{ steps.check.outputs.reason }}
run: |
{
echo "### Release CI verification"
echo
echo "- Already verified: \`${VERIFIED}\`"
echo "- Reason: \`${REASON}\`"
} >> "$GITHUB_STEP_SUMMARY"
full-ci:
needs: verify-ci
if: needs.verify-ci.outputs.verified != 'true'
# `actions: write` looks unused from here — the fail-fast cancellation step
# in ci.yml's leaf jobs is gated on `github.event_name == 'pull_request'`,
# and a called workflow inherits the caller's event, which is this push.
# It still has to be granted: a called workflow's jobs cannot hold a
# permission the calling job does not, so narrowing this to `contents: read`
# strips `actions: write` from leaf jobs that declare it. This block also
# scopes full-ci DOWN from the workflow-level `contents: write` the release
# job needs; ci.yml only ever checks out. ci-fail-fast.test.js pins it.
permissions:
contents: read
actions: write
uses: ./.github/workflows/ci.yml
with:
full: true
release:
needs: [verify-ci, full-ci]
runs-on: ubuntu-latest
# Skip version bump commits. Every other reason a release must not ship is
# reported loudly by the step below rather than silently skipping the job —
# a silent skip is what leaves a tag with no GitHub Release behind it.
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Use Node.js 24.x
uses: actions/setup-node@v7
with:
node-version: 24.x
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Get version from package.json
id: package-version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
# full-ci is skipped exactly when verify-ci found a green gate for this
# tree, so 'skipped' is a pass here and only a run that actually executed
# and failed blocks the release.
- name: Report release failure on CI failure
if: needs.verify-ci.result != 'success' || needs.full-ci.result == 'failure' || needs.full-ci.result == 'cancelled'
env:
VERIFY_RESULT: ${{ needs.verify-ci.result }}
FULL_CI_RESULT: ${{ needs.full-ci.result }}
VERSION: ${{ steps.package-version.outputs.version }}
run: |
if [ "$VERIFY_RESULT" != "success" ]; then
BECAUSE="the CI verification job status was '$VERIFY_RESULT'"
else
BECAUSE="full-ci status was '$FULL_CI_RESULT'"
fi
echo "::error::release v${VERSION} did NOT publish because ${BECAUSE}"
echo "release v${VERSION} did NOT publish"
exit 1
- name: Check if release exists
id: release-check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh release view "v${{ steps.package-version.outputs.version }}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Generate changelog
id: changelog
if: steps.release-check.outputs.exists == 'false'
env:
REPO: ${{ github.repository }}
run: |
VERSION="${{ steps.package-version.outputs.version }}"
MAJOR_MINOR=$(echo $VERSION | cut -d. -f1-2)
# Try exact version first (e.g., v0.10.5.md), then minor.x pattern (e.g., v0.10.x.md)
CHANGELOG_FILE_EXACT=".changelog/v${VERSION}.md"
CHANGELOG_FILE_PATTERN=".changelog/v${MAJOR_MINOR}.x.md"
SRC_FILE="" # repo-relative path of the changelog file at this tag (for the "full changelog" link)
if [ -f "$CHANGELOG_FILE_EXACT" ]; then
# Use exact version changelog
CHANGELOG=$(cat "$CHANGELOG_FILE_EXACT")
SRC_FILE="$CHANGELOG_FILE_EXACT"
elif [ -f "$CHANGELOG_FILE_PATTERN" ]; then
# Use minor version pattern changelog (e.g., v0.10.x.md)
CHANGELOG=$(cat "$CHANGELOG_FILE_PATTERN")
# Replace version placeholder with actual version
CHANGELOG=$(echo "$CHANGELOG" | sed "s/v${MAJOR_MINOR}.x/v${VERSION}/g" | sed "s/${MAJOR_MINOR}.x/${VERSION}/g" | sed "s/YYYY-MM-DD/$(date +%Y-%m-%d)/g")
# The pattern file is still present at this tag (the archive step renames it afterwards)
SRC_FILE="$CHANGELOG_FILE_PATTERN"
else
# Fallback: Generate changelog from commits (exclude [skip ci] commits)
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)
COMMIT_LOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s" --no-merges | grep -v "\[skip ci\]" | head -50)
# Create a basic changelog structure
CHANGELOG="# Release v${VERSION}
## Changes
${COMMIT_LOG}
## Installation
\`\`\`bash
git clone https://github.com/atomantic/PortOS.git
cd PortOS
npm run install:all
pm2 start ecosystem.config.cjs
\`\`\`"
fi
# Write the body to a FILE consumed via body_path (NOT an action input
# env var). A large changelog (this repo's run hard, ~175KB) blows two
# separate ceilings when passed as `body:`: GitHub caps a release body
# at 125,000 chars (HTTP 422), and the action input becomes an env var
# whose size counts toward ARG_MAX when the action's node process is
# exec'd ("Argument list too long", which broke the v2.22.0 release).
# So: cap to a safe ceiling on a whole-line boundary and, when
# truncated, append a pointer to the full changelog file at this tag.
BODY_FILE="$RUNNER_TEMP/release-body.md"
LIMIT=120000
printf '%s\n' "$CHANGELOG" | awk -v lim=$LIMIT '
{ if (total + length($0) + 1 > lim) { exit } print; total += length($0) + 1 }
' > "$BODY_FILE"
FULL_LEN=$(printf '%s\n' "$CHANGELOG" | wc -c)
if [ "$(wc -c < "$BODY_FILE")" -lt "$FULL_LEN" ] && [ -n "$SRC_FILE" ]; then
FULL_URL="https://github.com/${REPO}/blob/v${VERSION}/${SRC_FILE#./}"
printf '\n---\n\n> **This changelog was truncated to fit GitHub'"'"'s release-note size limit.** Read the complete v%s changelog here: [%s](%s)\n' \
"$VERSION" "$SRC_FILE" "$FULL_URL" >> "$BODY_FILE"
fi
echo "body_path=$BODY_FILE" >> $GITHUB_OUTPUT
- name: Create Release
if: steps.release-check.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ steps.package-version.outputs.version }}
name: v${{ steps.package-version.outputs.version }}
body_path: ${{ steps.changelog.outputs.body_path }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Archive changelog on main
run: |
CURRENT_VERSION=${{ steps.package-version.outputs.version }}
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
MAJOR_MINOR="$MAJOR.$MINOR"
PATTERN_FILE=".changelog/v${MAJOR_MINOR}.x.md"
VERSIONED_FILE=".changelog/v${CURRENT_VERSION}.md"
if [ -f "$PATTERN_FILE" ]; then
# Archive directly on main so release never diverges
git fetch origin main
git checkout main
# After switching to main: re-check whether the pattern file is
# actually present. A prior workflow run that archived but failed
# before pushing back to release leaves the file present on the
# release branch (initial check) but absent on main (already moved).
# Skip cleanly in that case.
if [ ! -f "$PATTERN_FILE" ]; then
echo "Changelog already archived on main — nothing to do"
exit 0
fi
git mv "$PATTERN_FILE" "$VERSIONED_FILE"
sed -i.bak "s/v${MAJOR_MINOR}\.x/v${CURRENT_VERSION}/g; s/${MAJOR_MINOR}\.x/${CURRENT_VERSION}/g; s/YYYY-MM-DD/$(date +%Y-%m-%d)/g" "$VERSIONED_FILE"
rm "${VERSIONED_FILE}.bak"
git add "$VERSIONED_FILE"
git commit -m "docs: archive changelog for v${CURRENT_VERSION} [skip ci]"
git push origin main
# Fast-forward release to match main
git push origin main:release
fi