-
Notifications
You must be signed in to change notification settings - Fork 0
195 lines (162 loc) · 6.23 KB
/
Copy pathrelease.yml
File metadata and controls
195 lines (162 loc) · 6.23 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
name: Publish Package
on:
push:
tags:
- 'v*'
permissions:
id-token: write # Required for OIDC trusted publishing
contents: write # Required for creating GitHub releases
jobs:
publish:
# npm provenance publishing only supports GitHub-hosted Actions runners.
# Keep this job on ubuntu-latest unless npm adds self-hosted provenance support.
runs-on: ubuntu-latest
# Workflow-context values are bound to env here and referenced as
# shell variables ($TAG/$REPO/$COMMIT_SHA) in run: blocks instead of
# `${{ }}` interpolation, so an attacker-controlled tag name cannot be
# injected into a script body. Tag pushes are attacker-controllable:
# anyone able to push a v* tag triggers this workflow.
env:
TAG: ${{ github.ref_name }}
REPO: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
steps:
- name: Validate tag format
# Fail closed before any other step runs. A strict semver gate
# rejects a tag containing shell metacharacters ($(), backticks,
# ;, |) so it never reaches a later run: block.
run: |
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "❌ Error: Tag '$TAG' is not a valid vMAJOR.MINOR.PATCH semver tag"
echo "Releases must be triggered by a strict semver tag, e.g. v1.2.3 or v1.2.3-rc.1"
exit 1
fi
echo "✅ Tag format validated: $TAG"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Verify tag is on main branch
run: |
git fetch origin main
if ! git merge-base --is-ancestor "$COMMIT_SHA" origin/main; then
echo "❌ Tag is not on the main branch — aborting release"
exit 1
fi
echo "✅ Tag is on main branch"
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22.14.0"
cache: 'pnpm'
# No registry-url - using OIDC trusted publishing instead
- name: Update npm for trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Extract version from tag
id: version
run: |
# $GITHUB_REF is a runner-provided env var (safe shell
# expansion, not template interpolation); the tag was strictly
# validated above, so VERSION is a clean semver string.
VERSION="${GITHUB_REF#refs/tags/v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing version: $VERSION"
- name: Verify tag matches package.json version
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$VERSION" != "$PKG_VERSION" ]; then
echo "❌ Tag v$VERSION does not match package.json version $PKG_VERSION"
exit 1
fi
echo "✅ Version confirmed: $VERSION"
- name: Build
run: pnpm build
- name: Test
run: pnpm test
env:
TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
- name: Publish to npm
run: npm publish --provenance
# Explicitly use --provenance flag for clarity
# OIDC trusted publishing (id-token: write) enables automatic provenance generation
- name: Generate release notes
id: release_notes
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
PREV_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${TAG}$" | head -1)
RELEASE_DATE=$(date +%Y-%m-%d)
if [ -n "$PREV_TAG" ]; then
COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"%s %h" --no-merges)
else
COMMITS=$(git log --pretty=format:"%s %h" --no-merges)
fi
FEATURES=""
FIXES=""
OTHER=""
while IFS=$'\t' read -r message hash; do
[ -z "$message" ] && continue
if [[ $message =~ \(#([0-9]+)\) ]]; then
PR_NUM="${BASH_REMATCH[1]}"
CLEAN_MESSAGE=$(echo "$message" | sed -E 's/ ?\(#[0-9]+\)//')
PR_LINK="[#$PR_NUM](https://github.com/${REPO}/pull/$PR_NUM)"
COMMIT_LINK="[$hash](https://github.com/${REPO}/commit/$hash)"
ITEM="$CLEAN_MESSAGE ($PR_LINK) ($COMMIT_LINK)"
else
COMMIT_LINK="[$hash](https://github.com/${REPO}/commit/$hash)"
ITEM="$message ($COMMIT_LINK)"
fi
if [[ $message =~ ^feat(\([^\)]+\))?: ]]; then
STRIPPED=$(echo "$ITEM" | sed -E 's/^feat(\([^)]+\))?: //')
FEATURES="${FEATURES}- ${STRIPPED}
"
elif [[ $message =~ ^fix(\([^\)]+\))?: ]]; then
STRIPPED=$(echo "$ITEM" | sed -E 's/^fix(\([^)]+\))?: //')
FIXES="${FIXES}- ${STRIPPED}
"
else
OTHER="${OTHER}- ${ITEM}
"
fi
done <<< "$COMMITS"
cat > release_notes.md <<EOF
$VERSION ($RELEASE_DATE)
EOF
if [ -n "$FEATURES" ]; then
cat >> release_notes.md <<EOF
## Features
$FEATURES
EOF
fi
if [ -n "$FIXES" ]; then
cat >> release_notes.md <<EOF
## Bug Fixes
$FIXES
EOF
fi
if [ -n "$OTHER" ]; then
cat >> release_notes.md <<EOF
## Changes
$OTHER
EOF
fi
cat >> release_notes.md <<EOF
## Install
\`\`\`bash
npm install -g @formo/cli@$VERSION
\`\`\`
EOF
- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
body_path: release_notes.md
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}