Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 360 additions & 0 deletions .github/workflows/release-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
name: release npm

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_ref:
description: Git ref to build and publish from.
required: true
default: refs/heads/main
type: string
release_version:
description: SemVer version for dry-run metadata (without v prefix).
required: true
default: 0.0.0-dryrun
type: string
npm_dist_tag:
description: npm dist-tag for dry-run publishes (for example next).
required: true
default: next
type: string
publish_to_npm:
description: Publish to npm with provenance (requires npm trusted publisher).
required: true
default: true
type: boolean

permissions:
contents: write
id-token: write

jobs:
test:
name: "IntentProof Release: Test npm Package"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_ref || github.ref }}

- name: Checkout intentproof-tools (SPEC_REF source)
uses: actions/checkout@v6
with:
repository: IntentProof/intentproof-tools
ref: main
path: intentproof-tools

- name: Read pinned spec ref
id: spec_ref
run: |
ref="$(tr -d '[:space:]' < intentproof-tools/SPEC_REF)"
if ! echo "$ref" | grep -qE '^[0-9a-f]{40}$'; then
echo "Invalid SPEC_REF in intentproof-tools: '$ref'" >&2
exit 1
fi
echo "ref=$ref" >> "$GITHUB_OUTPUT"

- name: Checkout intentproof-spec at pinned ref
uses: actions/checkout@v6
with:
repository: IntentProof/intentproof-spec
ref: ${{ steps.spec_ref.outputs.ref }}
path: intentproof-spec

- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Verify sdk-signing fixtures match pinned spec
env:
INTENTPROOF_SPEC_DIR: intentproof-spec
run: bash scripts/check-sdk-signing-fixtures-sync.sh

- name: Run tests
env:
INTENTPROOF_SPEC_DIR: intentproof-spec
run: npm test

pack:
name: "IntentProof Release: Pack npm Package"
needs: test
runs-on: ubuntu-latest
outputs:
artifact_paths: ${{ steps.pack.outputs.artifact_paths }}
release_ref: ${{ steps.release.outputs.release_ref }}
release_version: ${{ steps.release.outputs.release_version }}
npm_dist_tag: ${{ steps.release.outputs.npm_dist_tag }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_ref || github.ref }}

- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'

- name: Resolve release metadata
id: release
env:
INPUT_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
INPUT_RELEASE_REF: ${{ inputs.release_ref }}
INPUT_RELEASE_VERSION: ${{ inputs.release_version }}
run: |
set -euo pipefail
semver_tag='^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'

