Skip to content

Instantly share code, notes, and snippets.

@tunnckoCore
Last active November 29, 2025 15:59
Show Gist options
  • Select an option

  • Save tunnckoCore/a8200f034d10bd4c71c93b3ac85a8d48 to your computer and use it in GitHub Desktop.

Select an option

Save tunnckoCore/a8200f034d10bd4c71c93b3ac85a8d48 to your computer and use it in GitHub Desktop.
Secure Release Protocol for node/typescript/npm packages, without publishing to NPM registry. Everything is secured by Github features, and it's Github-only and Github-native.

Secure Release Protocol

This document outlines the secure, tamper-resistant release protocol for npm packages. The setup uses GitHub Actions workflows to automate building, attesting provenance, and releasing packages while enforcing immutability through repository rulesets. This ensures that releases are verifiable, auditable, and protected against unauthorized modifications.

Overview

The protocol consists of two chained GitHub Actions workflows:

  1. Build & Push Workflow: Handles building the package and pushing built artifacts to a dedicated release branch.
  2. Attest & Release Workflow: Triggered only after a successful build; generates Sigstore attestations, creates GitHub releases, uploads assets.

Releases are published as immutable npm packages installable via npm install github:{owner}/{repo}#v{version}, with provenance attestations for verification.

Key Principles

  • Chaining: Release workflows only run after successful builds, preventing arbitrary pushes from triggering releases.
  • Provenance: Every release includes a Sigstore attestation proving the package's origin and integrity.
  • Immutability: Release branches and tags are locked down via GitHub Rulesets, and releases are made immutable to block deletions, updates, or modifications.
  • Automation: Dependency bots (e.g., Renovate, Dependabot) detect new versions automatically via tags.
  • Transparency: All steps are logged in GitHub Actions, with attestations downloadable for verification.

Workflow Details

1. Build & Push Workflow (build-and-push.yml)

Trigger: Push to main branch (or manual dispatch for testing).

Steps:

  • Checkout the latest main.
  • Set up Bun runtime.
  • Install dependencies and run tests.
  • Build the package (e.g., generate dist/).
  • Extract version from package.json (e.g., 1.2.3).
  • Create a new branch release/v{version} containing only built artifacts (dist/, package.json, etc.).
  • Push the branch to trigger the downstream workflow.

Purpose: Isolates build logic and ensures only vetted, built code reaches release branches.

2. Attest & Release Workflow (attest-and-release.yml)

Trigger: workflow_run event on completion of the Build & Push workflow, filtered to release/v* branches, and only if the build succeeded.

Steps:

  • Extract version from the triggering branch name (e.g., release/v1.2.31.2.3).
  • Checkout the release/v{version} branch.
  • Set up Bun.
  • Pack the package into a .tgz file using bun pm pack.
  • Generate a Sigstore attestation for the .tgz using actions/attest-build-provenance.
  • Download the attestation as a JSON file (sigstore-provenance-v{version}.json).
  • Create a Git tag v{version} pointing to the branch's commit.
  • Create a GitHub Release for the tag, including release notes with installation instructions.
  • Upload the .tgz package and provenance JSON as release assets.
  • Push the tag to the remote.

Purpose: Handles attestation and release creation without exposing sensitive build steps.

Security Measures

Provenance and Attestation

  • Sigstore Integration: Uses GitHub's built-in Sigstore attestation to cryptographically prove the package was built in CI and hasn't been tampered with.
  • Offline Verification: The attestation JSON is uploaded as a release asset, allowing users to verify provenance even if GitHub's attestation view is unavailable.
  • Subject: Attestation is tied to the packed .tgz file, ensuring the distributable artifact is attested.

Immutable Releases

  • GitHub Immutable Releases: Once enabled, release assets (including the .tgz package and provenance JSON) cannot be modified or deleted after publication, providing permanent tamper resistance.
  • Enabled on setup: Enabled by repository admin via Github CLI on initial setup phase.

Repository Protection

  • Rulesets: Manual creation of GitHub Rulesets via a local script (run with admin permissions):
    • Protect Release Branches: Blocks deletions, updates, force pushes, and enforces linear history on refs/heads/release/v*.
    • Protect Release Tags: Blocks deletions, updates, force pushes, and enforces linear history on refs/tags/v*.
  • No Bypass: Rules apply to all users, including admins, for maximum security.
  • Immutable Releases: Enabled on initial setup via Github CLI/API to make release assets unmodifiable.
  • Immutability: Release branches/tags and assets are permanently locked down.

Trigger Hardening

  • Chained Workflows: The attest/release workflow only runs after a successful build workflow, preventing malicious pushes to release/v* from bypassing the build.
  • Branch Filtering: Only release/v* branches trigger the downstream workflow.
  • Success Guard: Explicit check for github.event.workflow_run.conclusion == 'success'.

Permissions and Tools

  • Required Permissions: contents: write, attestations: write, id-token: write
  • Tools: Bun for packaging, GitHub CLI (gh) for releases/attestations, jq for JSON parsing.
  • No Exposed Secrets: Relies on GITHUB_TOKEN and OIDC; no hardcoded keys, nor tokens or PATs.

Installation and Usage

Once a release is published:

  • Install via GitHub: npm install github:{owner}/{repo}#v{version}.
  • Verify Provenance: Download the sigstore-provenance-v{version}.json from the release assets and use Sigstore tools (e.g., cosign verify-blob) to check the attestation.
  • Immutable Assets: Release assets are immutable, ensuring the package and provenance remain unaltered.
  • Automation: Bots like Renovate will auto-detect new v* tags and update dependencies.

