Skip to content

Commit ee199ba

Browse files
committed
feat: trusted publishers
1 parent 87430ec commit ee199ba

25 files changed

Lines changed: 2103 additions & 207 deletions

File tree

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
name: NPM Trusted Release (Android engines)
2+
3+
# Publishes one or more engine-specific NativeScript Android runtime packages
4+
# (@nativescript/android-v8, @nativescript/android-hermes, @nativescript/android-jsc,
5+
# @nativescript/android-quickjs, @nativescript/android-quickjs-ng,
6+
# @nativescript/android-shermes, @nativescript/android-primjs) via npm trusted
7+
# publishing (OIDC).
8+
#
9+
# Each package must be configured on npmjs.com with a trusted publisher that
10+
# points at this repository + workflow + environment. With `engine: all`, the
11+
# workflow fans out across every engine via a matrix.
12+
13+
on:
14+
workflow_dispatch:
15+
inputs:
16+
engine:
17+
description: "Engine to release (or 'all' to publish every engine)"
18+
required: true
19+
type: choice
20+
default: v8
21+
options:
22+
- v8
23+
- hermes
24+
- jsc
25+
- quickjs
26+
- quickjs-ng
27+
- shermes
28+
- primjs
29+
- all
30+
release-type:
31+
description: "Version bump (patch/minor/major publish to 'latest'; prerelease uses 'preid' as the dist-tag)"
32+
required: false
33+
type: choice
34+
default: prerelease
35+
options:
36+
- prerelease
37+
- patch
38+
- minor
39+
- major
40+
version:
41+
description: "Exact npm version to publish; overrides release-type/preid. Use a prerelease version for preview publishes, e.g. 9.0.0-preview.0"
42+
required: false
43+
type: string
44+
preid:
45+
description: "Prerelease identifier (used only when release-type=prerelease; also becomes the npm dist-tag, e.g. next | canary)"
46+
required: false
47+
type: string
48+
default: next
49+
npm-tag:
50+
description: "Optional npm dist-tag override for publish, e.g. latest"
51+
required: false
52+
type: string
53+
dry-run:
54+
description: "Run release steps without making changes (no publish)"
55+
required: false
56+
type: boolean
57+
default: true
58+
59+
concurrency:
60+
# Avoid overlapping publishes on the same ref/engine selection.
61+
group: npm-trusted-release-${{ github.ref }}-${{ inputs.engine }}
62+
cancel-in-progress: false
63+
64+
jobs:
65+
matrix:
66+
name: Resolve engine matrix
67+
runs-on: ubuntu-latest
68+
permissions: {}
69+
outputs:
70+
engines: ${{ steps.compute.outputs.engines }}
71+
steps:
72+
- name: Compute matrix
73+
id: compute
74+
env:
75+
ENGINE: ${{ inputs.engine }}
76+
run: |
77+
set -euo pipefail
78+
case "$ENGINE" in
79+
all)
80+
echo 'engines=["v8","hermes","jsc","quickjs","quickjs-ng","shermes","primjs"]' >> "$GITHUB_OUTPUT"
81+
;;
82+
v8|hermes|jsc|quickjs|quickjs-ng|shermes|primjs)
83+
printf 'engines=["%s"]\n' "$ENGINE" >> "$GITHUB_OUTPUT"
84+
;;
85+
*)
86+
echo "Unsupported engine: $ENGINE" >&2
87+
exit 1
88+
;;
89+
esac
90+
91+
build:
92+
name: Build ${{ matrix.engine }}
93+
needs: matrix
94+
runs-on: ubuntu-latest
95+
permissions:
96+
contents: read
97+
strategy:
98+
fail-fast: false
99+
matrix:
100+
engine: ${{ fromJson(needs.matrix.outputs.engines) }}
101+
outputs:
102+
# Per-engine outputs aren't natively supported with matrices, so each job
103+
# uploads its computed metadata alongside the tarball artifact.
104+
placeholder: noop
105+
env:
106+
JS_PARSER_DIR: test-app/build-tools/jsparser
107+
steps:
108+
- name: Harden the runner (Audit all outbound calls)
109+
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
110+
with:
111+
egress-policy: audit
112+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
113+
with:
114+
fetch-depth: 0
115+
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
116+
with:
117+
node-version: 24
118+
registry-url: "https://registry.npmjs.org"
119+
- name: Set up JDK 17
120+
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
121+
with:
122+
java-version: "17"
123+
distribution: "temurin"
124+
- name: Install root dependencies
125+
run: npm install
126+
- name: Resolve engine target
127+
id: target
128+
shell: bash
129+
env:
130+
ENGINE: ${{ matrix.engine }}
131+
run: |
132+
set -euo pipefail
133+
case "$ENGINE" in
134+
v8) GRADLE_ENGINE=V8 ;;
135+
hermes) GRADLE_ENGINE=HERMES ;;
136+
jsc) GRADLE_ENGINE=JSC ;;
137+
quickjs) GRADLE_ENGINE=QUICKJS ;;
138+
quickjs-ng) GRADLE_ENGINE=QUICKJS_NG ;;
139+
shermes) GRADLE_ENGINE=SHERMES ;;
140+
primjs) GRADLE_ENGINE=PRIMJS ;;
141+
*) echo "Unsupported engine: $ENGINE" >&2; exit 1 ;;
142+
esac
143+
DIST_DIR="dist_$(printf '%s' "$GRADLE_ENGINE" | tr '[:upper:]' '[:lower:]')"
144+
echo "GRADLE_ENGINE=$GRADLE_ENGINE" >> "$GITHUB_OUTPUT"
145+
echo "DIST_DIR=$DIST_DIR" >> "$GITHUB_OUTPUT"
146+
echo "PACKAGE_NAME=@nativescript/android-${ENGINE}" >> "$GITHUB_OUTPUT"
147+
echo "PACKAGE_DIR=packages/android-${ENGINE}" >> "$GITHUB_OUTPUT"
148+
- name: Bump version
149+
id: bump
150+
shell: bash
151+
env:
152+
RELEASE_TYPE: ${{ inputs.release-type }}
153+
PACKAGE_VERSION: ${{ inputs.version }}
154+
PREID: ${{ inputs.preid }}
155+
NPM_TAG_OVERRIDE: ${{ inputs.npm-tag }}
156+
ENGINE: ${{ matrix.engine }}
157+
PACKAGE_DIR: ${{ steps.target.outputs.PACKAGE_DIR }}
158+
PACKAGE_NAME: ${{ steps.target.outputs.PACKAGE_NAME }}
159+
run: |
160+
set -euo pipefail
161+
release_type="$RELEASE_TYPE"
162+
package_version="$PACKAGE_VERSION"
163+
preid="$PREID"
164+
npm_tag_override="$NPM_TAG_OVERRIDE"
165+
166+
pushd "$PACKAGE_DIR" >/dev/null
167+
if [ -n "$package_version" ]; then
168+
npm version "$package_version" --no-git-tag-version >/dev/null
169+
elif [ "$release_type" = "prerelease" ]; then
170+
npm version prerelease --preid "$preid" --no-git-tag-version >/dev/null
171+
else
172+
npm version "$release_type" --no-git-tag-version >/dev/null
173+
fi
174+
NPM_VERSION=$(node -e "console.log(require('./package.json').version)")
175+
popd >/dev/null
176+
177+
NPM_TAG=$(NPM_VERSION="$NPM_VERSION" node ./scripts/get-npm-tag.js "android-${ENGINE}")
178+
if [ -n "$npm_tag_override" ]; then
179+
case "$npm_tag_override" in
180+
*[[:space:]]*)
181+
echo "Invalid npm tag override '$npm_tag_override': dist-tags cannot contain whitespace." >&2
182+
exit 1
183+
;;
184+
esac
185+
if printf '%s\n' "$npm_tag_override" | grep -Eq '^[0-9]+(\.[0-9]+)*$'; then
186+
echo "Invalid npm tag override '$npm_tag_override': dist-tags must not look like semver versions." >&2
187+
exit 1
188+
fi
189+
NPM_TAG="$npm_tag_override"
190+
fi
191+
if [ -n "$package_version" ] && [ "$release_type" = "prerelease" ] && [ -z "$npm_tag_override" ] && [ "$NPM_TAG" = "latest" ]; then
192+
echo "Exact prerelease publishes must include a prerelease identifier (for example 9.0.0-preview.0)." >&2
193+
exit 1
194+
fi
195+
196+
# Stamp the package identity into the root package.json so Gradle bakes the
197+
# correct name + version straight into the dist tarball.
198+
npm pkg set name="$PACKAGE_NAME" version="$NPM_VERSION"
199+
200+
echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT"
201+
echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT"
202+
echo "Resolved $PACKAGE_NAME@$NPM_VERSION (tag: $NPM_TAG)"
203+
- name: Install jsparser dependencies
204+
working-directory: ${{ env.JS_PARSER_DIR }}
205+
run: npm ci
206+
- name: Grant execute permission for gradlew
207+
run: chmod +x gradlew
208+
- name: Build (-Pengine=${{ steps.target.outputs.GRADLE_ENGINE }})
209+
env:
210+
GRADLE_ENGINE: ${{ steps.target.outputs.GRADLE_ENGINE }}
211+
run: ./gradlew -Pengine="${GRADLE_ENGINE}"
212+
- name: Record metadata
213+
shell: bash
214+
env:
215+
ENGINE: ${{ matrix.engine }}
216+
DIST_DIR: ${{ steps.target.outputs.DIST_DIR }}
217+
PACKAGE_NAME: ${{ steps.target.outputs.PACKAGE_NAME }}
218+
NPM_VERSION: ${{ steps.bump.outputs.NPM_VERSION }}
219+
NPM_TAG: ${{ steps.bump.outputs.NPM_TAG }}
220+
run: |
221+
set -euo pipefail
222+
tarball_file="nativescript-android-${ENGINE}-${NPM_VERSION}.tgz"
223+
if [ ! -f "${DIST_DIR}/${tarball_file}" ]; then
224+
echo "Expected tarball ${DIST_DIR}/${tarball_file} was not produced by the build." >&2
225+
ls -la "${DIST_DIR}" || true
226+
exit 1
227+
fi
228+
cat > "${DIST_DIR}/release-meta.json" <<EOF
229+
{
230+
"engine": "${ENGINE}",
231+
"dist_dir": "${DIST_DIR}",
232+
"package_name": "${PACKAGE_NAME}",
233+
"version": "${NPM_VERSION}",
234+
"tag": "${NPM_TAG}",
235+
"tarball": "${tarball_file}"
236+
}
237+
EOF
238+
- name: Upload npm package artifact
239+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
240+
with:
241+
name: npm-package-${{ matrix.engine }}
242+
path: |
243+
${{ steps.target.outputs.DIST_DIR }}/nativescript-android-${{ matrix.engine }}-${{ steps.bump.outputs.NPM_VERSION }}.tgz
244+
${{ steps.target.outputs.DIST_DIR }}/release-meta.json
245+
246+
publish:
247+
name: Publish ${{ matrix.engine }}
248+
needs:
249+
- matrix
250+
- build
251+
runs-on: ubuntu-latest
252+
environment:
253+
name: ${{ inputs.dry-run && 'npm-publish-dry-run' || 'npm-publish' }}
254+
strategy:
255+
fail-fast: false
256+
matrix:
257+
engine: ${{ fromJson(needs.matrix.outputs.engines) }}
258+
permissions:
259+
contents: read
260+
id-token: write
261+
steps:
262+
- name: Harden the runner (Audit all outbound calls)
263+
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
264+
with:
265+
egress-policy: audit
266+
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
267+
with:
268+
node-version: 24
269+
registry-url: "https://registry.npmjs.org"
270+
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
271+
with:
272+
name: npm-package-${{ matrix.engine }}
273+
path: npm-package/${{ matrix.engine }}
274+
- name: Update npm (required for OIDC trusted publishing)
275+
run: |
276+
corepack enable npm
277+
corepack install -g npm@11.6.2
278+
test "$(npm --version)" = "11.6.2"
279+
test "$(npx --version)" = "11.6.2"
280+
- name: Read release metadata
281+
id: meta
282+
shell: bash
283+
env:
284+
ENGINE: ${{ matrix.engine }}
285+
run: |
286+
set -euo pipefail
287+
meta="npm-package/${ENGINE}/release-meta.json"
288+
if [ ! -f "$meta" ]; then
289+
echo "Missing release metadata at $meta" >&2
290+
exit 1
291+
fi
292+
NPM_VERSION=$(node -e "console.log(require('./$meta').version)")
293+
NPM_TAG=$(node -e "console.log(require('./$meta').tag)")
294+
PACKAGE_NAME=$(node -e "console.log(require('./$meta').package_name)")
295+
TARBALL=$(node -e "console.log(require('./$meta').tarball)")
296+
echo "NPM_VERSION=$NPM_VERSION" >> "$GITHUB_OUTPUT"
297+
echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT"
298+
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
299+
echo "TARBALL=$TARBALL" >> "$GITHUB_OUTPUT"
300+
- name: Publish package (OIDC trusted publishing)
301+
if: ${{ vars.USE_NPM_TOKEN != 'true' }}
302+
shell: bash
303+
env:
304+
NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }}
305+
NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }}
306+
PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }}
307+
TARBALL: ${{ steps.meta.outputs.TARBALL }}
308+
ENGINE: ${{ matrix.engine }}
309+
DRY_RUN: ${{ inputs.dry-run }}
310+
NODE_AUTH_TOKEN: ""
311+
run: |
312+
set -euo pipefail
313+
TARBALL_PATH="npm-package/${ENGINE}/${TARBALL}"
314+
PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance)
315+
if [ "$DRY_RUN" = "true" ]; then
316+
PUBLISH_ARGS+=(--dry-run)
317+
fi
318+
echo "Publishing ${PACKAGE_NAME}@${NPM_VERSION} (tag: $NPM_TAG, dry-run: $DRY_RUN) via OIDC trusted publishing..."
319+
unset NODE_AUTH_TOKEN
320+
rm -f ~/.npmrc || true
321+
if [ -n "${NPM_CONFIG_USERCONFIG:-}" ]; then
322+
rm -f "$NPM_CONFIG_USERCONFIG" || true
323+
fi
324+
npm publish "${PUBLISH_ARGS[@]}"
325+
- name: Publish package (granular token fallback)
326+
if: ${{ vars.USE_NPM_TOKEN == 'true' }}
327+
shell: bash
328+
env:
329+
NPM_VERSION: ${{ steps.meta.outputs.NPM_VERSION }}
330+
NPM_TAG: ${{ steps.meta.outputs.NPM_TAG }}
331+
PACKAGE_NAME: ${{ steps.meta.outputs.PACKAGE_NAME }}
332+
TARBALL: ${{ steps.meta.outputs.TARBALL }}
333+
ENGINE: ${{ matrix.engine }}
334+
DRY_RUN: ${{ inputs.dry-run }}
335+
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
336+
run: |
337+
set -euo pipefail
338+
TARBALL_PATH="npm-package/${ENGINE}/${TARBALL}"
339+
PUBLISH_ARGS=("$TARBALL_PATH" --tag "$NPM_TAG" --access public --provenance)
340+
if [ "$DRY_RUN" = "true" ]; then
341+
PUBLISH_ARGS+=(--dry-run)
342+
fi
343+
echo "Publishing ${PACKAGE_NAME}@${NPM_VERSION} (tag: $NPM_TAG, dry-run: $DRY_RUN) via granular token..."
344+
npm publish "${PUBLISH_ARGS[@]}"
345+
346+
summary:
347+
name: Release summary
348+
if: always()
349+
needs:
350+
- matrix
351+
- build
352+
- publish
353+
runs-on: ubuntu-latest
354+
permissions: {}
355+
steps:
356+
- name: Print summary
357+
env:
358+
ENGINE_SELECTION: ${{ inputs.engine }}
359+
RELEASE_TYPE: ${{ inputs.release-type }}
360+
PACKAGE_VERSION: ${{ inputs.version }}
361+
PREID: ${{ inputs.preid }}
362+
NPM_TAG_OVERRIDE: ${{ inputs.npm-tag }}
363+
DRY_RUN: ${{ inputs.dry-run }}
364+
ENGINES: ${{ needs.matrix.outputs.engines }}
365+
BUILD_RESULT: ${{ needs.build.result }}
366+
PUBLISH_RESULT: ${{ needs.publish.result }}
367+
run: |
368+
echo "Engine selection: $ENGINE_SELECTION"
369+
echo "Release type: $RELEASE_TYPE"
370+
echo "Exact version: $PACKAGE_VERSION"
371+
echo "Preid: $PREID"
372+
echo "NPM tag override: $NPM_TAG_OVERRIDE"
373+
echo "Dry run: $DRY_RUN"
374+
echo "Engines: $ENGINES"
375+
echo "Build result: $BUILD_RESULT"
376+
echo "Publish result: $PUBLISH_RESULT"

0 commit comments

Comments
 (0)