diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9dcfd0..b04c17a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,10 @@ jobs: # --- Artifact signing (mirrors auths/auths release workflow) --- - name: Install auths CLI run: | - curl -sL https://github.com/auths-dev/auths/releases/latest/download/auths-linux-x86_64.tar.gz | tar xz -C /usr/local/bin + mkdir -p /tmp/auths-install + curl -sL https://github.com/auths-dev/auths/releases/latest/download/auths-linux-x86_64.tar.gz | tar xz -C /tmp/auths-install + sudo cp /tmp/auths-install/auths /usr/local/bin/auths + rm -rf /tmp/auths-install - name: Sign dist/index.js env: diff --git a/justfile b/justfile index e20681f..2f32d60 100644 --- a/justfile +++ b/justfile @@ -17,15 +17,26 @@ check-dist: # Run the full CI suite locally: test + build + verify dist ci: test build check-dist +# Set up CI secrets for release artifact signing (one-time) +ci-setup: + bash scripts/ci-setup.sh + # Sign the dist/index.js artifact locally (creates dist/index.js.auths.json) sign-dist: auths artifact sign dist/index.js -# Cut a release: bump version, commit, then tag+push via release script +# Cut a release: bump version (if needed), commit, then tag+push via release script # The release workflow handles build verification, artifact signing, and GitHub release creation. # Usage: just release 1.0.3 release VERSION: test build - npm version {{VERSION}} --no-git-tag-version - git add package.json package-lock.json dist/ src/ .github/ justfile - git commit -m "build: bump version to {{VERSION}}" + #!/usr/bin/env bash + set -euo pipefail + CURRENT=$(node -p "require('./package.json').version") + if [ "$CURRENT" != "{{VERSION}}" ]; then + npm version {{VERSION}} --no-git-tag-version + git add package.json package-lock.json dist/ src/ .github/ justfile + git commit -m "build: bump version to {{VERSION}}" + else + echo "Version already {{VERSION}}, skipping bump" + fi python scripts/release.py --push diff --git a/package-lock.json b/package-lock.json index 0dfa264..d944c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@auths/verify-action", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@auths/verify-action", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "@actions/cache": "^3.2.4", diff --git a/package.json b/package.json index 3393f0d..737e040 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@auths/verify-action", - "version": "1.1.0", + "version": "1.2.0", "description": "GitHub Action to verify commit signatures using Auths identity keys", "main": "dist/index.js", "scripts": { diff --git a/scripts/ci-setup.sh b/scripts/ci-setup.sh new file mode 100755 index 0000000..dd59ba3 --- /dev/null +++ b/scripts/ci-setup.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# +# CI Release Signing Setup (One-Time) +# +# Creates a limited-capability device key for GitHub Actions to sign +# release artifacts. Your root identity stays on your machine. +# +# Prerequisites: +# - auths CLI installed and initialized (auths init) +# - gh CLI installed and authenticated (gh auth login) +# +# Usage: +# bash scripts/ci-setup.sh # interactive setup +# just ci-setup # same thing via justfile +# +set -euo pipefail + +CYAN='\033[0;36m' +GREEN='\033[0;32m' +BOLD='\033[1m' +DIM='\033[2m' +YELLOW='\033[1;33m' +RESET='\033[0m' + +echo "" +echo -e "${CYAN}╔════════════════════════════════════════════════════════════╗${RESET}" +echo -e "${CYAN}║${RESET}${BOLD} CI Release Signing Setup (One-Time) ${RESET}${CYAN}║${RESET}" +echo -e "${CYAN}╚════════════════════════════════════════════════════════════╝${RESET}" +echo "" +echo "This creates a limited-capability device for GitHub Actions to sign" +echo "release artifacts. Your root identity stays on your machine." +echo "" + +# --- Step 1: Verify identity exists --- +if ! auths status > /dev/null 2>&1; then + echo "ERROR: No auths identity found. Run 'auths init' first." >&2 + exit 1 +fi + +# --- Step 2: Read identity info --- +ID_OUTPUT=$(auths id show) +IDENTITY_DID=$(echo "$ID_OUTPUT" | grep "Controller DID:" | awk '{print $3}') +if [ -z "$IDENTITY_DID" ]; then + echo "ERROR: Could not parse Controller DID from 'auths id show'" >&2 + exit 1 +fi + +KEY_OUTPUT=$(auths key list) +IDENTITY_KEY_ALIAS=$(echo "$KEY_OUTPUT" | grep '^-' | head -1 | awk '{print $2}') +if [ -z "$IDENTITY_KEY_ALIAS" ]; then + echo "ERROR: Could not parse key alias from 'auths key list'" >&2 + exit 1 +fi + +echo -e "${BOLD}Identity:${RESET} ${CYAN}${IDENTITY_DID}${RESET}" +echo -e "${BOLD}Key alias:${RESET} ${CYAN}${IDENTITY_KEY_ALIAS}${RESET}" +echo "" + +# --- Step 3: Check for existing CI device key --- +REUSE=0 +if echo "$KEY_OUTPUT" | grep -q "ci-release-device"; then + echo -e "${DIM}Found existing ci-release-device key — will reuse it.${RESET}" + REUSE=1 +fi + +# --- Step 4: Prompt for passphrase --- +echo -e "${BOLD}Choose a passphrase for the CI device key.${RESET}" +echo -e "${DIM}This will be stored as AUTHS_CI_PASSPHRASE in GitHub Secrets.${RESET}" +echo "" + +read -rsp "CI device passphrase: " CI_PASS +echo "" +read -rsp "Confirm passphrase: " CI_PASS_CONFIRM +echo "" + +if [ "$CI_PASS" != "$CI_PASS_CONFIRM" ]; then + echo "ERROR: Passphrases do not match" >&2 + exit 1 +fi + +# --- Step 5: Generate seed + import key (or reuse existing) --- +TMPDIR_WORK=$(mktemp -d) +trap 'rm -rf "$TMPDIR_WORK"' EXIT + +if [ "$REUSE" -eq 0 ]; then + echo "" + echo -e "${DIM}Generating CI device key...${RESET}" + + # Generate a fresh 32-byte Ed25519 seed + SEED_PATH="$TMPDIR_WORK/ci-device-seed.bin" + dd if=/dev/urandom of="$SEED_PATH" bs=32 count=1 2>/dev/null + chmod 600 "$SEED_PATH" + + # Import the seed into platform keychain + echo -e "${DIM}Importing key into platform keychain:${RESET}" + auths key import \ + --alias ci-release-device \ + --seed-file "$SEED_PATH" \ + --controller-did "$IDENTITY_DID" + + echo -e "${GREEN}✓${RESET} CI device key imported into platform keychain" +fi + +# Create CI file keychain +KEYCHAIN_PATH="$TMPDIR_WORK/ci-keychain.enc" +if [ "$REUSE" -eq 0 ]; then + echo -e "${DIM}Creating CI file keychain...${RESET}" +else + echo -e "${DIM}Reusing existing ci-release-device key — regenerating CI file keychain...${RESET}" +fi + +AUTHS_PASSPHRASE="$CI_PASS" auths key copy-backend \ + --alias ci-release-device \ + --dst-backend file \ + --dst-file "$KEYCHAIN_PATH" + +KEYCHAIN_B64=$(base64 < "$KEYCHAIN_PATH" | tr -d '\n') +echo -e "${GREEN}✓${RESET} CI file keychain created" + +# --- Step 6: Derive device DID --- +DEVICE_PUB=$(auths key export \ + --alias ci-release-device \ + --passphrase "$CI_PASS" \ + --format pub) + +DEVICE_DID=$(auths debug util pubkey-to-did "$DEVICE_PUB") +echo -e "${GREEN}✓${RESET} Device DID: ${CYAN}${DEVICE_DID}${RESET}" + +# --- Step 7: Link device (if not already linked) --- +DEVICES_OUTPUT=$(auths device list 2>/dev/null || echo "") +if echo "$DEVICES_OUTPUT" | grep -q "$DEVICE_DID"; then + echo -e "${GREEN}✓${RESET} CI device already linked — skipping" +else + echo "" + echo -e "${DIM}Linking CI device to identity...${RESET}" + AUTHS_PASSPHRASE="$CI_PASS" auths device link \ + --key "$IDENTITY_KEY_ALIAS" \ + --device-key ci-release-device \ + --device-did "$DEVICE_DID" \ + --note "GitHub Actions release signer" \ + --capabilities sign_release + echo -e "${GREEN}✓${RESET} CI device linked" +fi + +# --- Step 8: Package identity repo --- +AUTHS_DIR="${HOME}/.auths" +echo -e "${DIM}Packaging identity repo...${RESET}" + +if ! git -C "$AUTHS_DIR" rev-parse --git-dir > /dev/null 2>&1; then + echo "ERROR: ~/.auths does not appear to be a git repository. Run 'auths init' first." >&2 + exit 1 +fi +echo -e " ${GREEN}✓${RESET} ~/.auths is a valid git repo" + +# Build tar.gz of ~/.auths, excluding *.sock files +BUNDLE_PATH="$TMPDIR_WORK/identity-bundle.tar.gz" +tar -czf "$BUNDLE_PATH" \ + --exclude='*.sock' \ + -C "$(dirname "$AUTHS_DIR")" \ + "$(basename "$AUTHS_DIR")" + +IDENTITY_BUNDLE_B64=$(base64 < "$BUNDLE_PATH" | tr -d '\n') + +# --- Step 9: Set GitHub secrets --- +echo "" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +echo -e "${BOLD} Setting GitHub Secrets:${RESET}" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +echo "" + +REPO=$(git remote get-url origin | sed 's/.*github\.com[:/]//' | sed 's/\.git$//') +if [ -z "$REPO" ] || ! echo "$REPO" | grep -q '/'; then + echo "ERROR: Could not extract owner/repo from git remote URL" >&2 + exit 1 +fi + +GH_OK=1 + +if ! gh auth status > /dev/null 2>&1; then + GH_OK=0 +fi + +if [ "$GH_OK" -eq 1 ]; then + echo -e "${DIM}Setting secrets via gh CLI...${RESET}" + + echo -n "$CI_PASS" | gh secret set AUTHS_CI_PASSPHRASE --repo "$REPO" || GH_OK=0 + echo -n "$KEYCHAIN_B64" | gh secret set AUTHS_CI_KEYCHAIN --repo "$REPO" || GH_OK=0 + echo -n "$IDENTITY_BUNDLE_B64" | gh secret set AUTHS_CI_IDENTITY_BUNDLE --repo "$REPO" || GH_OK=0 +fi + +if [ "$GH_OK" -eq 1 ]; then + echo -e "${GREEN}✓${RESET} All 3 secrets set on ${CYAN}${REPO}${RESET}" +else + echo -e "${YELLOW}Could not set secrets automatically.${RESET}" + echo -e "${DIM}Try: gh auth login then re-run, or add manually:${RESET}" + echo -e "${DIM} Repository → Settings → Secrets → Actions → New secret${RESET}" + echo "" + echo -e "${BOLD}AUTHS_CI_PASSPHRASE${RESET}" + echo "$CI_PASS" + echo "" + echo -e "${BOLD}AUTHS_CI_KEYCHAIN${RESET}" + echo "$KEYCHAIN_B64" + echo "" + echo -e "${BOLD}AUTHS_CI_IDENTITY_BUNDLE${RESET}" + echo "$IDENTITY_BUNDLE_B64" +fi + +echo "" +echo -e "${BOLD}To revoke CI access at any time:${RESET}" +echo -e " ${CYAN}auths device revoke --device-did ${DEVICE_DID} --key ${IDENTITY_KEY_ALIAS}${RESET}" +echo ""