From 276e05dd8672896f5f12f206944a5a8e009c8d7d Mon Sep 17 00:00:00 2001 From: Ben Limmer Date: Sun, 24 May 2026 10:51:07 -0600 Subject: [PATCH] ci: add goreleaser release pipeline and analyze.sh installer goreleaser (bun --compile for darwin arm64/x64 + linux x64, macOS notarization, version-less archives) + release-please (node) + tag-triggered release.yml assuming the AWS release role via OIDC. Adds scripts/analyze.sh one-off runner (shipped as a release asset), zizmor + conventional-commit PR-title checks, and a README run-it rewrite. --- .../actions/fetch-release-secrets/action.yml | 85 +++++++++++ .github/workflows/lint-pr-title.yml | 35 +++++ .github/workflows/release-please.yml | 34 +++++ .github/workflows/release.yml | 44 ++++++ .github/workflows/test.yml | 4 + .github/workflows/zizmor.yml | 58 +++++++ .gitignore | 1 + .goreleaser.yaml | 64 ++++++++ .release-please-manifest.json | 3 + README.md | 22 ++- entitlements.plist | 21 +++ release-please-config.json | 26 ++++ scripts/analyze.sh | 143 ++++++++++++++++++ scripts/analyze.test.sh | 109 +++++++++++++ zizmor.yml | 1 + 15 files changed, 642 insertions(+), 8 deletions(-) create mode 100644 .github/actions/fetch-release-secrets/action.yml create mode 100644 .github/workflows/lint-pr-title.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/zizmor.yml create mode 100644 .goreleaser.yaml create mode 100644 .release-please-manifest.json create mode 100644 entitlements.plist create mode 100644 release-please-config.json create mode 100644 scripts/analyze.sh create mode 100644 scripts/analyze.test.sh create mode 100644 zizmor.yml diff --git a/.github/actions/fetch-release-secrets/action.yml b/.github/actions/fetch-release-secrets/action.yml new file mode 100644 index 0000000..02f120c --- /dev/null +++ b/.github/actions/fetch-release-secrets/action.yml @@ -0,0 +1,85 @@ +name: Fetch Release Secrets +description: Fetches release environment variables from AWS SSM Parameter Store and Secrets Manager + +inputs: + aws-region: + description: 'AWS region where secrets are stored' + required: false + default: 'us-west-2' + role-to-assume: + description: 'IAM role ARN to assume for secrets access' + required: true + parameter-path: + description: 'SSM parameter path prefix' + required: false + default: '/patchwave-analysis/ci/release/env-vars' + certificate-secret-id: + description: 'Secrets Manager secret ID for the signing certificate' + required: false + default: 'patchwave-analysis/ci/release/env-vars/MACOS_SIGN_P12' + +runs: + using: 'composite' + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ inputs.role-to-assume }} + aws-region: ${{ inputs.aws-region }} + + - name: Fetch MACOS_SIGN_P12 from Secrets Manager + shell: bash + env: + CERTIFICATE_SECRET_ID: ${{ inputs.certificate-secret-id }} + run: | + set -euo pipefail + + value=$(aws secretsmanager get-secret-value \ + --secret-id "$CERTIFICATE_SECRET_ID" \ + --query 'SecretString' \ + --output text) + + echo "::add-mask::$value" + + { + echo "MACOS_SIGN_P12<> "$GITHUB_ENV" + + echo "Exported: MACOS_SIGN_P12 (from Secrets Manager)" + + - name: Fetch secrets from SSM + shell: bash + env: + PARAMETER_PATH: ${{ inputs.parameter-path }} + run: | + set -euo pipefail + + PARAM_PATH="$PARAMETER_PATH" + + PARAMS=$(aws ssm get-parameters-by-path \ + --path "$PARAM_PATH" \ + --recursive \ + --with-decryption \ + --query 'Parameters[*].[Name,Value]' \ + --output text) + + if [ -z "$PARAMS" ]; then + echo "::error::No parameters found under path: $PARAM_PATH" + exit 1 + fi + + while IFS=$'\t' read -r name value; do + env_var="${name##*/}" + + echo "::add-mask::$value" + + { + echo "${env_var}<> "$GITHUB_ENV" + + echo "Exported: $env_var" + done <<< "$PARAMS" diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml new file mode 100644 index 0000000..07c9a22 --- /dev/null +++ b/.github/workflows/lint-pr-title.yml @@ -0,0 +1,35 @@ +name: Lint PR title + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: {} + +jobs: + lint: + name: Conventional commit + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + pull-requests: read + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Keep in sync with release-please-config.json changelog-sections. + types: | + feat + fix + perf + deps + revert + docs + chore + style + refactor + test + build + ci + requireScope: false diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..835bc1a --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,34 @@ +name: Release PR + +on: + push: + branches: [main] + +permissions: {} + +jobs: + release-please: + name: release-please + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + # release-please runs with an app token (not GITHUB_TOKEN) so the tag it + # pushes on release triggers the tag-gated release.yml workflow. + - name: Generate ContextBridge app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CB_PR_AUTOMATION_APP_ID }} + private-key: ${{ secrets.CB_PR_AUTOMATION_APP_PRIVATE_KEY }} + owner: contextbridge + repositories: patchwave-analysis + permission-contents: write + permission-pull-requests: write + + - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + target-branch: main + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3ab3a3e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: {} + +jobs: + test: + name: Tests + uses: ./.github/workflows/test.yml + permissions: + contents: read + + release: + name: GoReleaser + needs: test + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write # create/update the GitHub Release and upload artifacts + id-token: write # OIDC: assume the AWS release role for signing secrets + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: ./.github/actions/bootstrap + + - name: Fetch release secrets + uses: ./.github/actions/fetch-release-secrets + with: + role-to-assume: ${{ secrets.RELEASE_ROLE_ARN }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1 + with: + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_CURRENT_TAG: ${{ github.ref_name }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbcd39c..d5dde47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,10 @@ jobs: run: bun run lint - name: Typecheck run: bun run typecheck + - name: Shellcheck and test analyze.sh + run: | + shellcheck scripts/analyze.sh scripts/analyze.test.sh + sh scripts/analyze.test.sh test: name: Test diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..ee58c17 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,58 @@ +name: zizmor + +on: + pull_request: + push: + branches: [main] + +permissions: {} + +concurrency: + group: zizmor-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: read # zizmor reads workflow run metadata for online audits + security-events: write # upload SARIF to GitHub code scanning + steps: + # paths-filter needs a git checkout — it runs `git branch --show-current` + # before any API fallback, and fails outside a working tree. + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + # Audit only when something zizmor cares about changed. Keeps the job a + # required check that always reports a status, while skipping the + # API-heavy audit (and SARIF upload) on unrelated PRs. + - name: Detect workflow changes + id: changes + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + filters: | + github: + - '.github/**' + - 'zizmor.yml' + + - name: Install uv + if: steps.changes.outputs.github == 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + + - name: Run zizmor + if: steps.changes.outputs.github == 'true' + run: uvx zizmor --format=sarif .github/ > zizmor.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload zizmor SARIF to code scanning + if: steps.changes.outputs.github == 'true' + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + with: + sarif_file: zizmor.sarif + category: zizmor diff --git a/.gitignore b/.gitignore index e2eec53..582746b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules # output out dist +dist-release *.tgz # dev-only fixture for the report web app diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..c7f4d8a --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,64 @@ +version: 2 +project_name: patchwave-analysis + +# The before hook builds the embedded web report into dist/report-web/, so +# goreleaser can't also own dist/ — it requires its own output dir to be empty +# after hooks run. Keep goreleaser's artifacts separate. +dist: dist-release + +before: + hooks: + - bun install --frozen-lockfile + # The CLI embeds dist/report-web/index.html via a `with { type: 'text' }` + # import, so the web report must be built before the binary is compiled. + # `release --clean` wipes dist/ before these hooks run, so the order holds. + - bun run build:report-web + +builds: + - id: patchwave-analysis + builder: bun + main: ./src/index.ts + binary: patchwave-analysis + targets: + - bun-darwin-arm64 + - bun-darwin-x64 + - bun-linux-x64 + flags: + - --compile + +# Version-less archive names keep the GitHub `releases/latest/download/` +# URLs stable across releases, which is what scripts/analyze.sh fetches. +archives: + - formats: [tar.gz] + name_template: 'patchwave-analysis_{{ .Os }}_{{ .Arch }}' + +checksum: + name_template: checksums.txt + +notarize: + macos: + - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' + ids: + - patchwave-analysis + sign: + certificate: '{{ .Env.MACOS_SIGN_P12 }}' + password: '{{ .Env.MACOS_SIGN_PASSWORD }}' + entitlements: ./entitlements.plist + notarize: + issuer_id: '{{ .Env.MACOS_NOTARY_ISSUER_ID }}' + key_id: '{{ .Env.MACOS_NOTARY_KEY_ID }}' + key: '{{ .Env.MACOS_NOTARY_KEY }}' + wait: true + timeout: 20m + +# release-please owns the GitHub Release and its changelog body; goreleaser only +# attaches artifacts to the release release-please already created for the tag. +changelog: + disable: true + +release: + draft: false + prerelease: auto + mode: keep-existing + extra_files: + - glob: ./scripts/analyze.sh diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..b985ff6 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} diff --git a/README.md b/README.md index 67edbb5..8673d5a 100644 --- a/README.md +++ b/README.md @@ -17,22 +17,28 @@ Given an org or user, the report covers: ## Run it -You need a GitHub token with `repo` and `read:org` scopes. For CVE metrics, add `security_events`. +You need a GitHub token with `repo` and `read:org` scopes (add `security_events` for CVE metrics). The CLI resolves it from `GITHUB_TOKEN`, then `GH_TOKEN`, then `gh auth token` — so if you're signed in with the `gh` CLI there's nothing to set. -### With Bun installed +### One-off run (recommended) ```sh -bunx patchwave-analysis@latest +bash -c "$(curl -fsSL https://patchwave.ai/analyze.sh)" ``` -### With the gh CLI installed +This downloads the signed binary for your platform from the latest release, verifies its checksum, runs the interactive session, and cleans up after itself — nothing is installed. The report is written to your current directory. Pin a specific release with `PW_VERSION`: ```sh -bunx patchwave-analysis@latest -# auth is auto-resolved via `gh auth token` +PW_VERSION=v0.1.0 bash -c "$(curl -fsSL https://patchwave.ai/analyze.sh)" ``` -Token resolution order: `GITHUB_TOKEN` env var, then `GH_TOKEN` env var, then `gh auth token`. +### Download the binary yourself + +Grab the archive for your platform from the [latest release](https://github.com/contextbridge/patchwave-analysis/releases/latest), then: + +```sh +tar -xzf patchwave-analysis_darwin_arm64.tar.gz +./patchwave-analysis +``` ### From source @@ -41,7 +47,7 @@ git clone https://github.com/contextbridge/patchwave-analysis cd patchwave-analysis bun install bun run build:report-web -bun run src/index.ts +bun run src/index.ts ``` For local report UI development: diff --git a/entitlements.plist b/entitlements.plist new file mode 100644 index 0000000..fe2170f --- /dev/null +++ b/entitlements.plist @@ -0,0 +1,21 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..76393a4 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "node", + "package-name": "patchwave-analysis", + "include-component-in-tag": false, + "changelog-path": "CHANGELOG.md", + "bump-minor-pre-major": true, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "deps", "section": "Dependencies" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "refactor", "section": "Refactors", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true } + ] + } + } +} diff --git a/scripts/analyze.sh b/scripts/analyze.sh new file mode 100644 index 0000000..5044219 --- /dev/null +++ b/scripts/analyze.sh @@ -0,0 +1,143 @@ +#!/bin/sh +# +# analyze.sh — download and run patchwave-analysis as a one-off. +# +# Usage (interactive — it prompts you through everything, no flags needed): +# bash -c "$(curl -fsSL https://patchwave.ai/analyze.sh)" +# +# Downloads the patchwave-analysis binary for your platform from the latest +# GitHub release into a temp dir, verifies its checksum, runs it, then deletes +# it. The binary runs an interactive session and writes its report to your +# current directory. Nothing is installed. +# +# Use the `bash -c "$(curl ...)"` form rather than `curl ... | bash`: the +# command-substitution form leaves your terminal on stdin so the prompts work. +# As a fallback the script also reconnects the controlling terminal (/dev/tty) +# when running the binary, so a piped invocation still gets a TTY. +# +# Auth is the CLI's job: it reads GITHUB_TOKEN, then GH_TOKEN, then `gh auth +# token`. Export a token first, or be logged in via the gh CLI. +# +# Env vars: +# PW_VERSION pin to a release tag (e.g. v0.1.0) instead of the latest release + +set -eu + +REPO="contextbridge/patchwave-analysis" +BINARY_NAME="patchwave-analysis" + +main() { + _platform=$(detect_platform) + _os="${_platform% *}" + _arch="${_platform#* }" + + _asset="${BINARY_NAME}_${_os}_${_arch}.tar.gz" + _base_url=$(release_base_url) + + _tmp=$(mktemp -d 2>/dev/null || mktemp -d -t pw-analyze) + # shellcheck disable=SC2064 + trap "rm -rf '$_tmp'" EXIT INT TERM + + info "downloading ${_asset}..." + http_download "${_base_url}/${_asset}" "$_tmp/$_asset" \ + || fail "download failed: ${_base_url}/${_asset}" + + info "downloading checksums.txt..." + http_download "${_base_url}/checksums.txt" "$_tmp/checksums.txt" \ + || fail "checksum file download failed: ${_base_url}/checksums.txt" + + verify_checksum "$_tmp/$_asset" "$_asset" "$_tmp/checksums.txt" + + info "extracting..." + tar -xzf "$_tmp/$_asset" -C "$_tmp" || fail "tarball extraction failed" + [ -f "$_tmp/$BINARY_NAME" ] || fail "expected '$BINARY_NAME' in tarball, not found" + chmod +x "$_tmp/$BINARY_NAME" + + # The CLI is an interactive session, so it needs a terminal on stdin. + # Reconnect the controlling terminal (/dev/tty) so prompts work even when this + # script was piped into a shell (stdin = the pipe, not your terminal). Where + # there is no terminal (e.g. CI), run with inherited stdin and let the CLI + # report that it needs one. Run, don't exec, so the EXIT trap still deletes the + # temp binary; preserve the CLI's exit code for the caller. + info "starting ${BINARY_NAME}..." + set +e + if (: < /dev/tty) 2>/dev/null; then + "$_tmp/$BINARY_NAME" "$@" < /dev/tty + else + "$_tmp/$BINARY_NAME" "$@" + fi + _status=$? + set -e + exit "$_status" +} + +# Base URL for release assets: the latest release by default, or a pinned tag +# when PW_VERSION is set (with or without a leading `v`). +release_base_url() { + if [ -n "${PW_VERSION:-}" ]; then + _ver="$PW_VERSION" + case "$_ver" in v*) ;; *) _ver="v$_ver" ;; esac + printf 'https://github.com/%s/releases/download/%s\n' "$REPO" "$_ver" + else + printf 'https://github.com/%s/releases/latest/download\n' "$REPO" + fi +} + +detect_platform() { + _detected_os="" + _detected_arch="" + case "$(uname -s)" in + Darwin) _detected_os=darwin ;; + Linux) _detected_os=linux ;; + *) fail "unsupported OS: $(uname -s). patchwave-analysis supports macOS and Linux." ;; + esac + case "$(uname -m)" in + arm64 | aarch64) _detected_arch=arm64 ;; + x86_64 | amd64) _detected_arch=amd64 ;; + *) fail "unsupported architecture: $(uname -m). patchwave-analysis supports amd64 and arm64." ;; + esac + printf '%s %s\n' "$_detected_os" "$_detected_arch" +} + +http_download() { + if command -v curl >/dev/null 2>&1; then + curl --fail --silent --show-error --location --output "$2" "$1" + elif command -v wget >/dev/null 2>&1; then + wget --quiet -O "$2" "$1" + else + fail "neither curl nor wget is available" + fi +} + +verify_checksum() { + _file="$1" + _name="$2" + _checksums="$3" + _expected=$(awk -v n="$_name" '$2 == n || $2 == "*"n { print $1; exit }' "$_checksums") + [ -n "$_expected" ] || fail "could not find checksum for $_name in checksums.txt" + + if command -v sha256sum >/dev/null 2>&1; then + _actual=$(sha256sum "$_file" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + _actual=$(shasum -a 256 "$_file" | awk '{print $1}') + else + fail "neither sha256sum nor shasum is available for checksum verification" + fi + + [ "$_actual" = "$_expected" ] \ + || fail "checksum mismatch for $_name: expected $_expected, got $_actual" + info "checksum verified." +} + +# Diagnostics go to stderr so the CLI keeps stdout for its own output. +info() { printf '%s\n' "$1" >&2; } +fail() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} + +# Tests source this file with PW_ANALYZE_SH_LIB=1 to exercise helpers without +# running main. +if [ "${PW_ANALYZE_SH_LIB:-}" != "1" ]; then + main "$@" +fi diff --git a/scripts/analyze.test.sh b/scripts/analyze.test.sh new file mode 100644 index 0000000..265d59f --- /dev/null +++ b/scripts/analyze.test.sh @@ -0,0 +1,109 @@ +#!/bin/sh +# +# analyze.test.sh — plain-sh tests for analyze.sh helpers. + +set -u + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +export PW_ANALYZE_SH_LIB=1 +# shellcheck source=SCRIPTDIR/analyze.sh +# shellcheck disable=SC1091 +. "$script_dir/analyze.sh" + +pass=0 +fail=0 + +assert_eq() { + if [ "$1" = "$2" ]; then + pass=$((pass + 1)) + printf 'ok — %s\n' "$3" + else + fail=$((fail + 1)) + printf 'FAIL — %s\n expected: %s\n got: %s\n' "$3" "$2" "$1" + fi +} + +assert_exits_nonzero() { + if ( $1 ) >/dev/null 2>&1; then + fail=$((fail + 1)) + printf 'FAIL — %s (expected non-zero exit)\n' "$2" + else + pass=$((pass + 1)) + printf 'ok — %s\n' "$2" + fi +} + +# --- detect_platform --- + +# shellcheck disable=SC2317,SC2329 # uname redefinition called indirectly via assert_exits_nonzero $1 (SC2317 = shellcheck <0.10, SC2329 = >=0.10) +uname() { case "$1" in -s) echo Darwin ;; -m) echo arm64 ;; esac; } +assert_eq "$(detect_platform)" "darwin arm64" "darwin arm64" + +# shellcheck disable=SC2317,SC2329 +uname() { case "$1" in -s) echo Darwin ;; -m) echo x86_64 ;; esac; } +assert_eq "$(detect_platform)" "darwin amd64" "darwin x86_64 maps to amd64" + +# shellcheck disable=SC2317,SC2329 +uname() { case "$1" in -s) echo Linux ;; -m) echo x86_64 ;; esac; } +assert_eq "$(detect_platform)" "linux amd64" "linux x86_64 maps to amd64" + +# shellcheck disable=SC2317,SC2329 +uname() { case "$1" in -s) echo Linux ;; -m) echo aarch64 ;; esac; } +assert_eq "$(detect_platform)" "linux arm64" "linux aarch64 maps to arm64" + +# shellcheck disable=SC2317,SC2329 +uname() { case "$1" in -s) echo Linux ;; -m) echo i386 ;; esac; } +assert_exits_nonzero detect_platform "unsupported arch fails" + +# shellcheck disable=SC2317,SC2329 +uname() { case "$1" in -s) echo FreeBSD ;; -m) echo amd64 ;; esac; } +assert_exits_nonzero detect_platform "unsupported OS fails" + +unset -f uname + +# --- release_base_url --- +# PW_VERSION is read by release_base_url (sourced from analyze.sh); export so it +# reaches the command-substitution subshell and reads as used to shellcheck. + +export PW_VERSION="" +assert_eq "$(release_base_url)" \ + "https://github.com/contextbridge/patchwave-analysis/releases/latest/download" \ + "no PW_VERSION uses the latest release" + +PW_VERSION="v0.1.0" +assert_eq "$(release_base_url)" \ + "https://github.com/contextbridge/patchwave-analysis/releases/download/v0.1.0" \ + "PW_VERSION pins a tag" + +PW_VERSION="0.1.0" +assert_eq "$(release_base_url)" \ + "https://github.com/contextbridge/patchwave-analysis/releases/download/v0.1.0" \ + "PW_VERSION without leading v is normalized" + +# --- verify_checksum --- + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +printf 'hello\n' > "$tmp/sample.tar.gz" +expected=$(shasum -a 256 "$tmp/sample.tar.gz" 2>/dev/null | awk '{print $1}') +if [ -z "$expected" ]; then expected=$(sha256sum "$tmp/sample.tar.gz" | awk '{print $1}'); fi +printf '%s sample.tar.gz\nabc other.tar.gz\n' "$expected" > "$tmp/checksums.txt" + +if verify_checksum "$tmp/sample.tar.gz" "sample.tar.gz" "$tmp/checksums.txt" >/dev/null 2>&1; then + pass=$((pass + 1)); printf 'ok — verify_checksum accepts correct hash\n' +else + fail=$((fail + 1)); printf 'FAIL — verify_checksum rejected correct hash\n' +fi + +printf 'deadbeef sample.tar.gz\n' > "$tmp/bad.txt" +if ( verify_checksum "$tmp/sample.tar.gz" "sample.tar.gz" "$tmp/bad.txt" ) >/dev/null 2>&1; then + fail=$((fail + 1)); printf 'FAIL — verify_checksum accepted wrong hash\n' +else + pass=$((pass + 1)); printf 'ok — verify_checksum rejects wrong hash\n' +fi + +# --- summary --- + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..550e33a --- /dev/null +++ b/zizmor.yml @@ -0,0 +1 @@ +rules: {}