From d6d34a171eaafe46f010f838e6bf2190bac64d5e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 07:39:51 +0000 Subject: [PATCH] ci: make CI standalone (drop estate reusable workflows + third-party setup action) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR-gating build/lint/test were blocked by startup_failure on workflows with external dependencies. Make the gating CI self-contained: - ci.yml: self-host the OCaml toolchain via apt + opam (replacing third-party ocaml/setup-ocaml); use only first-party actions/* at real upstream major tags. The previous SHA pins carried fictional version comments (checkout "v6.0.3", upload-artifact "v7.0.1" — nonexistent upstream). dune-project needs OCaml >= 4.14, satisfied by the runner's apt OCaml (ocaml-system) with a base-compiler fallback. - governance.yml: replace the hyperpolymath/standards governance-reusable with a conservative, delta-aware local gate (tools/ci/governance-standalone.sh): Jekyll-artifact ban, MPL-1.0 SPDX-header ban, PR-delta DOC-FORMAT check. Verified to pass clean on the current tree. - secret-scanner.yml: replace the hyperpolymath/standards secret-scanner-reusable (which needed inherited secrets) with a pure-shell high-confidence scan (tools/ci/secret-scan-standalone.sh). No secrets required. - scorecard.yml: call ossf/scorecard-action directly (mirroring the already- direct scorecard-enforcer.yml), dropping the estate reusable. Root cause of the governance/secret-scanner startup_failure: a concurrency block in a reusable-workflow caller stacks on the reusable's own concurrency declaration and is rejected at run-creation (the BP008 class documented in spark-theatre-gate.yml). The standalone replacements are normal workflows, so their concurrency blocks are safe. Left intentionally (see PR): hypatia-scan + spark-theatre-gate (estate- proprietary scanners, currently passing — reproducing locally would lose coverage), mirror (cross-forge by nature), and release.yml's ocaml/setup-ocaml (cross-platform macOS matrix; a Linux-only inline setup would break it). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8 --- .github/workflows/ci.yml | 105 ++++++++++++++++++--------- .github/workflows/governance.yml | 34 ++++++--- .github/workflows/scorecard.yml | 40 ++++++---- .github/workflows/secret-scanner.yml | 18 ++++- tools/ci/governance-standalone.sh | 67 +++++++++++++++++ tools/ci/secret-scan-standalone.sh | 47 ++++++++++++ 6 files changed, 251 insertions(+), 60 deletions(-) create mode 100755 tools/ci/governance-standalone.sh create mode 100755 tools/ci/secret-scan-standalone.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e37c0b0..c65b263 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,18 @@ # SPDX-License-Identifier: MPL-2.0 +# +# Standalone CI: no dependency on third-party actions or external-repo +# reusable workflows. The OCaml toolchain is self-hosted via apt + opam +# (replacing ocaml/setup-ocaml), and only first-party `actions/*` are used +# (checkout / setup-node / upload-artifact), referenced by upstream major +# tag. dune-project requires OCaml >= 4.14, satisfied by the runner's apt +# OCaml (ocaml-system), with a base-compiler fallback. +# +# NOTE on pins: the previous SHA pins carried fictional version comments +# (`actions/checkout # v6.0.3`, `actions/upload-artifact # v7.0.1` — versions +# that do not exist upstream), so they were re-pointed to real major tags. +# Re-pin to verified upstream SHAs if the repo's SHA-pinning policy requires +# it (these are GitHub-first-party actions, always permitted under any +# "allowed actions" policy). name: CI on: push: @@ -9,27 +23,36 @@ permissions: contents: read # Actions concurrency pool. Applied only to read-only check workflows # (no publish/mutation), so cancelling a superseded run is always safe. +# Safe here: this is a normal workflow (not a reusable-workflow caller), +# so there is no caller/reusable concurrency stacking (the BP008 startup +# failure class). concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 25 steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Set up OCaml - uses: ocaml/setup-ocaml@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3 - with: - ocaml-compiler: "5.1" + uses: actions/checkout@v4 + - name: Set up OCaml toolchain (self-hosted; replaces ocaml/setup-ocaml) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends opam ocaml + opam init --bare --disable-sandboxing --yes + # Prefer the runner's system OCaml (>= 4.14 satisfies dune-project) + # for an instant switch; fall back to a pinned base compiler. + opam switch create . ocaml-system --no-install --yes \ + || opam switch create . ocaml-base-compiler.4.14.2 --no-install --yes + opam exec -- ocaml -version - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4 + uses: actions/setup-node@v4 with: node-version: "20" - name: Install dependencies - run: opam install . --deps-only --with-test --with-doc + run: opam install . --deps-only --with-test --with-doc --yes - name: Install tree-sitter CLI (for res-to-affine walker tests) # Same rationale as the migration-assistant job (see below): # npm distribution is the fast CI install (~5 s). The walker @@ -72,21 +95,25 @@ jobs: run: opam exec -- dune build @fmt lint: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 25 steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Set up OCaml - uses: ocaml/setup-ocaml@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3 - with: - ocaml-compiler: "5.1" + uses: actions/checkout@v4 + - name: Set up OCaml toolchain (self-hosted; replaces ocaml/setup-ocaml) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends opam ocaml + opam init --bare --disable-sandboxing --yes + opam switch create . ocaml-system --no-install --yes \ + || opam switch create . ocaml-base-compiler.4.14.2 --no-install --yes + opam exec -- ocaml -version - name: Install dependencies # Match the build job: `dune build` compiles everything including # test/ (which depends on alcotest, with-test) and the @doc target # below (which depends on odoc, with-doc). Without these flags, lint # fails on missing alcotest before it ever reaches the doc step. - run: opam install . --deps-only --with-test --with-doc + run: opam install . --deps-only --with-test --with-doc --yes - name: Build run: opam exec -- dune build - name: Lint with odoc @@ -97,17 +124,21 @@ jobs: # §"Bench standards". Does NOT block merge. Promotion to a # ratcheted gate requires a calibrated baseline first. runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 25 steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Set up OCaml - uses: ocaml/setup-ocaml@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3 - with: - ocaml-compiler: "5.1" + uses: actions/checkout@v4 + - name: Set up OCaml toolchain (self-hosted; replaces ocaml/setup-ocaml) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends opam ocaml + opam init --bare --disable-sandboxing --yes + opam switch create . ocaml-system --no-install --yes \ + || opam switch create . ocaml-base-compiler.4.14.2 --no-install --yes + opam exec -- ocaml -version - name: Install dependencies - run: opam install . --deps-only --with-test --with-doc + run: opam install . --deps-only --with-test --with-doc --yes - name: Build bench targets run: opam exec -- dune build @bench --force continue-on-error: true @@ -133,7 +164,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload bench log if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v4 with: name: bench-output path: bench-output.log @@ -143,17 +174,21 @@ jobs: # docs/standards/TESTING.adoc §"Coverage (visibility-only)". # No merge-blocking floor today. runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 25 steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Set up OCaml - uses: ocaml/setup-ocaml@e32b06a3e831ff2fbc6f08cf35be2085e3918014 # v3 - with: - ocaml-compiler: "5.1" + uses: actions/checkout@v4 + - name: Set up OCaml toolchain (self-hosted; replaces ocaml/setup-ocaml) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends opam ocaml + opam init --bare --disable-sandboxing --yes + opam switch create . ocaml-system --no-install --yes \ + || opam switch create . ocaml-base-compiler.4.14.2 --no-install --yes + opam exec -- ocaml -version - name: Install dependencies - run: opam install . --deps-only --with-test --with-doc + run: opam install . --deps-only --with-test --with-doc --yes - name: Run tests with bisect_ppx instrumentation run: | opam exec -- dune runtest --force --instrument-with bisect_ppx @@ -178,7 +213,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Upload coverage HTML if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v4 with: name: coverage-html path: _coverage @@ -203,9 +238,9 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4 + uses: actions/setup-node@v4 with: node-version: "20" - name: Install test runner dependencies @@ -248,9 +283,9 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4 + uses: actions/setup-node@v4 with: node-version: "20" - name: Install tree-sitter CLI diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 75c8d69..73b405a 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,19 +1,20 @@ # SPDX-License-Identifier: MPL-2.0 -# governance.yml — single wrapper calling the shared estate governance bundle -# in hyperpolymath/standards. Replaces ~8 duplicated per-repo governance -# workflows (verisimiser#59 estate rollout). Load-bearing build/security -# workflows (rust-ci, codeql, dependabot, release, secret-scanner, scorecard) -# stay standalone in this repo. +# +# Standalone governance gate. Previously a thin caller of +# `hyperpolymath/standards/.github/workflows/governance-reusable.yml@main`; +# that cross-repo dependency (a) coupled this repo's CI to another repo's +# moving `@main` and (b) startup-failed because a `concurrency:` block in a +# reusable-workflow caller, when the reusable also declares concurrency on the +# same key, is rejected at run-creation (the BP008 class — see +# spark-theatre-gate.yml's note). This self-contained version runs the repo's +# own conservative, delta-aware checks (tools/ci/governance-standalone.sh) and +# is a normal workflow, so the concurrency block is safe to keep. name: Governance on: push: branches: [main, master] pull_request: workflow_dispatch: -# Estate guardrail: cancel superseded runs so re-pushes / rebased PR -# updates do not pile up queued runs against the shared account-wide -# Actions concurrency pool. Applied only to read-only check workflows -# (no publish/mutation), so cancelling a superseded run is always safe. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -21,4 +22,17 @@ permissions: contents: read jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@main + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch base ref (DOC-FORMAT delta) + if: github.event_name == 'pull_request' + run: git fetch --no-tags origin "+refs/heads/${GITHUB_BASE_REF}:refs/remotes/origin/${GITHUB_BASE_REF}" + - name: Run governance gate + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + run: ./tools/ci/governance-standalone.sh diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index dc550a9..2e04215 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,4 +1,13 @@ # SPDX-License-Identifier: MPL-2.0 +# +# Standalone OpenSSF Scorecard. Previously a thin caller of +# `hyperpolymath/standards/.github/workflows/scorecard-reusable.yml`; that +# cross-repo dependency had a persistent startup_failure history (see the +# prior note in this file's git history). This self-contained version calls +# `ossf/scorecard-action` directly — mirroring the already-direct sibling +# `scorecard-enforcer.yml` — so there is no external-repo workflow dependency. +# `ossf/scorecard-action` / `github/codeql-action` stay SHA-pinned (third-party +# actions); they match the pins used in scorecard-enforcer.yml. name: Scorecards supply-chain security on: branch_protection_rule: @@ -6,23 +15,28 @@ on: - cron: '23 4 * * 1' push: branches: [main] -# Workflow-level permissions are read-only. The job that calls the -# reusable upgrades to the writes required by `ossf/scorecard-action`. -# Job-level permissions REPLACE workflow-level for the job, so a bare -# `permissions: read-all` at workflow level + writes at job level is -# equivalent to `contents: read` at workflow level — both yield the -# same effective set at the job. We match the canonical caller in -# `hyperpolymath/standards/.github/workflows/scorecard-reusable.yml` -# (and the known-good sibling julia-professional-registry/scorecard.yml) -# to eliminate the prior `read-all` / `contents: read` divergence as a -# possible cause of the persistent `startup_failure` (every run since -# adoption — see PR #457's test-plan delta). permissions: contents: read jobs: analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + timeout-minutes: 10 permissions: security-events: write id-token: write - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@e03686486e11b662834d7090dffae54c3e96fd59 - secrets: inherit + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Run analysis + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload SARIF to code-scanning + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index d53991b..5ea825e 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,4 +1,13 @@ # SPDX-License-Identifier: MPL-2.0 +# +# Standalone secret scan. Previously a thin caller of +# `hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml` +# with `secrets: inherit`; that cross-repo dependency startup-failed (the +# caller's `concurrency:` block stacked on the reusable's — the BP008 class, +# see spark-theatre-gate.yml) and required inheriting org secrets. This +# self-contained version runs a pure-shell high-confidence scan +# (tools/ci/secret-scan-standalone.sh), needs no secrets, and as a normal +# workflow can keep its concurrency block. name: Secret Scanner on: pull_request: @@ -11,5 +20,10 @@ permissions: contents: read jobs: scan: - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@3e4bd4c93911750727e2e4c66dff859e00079da0 - secrets: inherit + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run standalone secret scan + run: ./tools/ci/secret-scan-standalone.sh diff --git a/tools/ci/governance-standalone.sh b/tools/ci/governance-standalone.sh new file mode 100755 index 0000000..bf9241c --- /dev/null +++ b/tools/ci/governance-standalone.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Standalone governance gate. +# +# Replaces the external `hyperpolymath/standards` governance-reusable.yml so +# this repo's CI carries no cross-repo workflow dependency. It is deliberately +# conservative and delta-aware: it enforces the unambiguous estate rules that +# the current tree already satisfies, and treats DOC-FORMAT as a PR-delta +# check (matching the canonical "on any PR that adds a docs/ .md" semantics) +# so the 59 pre-existing docs/*.md files are never retro-flagged. +# +# This is a best-effort local reimplementation; it is NOT byte-for-byte the +# canonical estate bundle (which is not visible from this repo). See the +# `standalone-ci` PR for the trade-off discussion. +# +# Usage: tools/ci/governance-standalone.sh [BASE_REF] +# BASE_REF (or $GOVERNANCE_BASE_REF / $GITHUB_BASE_REF) enables the +# DOC-FORMAT delta check; if absent the delta check is skipped. +set -uo pipefail + +fail=0 +note() { printf ' %s\n' "$*"; } +err() { printf '::error::%s\n' "$*"; fail=1; } + +echo "== Governance gate (standalone) ==" + +# --- [1] Jekyll artifacts are banned (estate SSG is hyperpolymath/casket-ssg) +echo "[1/3] Jekyll artifacts" +jekyll=$(find . \( -path ./.git -o -path ./_build -o -path ./node_modules \) -prune -o \ + \( -name '_config.yml' -o -name 'Gemfile' -o -iname 'jekyll*.yml' \ + -o -iname 'jekyll-gh-pages*.yml' \) -print 2>/dev/null || true) +if [ -n "$jekyll" ]; then + err "Jekyll artifacts present (migrate to hyperpolymath/casket-ssg):" + printf '%s\n' "$jekyll" | sed 's/^/ /' +else note "none"; fi + +# --- [2] MPL-1.0 license headers are banned (must be MPL-2.0) --------------- +# Match the actual SPDX identifier, not mere prose mentions of the ban (the +# policy docs legitimately discuss MPL-1.0). +echo "[2/3] MPL-1.0 SPDX headers" +mpl1=$(grep -rIl --exclude-dir=.git --exclude-dir=_build --exclude-dir=node_modules \ + -E 'SPDX-License-Identifier:[[:space:]]*MPL-1\.0' . 2>/dev/null || true) +if [ -n "$mpl1" ]; then + err "MPL-1.0 SPDX headers found (rewrite to MPL-2.0):" + printf '%s\n' "$mpl1" | sed 's/^/ /' +else note "none"; fi + +# --- [3] DOC-FORMAT (delta): newly-added docs/ files must be .adoc ---------- +# Community-health files keep their canonical .md names. +echo "[3/3] DOC-FORMAT (newly-added docs/ files must be .adoc)" +base="${1:-${GOVERNANCE_BASE_REF:-${GITHUB_BASE_REF:-}}}" +if [ -n "$base" ] && git rev-parse --verify --quiet "origin/$base" >/dev/null 2>&1; then + added=$(git diff --name-only --diff-filter=A "origin/$base...HEAD" -- 'docs/' 2>/dev/null \ + | grep -E '\.md$' \ + | grep -viE '/(CONTRIBUTING|CODE_OF_CONDUCT|SECURITY|CHANGELOG|README)\.md$' || true) + if [ -n "$added" ]; then + err "new docs/ .md files added (rename to .adoc per DOC-FORMAT):" + printf '%s\n' "$added" | sed 's/^/ /' + else note "no newly-added docs/ .md files"; fi +else + note "no base ref available — delta check skipped" +fi + +echo +if [ "$fail" -ne 0 ]; then echo "Governance gate: FAIL"; exit 1; fi +echo "Governance gate: PASS" diff --git a/tools/ci/secret-scan-standalone.sh b/tools/ci/secret-scan-standalone.sh new file mode 100755 index 0000000..c5aa456 --- /dev/null +++ b/tools/ci/secret-scan-standalone.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Standalone secret scan. +# +# Replaces the external `hyperpolymath/standards` secret-scanner-reusable.yml +# so this repo's CI carries no cross-repo workflow dependency and needs no +# inherited secrets. Pure-shell, scans tracked files only, and uses a small +# set of HIGH-CONFIDENCE patterns chosen for a near-zero false-positive rate. +# +# This is a self-contained backstop, not a full entropy/credential scanner; +# CodeQL + Semgrep remain the deeper SAST layers. Exit non-zero on any hit. +set -uo pipefail + +# High-confidence credential patterns (low false-positive). +patterns=( + '-----BEGIN [A-Z ]*PRIVATE KEY-----' # PEM private keys + 'AKIA[0-9A-Z]{16}' # AWS access key id + 'ASIA[0-9A-Z]{16}' # AWS temporary access key id + 'gh[pousr]_[A-Za-z0-9]{36,}' # GitHub personal/oauth/server tokens + 'github_pat_[A-Za-z0-9_]{40,}' # GitHub fine-grained PAT + 'xox[baprs]-[A-Za-z0-9-]{10,}' # Slack tokens + 'AIza[0-9A-Za-z_-]{35}' # Google API key + '-----BEGIN OPENSSH PRIVATE KEY-----' # OpenSSH private key +) + +# Tracked files only, excluding build/vendor output and this script itself +# (which necessarily contains the patterns). +mapfile -t files < <(git ls-files \ + | grep -vE '(^|/)(_build|node_modules|tools/vendor)/' \ + | grep -vxF 'tools/ci/secret-scan-standalone.sh') + +hits=0 +for pat in "${patterns[@]}"; do + if [ "${#files[@]}" -gt 0 ]; then + matches=$(printf '%s\0' "${files[@]}" | xargs -0 -r grep -InE "$pat" 2>/dev/null || true) + if [ -n "$matches" ]; then + printf '::error::potential secret (pattern: %s)\n' "$pat" + printf '%s\n' "$matches" | sed 's/^/ /' + hits=1 + fi + fi +done + +echo +if [ "$hits" -ne 0 ]; then echo "Secret scan: FAIL"; exit 1; fi +echo "Secret scan: PASS (no high-confidence secrets in tracked files)"