Maintenance

  • Updates: If package structure changes, update the build steps (e.g., what gets pushed to release/v*).
  • Ruleset Management: Rulesets and immutable releases are set up on initial setup phase.
  • Script Execution: Run bash initial-setup.sh locally with gh authenticated as a repo admin to apply rulesets and enable immutable releases.
  • Logs: Review workflow runs for attestation details or API errors.

This protocol ensures releases are secure, verifiable, and bot-friendly. For questions or issues, refer to the workflow YAML files or GitHub Actions logs.

name: Attest & Release
on:
workflow_run:
workflows: ["Build & Push"]
types: [completed]
branches: [release/v*]
permissions:
contents: write
attestations: write
id-token: write
jobs:
attest-and-release:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Get Version from Branch
run: |
VERSION=$(echo ${{ github.ref_name }} | sed 's/release\/v//')
echo "RELEASE_VER=$VERSION" >> $GITHUB_ENV
- uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
- uses: oven-sh/setup-bun@v2
- name: Pack Package Artifacts
run: bun pm pack
- name: Attest Provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: "*.tgz"
- name: Download Attestation
run: gh attestation download "*.tgz" --format json > sigstore-provenance-v${{ env.RELEASE_VER }}.json
- name: Create Tag
run: git tag v${{ env.RELEASE_VER }}
- name: Create Release and Upload Assets
run: |
gh release create v${{ env.RELEASE_VER }} "*.tgz" sigstore-provenance-*.json \
--title "Release v${{ env.RELEASE_VER }}" \
--notes 'Usage: `npm install github:${{ env.GITHUB_REPOSITORY }}#v${{ env.RELEASE_VER }}`'
- name: Push Tag
run: git push origin v${{ env.RELEASE_VER }}
name: Build & Push
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run test
- run: bun run build
- name: Get version
id: get_version
run: echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT
- name: Create & Push Branch
run: |
git checkout -b release/v${{ steps.get_version.outputs.version }}
git add dist/ package.json
git commit -m "Release v${{ steps.get_version.outputs.version }}"
git push origin release/v${{ steps.get_version.outputs.version }}
#!/usr/bin/env bash
set -euo pipefail
# TODO: update accordingly
REPO="user/repo"
echo "Setting up for repo: $REPO"
# Check GitHub CLI authentication
echo "Checking GitHub CLI authentication..."
if ! gh auth status >/dev/null 2>&1; then
echo "GitHub CLI not authenticated. Run 'gh auth login'." >&2
exit 1
fi
# Check repo access
echo "Checking repo access..."
if ! gh api "repos/${REPO}" >/dev/null 2>&1; then
echo "Repo $REPO not accessible. Check permissions or repo name." >&2
exit 1
fi
# Check and enable immutable releases
echo "Checking current immutable releases status..."
current_status=$(gh api "repos/${REPO}/immutable-releases" 2>/dev/null || echo '{"enabled": false}')
if echo "$current_status" | jq -r '.enabled // false' | grep -q true; then
echo "Immutable releases already enabled."
else
echo "Enabling immutable releases..."
gh api "repos/${REPO}/immutable-releases" --method PUT >/dev/null
echo "Verifying enablement..."
new_status=$(gh api "repos/${REPO}/immutable-releases" 2>/dev/null || echo '{"enabled": false}')
if echo "$new_status" | jq -r '.enabled // false' | grep -q true; then
echo "Immutable releases enabled successfully."
else
echo "Failed to enable immutable releases." >&2
exit 1
fi
fi
# Add Release Branch Ruleset
echo "Checking for 'Protect Release Branches' ruleset..."
if gh api "repos/$REPO/rulesets" | jq -e '.[] | select(.name == "Protect Release Branches")' >/dev/null 2>&1; then
echo "The 'Protect Release Branches' ruleset already exists."
else
echo "Creating 'Protect Release Branches' ruleset..."
gh api "repos/$REPO/rulesets" --method POST --input - <<EOF >/dev/null
{
"name": "Protect Release Branches",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/release/v*"],
"exclude": []
}
},
"rules": [
{"type": "deletion"},
{"type": "update"},
{"type": "non_fast_forward"},
{"type": "required_linear_history"}
]
}
EOF
echo "Verifying 'Protect Release Branches' ruleset creation..."
if gh api "repos/$REPO/rulesets" | jq -e '.[] | select(.name == "Protect Release Branches")' >/dev/null 2>&1; then
echo "The 'Protect Release Branches' ruleset created successfully."
else
echo "Failed to create 'Protect Release Branches' ruleset." >&2
exit 1
fi
fi
# Add Release Tags Ruleset
echo "Checking for 'Protect Release Tags' ruleset..."
if gh api "repos/$REPO/rulesets" | jq -e '.[] | select(.name == "Protect Release Tags")' >/dev/null 2>&1; then
echo "The 'Protect Release Tags' ruleset already exists."
else
echo "Creating 'Protect Release Tags' ruleset..."
gh api "repos/$REPO/rulesets" --method POST --input - <<EOF >/dev/null
{
"name": "Protect Release Tags",
"target": "tag",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/tags/v*"],
"exclude": []
}
},
"rules": [
{"type": "deletion"},
{"type": "update"},
{"type": "non_fast_forward"},
{"type": "required_linear_history"}
]
}
EOF
echo "Verifying 'Protect Release Tags' ruleset creation..."
if gh api "repos/$REPO/rulesets" | jq -e '.[] | select(.name == "Protect Release Tags")' >/dev/null 2>&1; then
echo "The 'Protect Release Tags' ruleset created successfully."
else
echo "Failed to create 'Protect Release Tags' ruleset." >&2
exit 1
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment