-
-
Notifications
You must be signed in to change notification settings - Fork 30
213 lines (181 loc) · 8.55 KB
/
publish.yml
File metadata and controls
213 lines (181 loc) · 8.55 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
name: Publish
on:
# Trigger after the Test workflow completes on main.
# This ensures tests pass before publishing.
workflow_run:
workflows: ["Test"]
types: [completed]
branches: [main]
# Allow manual triggering from the Actions tab
workflow_dispatch:
inputs:
mode:
description: How this run should exercise npm publishing
required: true
type: choice
options:
- oidc-check # Only test the OIDC token exchange — no publish
- dry-run # Preview what would be published
- publish # Actually publish to npm
default: oidc-check
npm_tag:
# npm dist-tags control what users get when they run `npm install @tryghost/<pkg>`.
# "latest" is the default tag — it's what `npm install` resolves to.
# Use other tags (e.g. "beta", "next") to publish pre-release versions
# that users must opt into with `npm install @tryghost/<pkg>@beta`.
description: Dist-tag to use for manual runs (e.g. latest, beta)
required: true
type: string
default: latest
jobs:
publish:
name: Publish to npm
runs-on: ubuntu-latest
# Only run when:
# - Manual dispatch (oidc-check, dry-run, publish), OR
# - Test workflow completed successfully on a version commit (created by `pnpm ship`)
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_commit.message, 'Published new versions'))
permissions:
contents: read
id-token: write # Required for OIDC token exchange with npm trusted publishers
env:
FORCE_COLOR: 1
CI: true
# Enables build provenance attestation on published packages
NPM_CONFIG_PROVENANCE: true
# Prevent classic token auth from interfering with OIDC (some orgs inject NODE_AUTH_TOKEN)
NODE_AUTH_TOKEN: ""
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Install pnpm (reads version from packageManager field in package.json)
- uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6
# First setup-node: used for building (keeps the monorepo on its expected Node version)
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: pnpm
env:
NODE_AUTH_TOKEN: ""
- name: Install dependencies
run: pnpm install
# Build is required because published packages ship compiled output (build/)
- name: Build all packages
run: pnpm build
# Second setup-node: used for publishing only.
# Node 24 has better OIDC trusted publisher support in its bundled npm.
# registry-url is required for npm to authenticate via the OIDC token.
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.x
registry-url: https://registry.npmjs.org
env:
NODE_AUTH_TOKEN: ""
# Determine what this run should do based on trigger type
- name: Resolve publish mode
id: publish_mode
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "mode=${{ inputs.mode }}" >> "$GITHUB_OUTPUT"
echo "tag=${{ inputs.npm_tag }}" >> "$GITHUB_OUTPUT"
else
# workflow_run-triggered runs always publish with the "latest" tag
echo "mode=publish" >> "$GITHUB_OUTPUT"
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi
# Ensure we have the latest npm for trusted publishing support
- name: Upgrade npm
run: npm install -g npm@latest
- name: Print runtime versions
run: |
node --version
npm --version
pnpm --version
# actions/setup-node writes _authToken to .npmrc which can conflict with OIDC auth.
# Strip it so npm falls through to OIDC token exchange.
- name: Strip npm auth token from npmrc (OIDC only)
run: |
set -euo pipefail
npmrc="$(npm config get userconfig)"
echo "using npmrc=$npmrc"
sed -i.bak '/_authToken/d' "$npmrc" || true
echo "auth token lines remaining? $(grep -c _authToken "$npmrc" || true)"
# Manually test the OIDC token exchange with npm's API.
# This pinpoints whether GitHub is issuing the token and whether npm accepts it,
# so we get a clear error instead of a cryptic 404 on publish.
- name: Check npm OIDC exchange (probe)
run: |
set -euo pipefail
echo "ACTIONS_ID_TOKEN_REQUEST_URL=${ACTIONS_ID_TOKEN_REQUEST_URL:+present}"
echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN=${ACTIONS_ID_TOKEN_REQUEST_TOKEN:+present}"
# 1) Request a GitHub OIDC token with npm as the audience
OIDC_RESPONSE=$(curl --silent --show-error --fail \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org")
OIDC_TOKEN=$(node -p "JSON.parse(process.argv[1]).value" "$OIDC_RESPONSE")
# 2) Exchange it with npm for a representative package to verify trust config
PROBE_PKG='@tryghost/migrate'
ENCODED_PKG=$(node -p "encodeURIComponent(process.argv[1])" "$PROBE_PKG")
HTTP_STATUS=$(curl --silent --show-error --output oidc-exchange.json --write-out "%{http_code}" \
--request POST \
--header "Authorization: Bearer ${OIDC_TOKEN}" \
"https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/${ENCODED_PKG}")
if [ "$HTTP_STATUS" != "201" ]; then
echo "npm OIDC exchange failed with HTTP ${HTTP_STATUS}"
cat oidc-exchange.json
exit 1
fi
node -p "const response=require('./oidc-exchange.json'); 'npm OIDC exchange succeeded; token expires at ' + response.expires"
rm oidc-exchange.json
# In oidc-check mode, stop here — we only wanted to verify the token exchange
- name: Stop after OIDC check
if: steps.publish_mode.outputs.mode == 'oidc-check'
run: echo "OIDC check complete; stopping (mode=oidc-check)."
# Iterate over all packages, skip already-published versions, and publish new ones.
# Uses pnpm pack to resolve workspace:* references to real versions in the tarball,
# then npm publish for OIDC trusted publisher support.
- name: Publish to npm
if: steps.publish_mode.outputs.mode != 'oidc-check'
env:
NODE_AUTH_TOKEN: ""
run: |
set -euo pipefail
unset NODE_AUTH_TOKEN
DRY_RUN_FLAG=""
if [ "${{ steps.publish_mode.outputs.mode }}" = "dry-run" ]; then
DRY_RUN_FLAG="--dry-run"
fi
TAG="${{ steps.publish_mode.outputs.tag }}"
failed=0
for pkg in packages/*/; do
# Skip directories without a package.json
[ -f "$pkg/package.json" ] || continue
# Read package metadata from package.json
name=$(node -p "require('./$pkg/package.json').name")
version=$(node -p "require('./$pkg/package.json').version")
private=$(node -p "require('./$pkg/package.json').private || false")
# Skip private packages
if [ "$private" = "true" ]; then
echo "Skipping $name (private)"
continue
fi
# Check if this exact version already exists on the registry — skip if so
# Uses grep -x for full-line matching to avoid partial hits (e.g. 1.2.3 matching 11.2.3)
if npm view "$name@$version" version 2>/dev/null | grep -qx "$version"; then
echo "Skipping $name@$version (already published)"
continue
fi
# pnpm pack resolves workspace:* to real version numbers in the tarball.
# npm publish handles the OIDC token exchange for trusted publishers.
# --access public is required for scoped @tryghost packages.
# --provenance attaches build attestation via OIDC (belt-and-suspenders with env var).
echo "Publishing $name@$version..."
if ! (cd "$pkg" && pnpm pack && npm publish *.tgz --access public --tag "$TAG" --provenance $DRY_RUN_FLAG && rm -f *.tgz); then
echo "::error::Failed to publish $name@$version"
failed=1
fi
done
# Fail the workflow if any package failed to publish
exit $failed