Skip to content

Commit 5ae7e43

Browse files
Burak Bayırkriptoburak
andauthored
ci: publish signed release provenance (#31)
Co-authored-by: kriptoburak <kriptoburak@users.noreply.github.com>
1 parent 7acf0bd commit 5ae7e43

6 files changed

Lines changed: 166 additions & 111 deletions

File tree

.github/workflows/publish-npm.yml

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Publish npm
33
on:
44
push:
55
tags:
6-
- "v*.*.*"
6+
- 'v*.*.*'
77

88
permissions: {}
99

@@ -19,8 +19,10 @@ jobs:
1919
timeout-minutes: 20
2020
environment: npm
2121
permissions:
22+
artifact-metadata: write
23+
attestations: write
2224
contents: read
23-
id-token: write # Required for npm trusted publishing and provenance.
25+
id-token: write
2426
steps:
2527
- name: Check out the release tag
2628
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -31,14 +33,14 @@ jobs:
3133
- name: Set up Node.js
3234
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
3335
with:
34-
node-version: "24.18.0"
35-
registry-url: "https://registry.npmjs.org"
36+
node-version: '24.18.0'
37+
registry-url: 'https://registry.npmjs.org'
3638
package-manager-cache: false
3739

3840
- name: Set up pnpm
3941
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
4042
with:
41-
version: "11.15.1"
43+
version: '11.15.1'
4244
run_install: false
4345

4446
- name: Verify release context
@@ -62,5 +64,109 @@ jobs:
6264
- name: Verify reproducible package
6365
run: pnpm run check:reproducible
6466

65-
- name: Build, inspect, and publish
66-
run: bash ./bin/publish-npm
67+
- name: Build and inspect the package
68+
run: bash ./bin/prepare-npm-release
69+
70+
- name: Pack the release artifact
71+
id: package
72+
working-directory: dist
73+
run: |
74+
set -euo pipefail
75+
76+
package_name="$(jq -r -e '.name' package.json)"
77+
package_version="$(jq -r -e '.version' package.json)"
78+
package_file="$(npm pack --json --pack-destination "$RUNNER_TEMP" | jq -r -e '.[0].filename')"
79+
package_path="$RUNNER_TEMP/$package_file"
80+
81+
test -f "$package_path"
82+
{
83+
echo "name=$package_name"
84+
echo "version=$package_version"
85+
echo "path=$package_path"
86+
} >> "$GITHUB_OUTPUT"
87+
88+
- name: Attest the release artifact
89+
uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
90+
with:
91+
subject-path: ${{ steps.package.outputs.path }}
92+
93+
- name: Export the signed provenance
94+
id: provenance
95+
env:
96+
GH_TOKEN: ${{ github.token }}
97+
PACKAGE_PATH: ${{ steps.package.outputs.path }}
98+
run: |
99+
set -euo pipefail
100+
101+
provenance_directory="$RUNNER_TEMP/provenance"
102+
mkdir -p "$provenance_directory"
103+
(
104+
cd "$provenance_directory"
105+
gh attestation download "$PACKAGE_PATH" --repo "$GITHUB_REPOSITORY"
106+
)
107+
108+
provenance_source="$(find "$provenance_directory" -maxdepth 1 -type f -name 'sha256*.jsonl' -print -quit)"
109+
test -n "$provenance_source"
110+
provenance_path="${PACKAGE_PATH}.intoto.jsonl"
111+
cp "$provenance_source" "$provenance_path"
112+
echo "path=$provenance_path" >> "$GITHUB_OUTPUT"
113+
114+
- name: Publish to npm
115+
env:
116+
PACKAGE_NAME: ${{ steps.package.outputs.name }}
117+
PACKAGE_PATH: ${{ steps.package.outputs.path }}
118+
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
119+
run: |
120+
set -euo pipefail
121+
122+
published_version="$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version 2>/dev/null || true)"
123+
if [[ -n "$published_version" ]]; then
124+
if [[ "$published_version" != "$PACKAGE_VERSION" ]]; then
125+
echo "The registry returned an unexpected version." >&2
126+
exit 1
127+
fi
128+
129+
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published."
130+
exit 0
131+
fi
132+
133+
npm publish "$PACKAGE_PATH" --access public --provenance --tag latest
134+
135+
- name: Stage signed release assets
136+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
137+
with:
138+
name: signed-release-assets
139+
path: |
140+
${{ steps.package.outputs.path }}
141+
${{ steps.provenance.outputs.path }}
142+
if-no-files-found: error
143+
retention-days: 1
144+
145+
release-assets:
146+
name: Publish Release Assets
147+
needs: publish
148+
runs-on: ubuntu-24.04
149+
timeout-minutes: 5
150+
permissions:
151+
actions: read
152+
contents: write
153+
steps:
154+
- name: Download signed release assets
155+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
156+
with:
157+
name: signed-release-assets
158+
path: signed-release-assets
159+
160+
- name: Publish signed release assets
161+
env:
162+
GH_TOKEN: ${{ github.token }}
163+
run: |
164+
set -euo pipefail
165+
166+
mapfile -t assets < <(find signed-release-assets -maxdepth 1 -type f -print)
167+
if [[ "${#assets[@]}" -ne 2 ]]; then
168+
echo "Expected one package and one provenance file." >&2
169+
exit 1
170+
fi
171+
172+
gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber

.github/workflows/release-doctor.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ on:
55
branches:
66
- main
77
paths:
8-
- ".github/scripts/verify-release-context.sh"
9-
- ".github/workflows/publish-npm.yml"
10-
- ".github/workflows/release-doctor.yml"
11-
- "bin/check-release-environment"
12-
- "bin/publish-npm"
13-
- "package.json"
8+
- '.github/scripts/verify-release-context.sh'
9+
- '.github/workflows/publish-npm.yml'
10+
- '.github/workflows/release-doctor.yml'
11+
- 'bin/check-release-environment'
12+
- 'bin/prepare-npm-release'
13+
- 'package.json'
1414
workflow_dispatch:
1515

1616
permissions: {}
@@ -36,7 +36,7 @@ jobs:
3636
- name: Set up Node.js
3737
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
3838
with:
39-
node-version: "24.18.0"
39+
node-version: '24.18.0'
4040
package-manager-cache: false
4141

4242
- name: Check release environment

CONTRIBUTING.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,12 @@ $ pnpm fix
116116

117117
## Publishing and releases
118118

119-
Changes made to this repository via the automated release PR pipeline should publish to npm automatically. If
120-
the changes aren't made through the automated pipeline, you may want to make releases manually.
119+
Changes made through the automated release pipeline publish to npm automatically.
121120

122121
### Publish with a GitHub workflow
123122

124-
You can release to package managers by using [the `Publish NPM` GitHub action](https://www.github.com/Xquik-dev/x-twitter-scraper-typescript/actions/workflows/publish-npm.yml). This requires a setup organization or repository secret to be set up.
125-
126-
### Publish manually
127-
128-
If you need to manually release a package, you can run the `bin/publish-npm` script with an `NPM_TOKEN` set on
129-
the environment.
123+
The [Publish npm workflow](https://github.com/Xquik-dev/x-twitter-scraper-typescript/actions/workflows/publish-npm.yml)
124+
validates release tags, builds the package, signs its provenance, uploads release assets, and publishes through
125+
npm trusted publishing. The release path uses short-lived identity tokens. It stores no npm token.
130126

131127
Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp.

bin/check-release-environment

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@ fi
1212
for required in \
1313
"environment: npm" \
1414
"id-token: write" \
15+
"attestations: write" \
16+
"artifact-metadata: write" \
1517
"EXPECTED_PACKAGE_NAME: x-twitter-scraper" \
16-
"bash ./bin/publish-npm"; do
18+
"bash ./bin/prepare-npm-release" \
19+
"actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d" \
20+
"actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" \
21+
"actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" \
22+
"gh release upload" \
23+
"npm publish"; do
1724
if ! grep -Fq "$required" "$workflow"; then
1825
errors+=("publish-npm.yml is missing: ${required}")
1926
fi
2027
done
2128

2229
for forbidden in "NPM_TOKEN" "NODE_AUTH_TOKEN" "workflow_dispatch"; do
23-
if grep -Fq "$forbidden" "$workflow" bin/publish-npm; then
30+
if grep -Fq "$forbidden" "$workflow" bin/prepare-npm-release; then
2431
errors+=("release path must not contain: ${forbidden}")
2532
fi
2633
done

bin/prepare-npm-release

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
3+
# SPDX-FileCopyrightText: 2026 Xquik contributors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
set -euo pipefail
8+
9+
pnpm build
10+
cd dist
11+
12+
package_name="$(jq -r -e '.name' package.json)"
13+
package_version="$(jq -r -e '.version' package.json)"
14+
source_version="$(jq -r -e '.version' ../package.json)"
15+
16+
if [[ "$package_name" != "x-twitter-scraper" ]]; then
17+
echo "Refusing to package unexpected project ${package_name}." >&2
18+
exit 1
19+
fi
20+
21+
if [[ "$package_version" != "$source_version" ]]; then
22+
echo "Built version ${package_version} does not match source version ${source_version}." >&2
23+
exit 1
24+
fi
25+
26+
for package_file in package.json README.md; do
27+
if ! grep -Fq "Not affiliated with X Corp." "$package_file"; then
28+
echo "${package_file} must include the X Corp. non-affiliation notice." >&2
29+
exit 1
30+
fi
31+
done
32+
33+
npm pack --dry-run --json >/dev/null

bin/publish-npm

Lines changed: 0 additions & 87 deletions
This file was deleted.

0 commit comments

Comments
 (0)