if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
release_ref="$INPUT_RELEASE_REF"
release_version="$INPUT_RELEASE_VERSION"
npm_dist_tag="$INPUT_NPM_DIST_TAG"
else
release_ref="${GITHUB_REF}"
release_version="${GITHUB_REF_NAME#v}"
if [[ "$release_ref" =~ $semver_tag ]]; then
if [[ "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
npm_dist_tag="latest"
else
npm_dist_tag="${release_version#*-}"
npm_dist_tag="${npm_dist_tag%%.*}"
fi
else
echo "tag ref must match vMAJOR.MINOR.PATCH: ${release_ref}" >&2
exit 1
fi
fi

{
echo "release_ref=${release_ref}"
echo "release_version=${release_version}"
echo "npm_dist_tag=${npm_dist_tag}"
} >> "$GITHUB_OUTPUT"

- name: Install dependencies
run: npm ci

- name: Sync package version
run: npm version "${{ steps.release.outputs.release_version }}" --no-git-tag-version --allow-same-version

- name: Build package
run: npm run build

- name: Pack npm tarball
id: pack
run: |
set -euo pipefail
npm pack
mapfile -t tarballs < <(find . -maxdepth 1 -type f -name '*.tgz' | sort)
test "${#tarballs[@]}" -eq 1
{
echo "artifact_paths<<EOF"
printf '%s\n' "${tarballs[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- uses: actions/upload-artifact@v7
with:
name: npm-release-package
path: ./*.tgz

sign:
name: "IntentProof Release: Sign npm Package Tarball"
needs: pack
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.pack.outputs.release_ref }}

- uses: actions/download-artifact@v8
with:
name: npm-release-package
path: .

- uses: sigstore/cosign-installer@v3

- uses: anchore/sbom-action/download-syft@v0

- name: Sign tarball and generate release metadata
env:
ARTIFACT_PATHS: ${{ needs.pack.outputs.artifact_paths }}
RELEASE_VERSION: ${{ needs.pack.outputs.release_version }}
ATTEST_TO_REKOR: ${{ github.event_name != 'workflow_dispatch' }}
run: |
set -euo pipefail
tlog_args=()
if [[ "$ATTEST_TO_REKOR" != "true" ]]; then
tlog_args+=(--tlog-upload=false)
fi

mkdir -p release-attestations
: > release-attestations/SHA256SUMS

while IFS= read -r artifact; do
[[ -z "$artifact" ]] && continue
test -f "$artifact" || { echo "artifact not found: $artifact" >&2; exit 1; }

(
cd "$(dirname "$artifact")"
sha256sum "$(basename "$artifact")"
) >> release-attestations/SHA256SUMS
cosign sign-blob --yes \
"${tlog_args[@]}" \
--bundle "${artifact}.sigstore.json" \
--output-signature "${artifact}.sig" \
"$artifact"

syft packages "file:${artifact}" \
-o "spdx-json=${artifact}.spdx.json"

syft packages . \
-o "spdx-json=${artifact}.source.spdx.json"

cosign attest-blob --yes \
"${tlog_args[@]}" \
--type spdxjson \
--predicate "${artifact}.spdx.json" \
--bundle "${artifact}.sbom.sigstore.json" \
"$artifact"

artifact_sha="$(sha256sum "$artifact" | awk '{print $1}')"
ARTIFACT_PATH="$artifact" ARTIFACT_SHA="$artifact_sha" python3 - <<'PY'
import json
import os
from pathlib import Path

artifact_path = os.environ["ARTIFACT_PATH"]
run_id = os.environ["GITHUB_RUN_ID"]
predicate = {
"builder": {
"id": (
f"{os.environ['GITHUB_SERVER_URL']}/"
f"{os.environ['GITHUB_REPOSITORY']}/actions/runs/{run_id}"
)
},
"buildType": "https://github.com/IntentProof/release-npm",
"invocation": {
"parameters": {
"releaseVersion": os.environ["RELEASE_VERSION"],
"artifactPath": artifact_path,
"subjectName": "@intentproof/sdk",
}
},
"metadata": {
"buildInvocationId": (
f"{run_id}-{os.environ['GITHUB_RUN_ATTEMPT']}"
)
},
"materials": [
{
"uri": f"file:{artifact_path}",
"digest": {"sha256": os.environ["ARTIFACT_SHA"]},
}
],
}
Path(f"{artifact_path}.intoto.jsonl").write_text(
json.dumps(predicate, separators=(",", ":")) + "\n",
encoding="utf-8",
)
PY
cosign attest-blob --yes \
"${tlog_args[@]}" \
--type slsaprovenance \
--predicate "${artifact}.intoto.jsonl" \
--bundle "${artifact}.intoto.sigstore.json" \
"$artifact"
done <<< "$ARTIFACT_PATHS"

cosign sign-blob --yes \
"${tlog_args[@]}" \
--bundle release-attestations/SHA256SUMS.sigstore.json \
--output-signature release-attestations/SHA256SUMS.sig \
release-attestations/SHA256SUMS

- uses: actions/upload-artifact@v7
with:
name: release-signing-metadata
path: |
**/*.sig
**/*.sigstore.json
**/*.spdx.json
**/*.intoto.jsonl
release-attestations/SHA256SUMS

publish:
name: "IntentProof Release: Publish npm Package"
needs: pack
if: github.event_name != 'workflow_dispatch' || inputs.publish_to_npm
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'

- uses: actions/download-artifact@v8
with:
name: npm-release-package
path: .

- name: Publish with npm provenance
run: |
set -euo pipefail
if [[ -n "${NPM_CONFIG_USERCONFIG:-}" && -f "$NPM_CONFIG_USERCONFIG" ]]; then
sed -i '/:_authToken=/d' "$NPM_CONFIG_USERCONFIG"
fi
npm --version
node --version
mapfile -t tarballs < <(find . -maxdepth 1 -type f -name '*.tgz' | sort)
test "${#tarballs[@]}" -eq 1
npm publish "${tarballs[0]}" --provenance --access public \
--tag "${{ needs.pack.outputs.npm_dist_tag }}"

upload-release:
name: "IntentProof Release: Publish npm Release Artifacts"
needs:
- pack
- sign
- publish
if: github.event_name != 'workflow_dispatch' && needs.pack.result == 'success' && needs.sign.result == 'success' && needs.publish.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: npm-release-package
path: release-package

- uses: actions/download-artifact@v8
with:
name: release-signing-metadata
path: release-signing-metadata

- name: Upload tarball and signing metadata to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
mapfile -t files < <(find release-package release-signing-metadata -type f | sort)
test "${#files[@]}" -gt 0

if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
gh release create "$RELEASE_TAG" --generate-notes
fi
gh release upload "$RELEASE_TAG" "${files[@]}" --clobber