Skip to content
Merged
Show file tree
Hide file tree
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
942 changes: 54 additions & 888 deletions .github/workflows/governance-reusable.yml

Large diffs are not rendered by default.

24 changes: 3 additions & 21 deletions .github/workflows/governance.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
# SPDX-License-Identifier: MPL-2.0
# governance.yml — single wrapper calling the shared estate governance bundle
# in hyperpolymath/standards instead of carrying per-repo copies.
#
# Replaces the per-repo governance scaffolding removed in the same commit:
# quality.yml, guix-nix-policy.yml, npm-bun-blocker.yml, ts-blocker.yml,
# security-policy.yml, rsr-antipattern.yml, wellknown-enforcement.yml,
# workflow-linter.yml
#
# Load-bearing build/security workflows stay standalone in the repo
# (rust-ci, codeql, dependabot, release, scan/mirror/pages plumbing).

# SPDX-License-Identifier: PMPL-1.0-or-later
name: Governance

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
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

permissions:
contents: read

jobs:
governance:
uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@861b5e911d9e5dcfb3c0ab3dd2a9a3c8fd0a1613
uses: ./.github/workflows/governance-reusable.yml
629 changes: 46 additions & 583 deletions .github/workflows/hypatia-scan-reusable.yml

Large diffs are not rendered by default.

410 changes: 6 additions & 404 deletions .github/workflows/hypatia-scan.yml

Large diffs are not rendered by default.

81 changes: 5 additions & 76 deletions .github/workflows/scorecard-enforcer.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: MPL-2.0
# SPDX-License-Identifier: PMPL-1.0-or-later
# Prevention workflow - runs OpenSSF Scorecard and fails on low scores
name: OpenSSF Scorecard Enforcer

Expand All @@ -9,99 +9,30 @@ on:
- cron: '0 6 * * 1' # Weekly on Monday
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

permissions:
contents: read

jobs:
# The OSSF Scorecard publish endpoint enforces a hard contract: the job that
# runs `ossf/scorecard-action` with `publish_results: true` must contain
# ONLY steps with `uses:` (no `run:` steps in the same job). If a `run:`
# step is present, the publish step fails with:
# "webapp: scorecard job must only have steps with uses"
# (49 estate repos hit this; see ROADMAP audit 2026-05-30.)
#
# Fix: split the threshold check into a downstream job that depends on
# `scorecard` and consumes the SARIF artifact. The `scorecard` job stays
# uses-only; `check-score` is the gating job that emits the error.
scorecard:
timeout-minutes: 20
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write # For OIDC
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Run Scorecard
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- name: Upload SARIF
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
sarif_file: results.sarif

# Compute the aggregate score in its OWN uses-only job. The score is NOT in
# the SARIF output (`jq '.runs[0].tool.driver.properties.score'` always
# returned null → 0 → this gate failed on every push regardless of the real
# posture); it only exists in scorecard's JSON output. scorecard-action and a
# `run:` step must never share a job (OSSF publish contract — see #304, and
# hypatia `scorecard_publish_with_run_step`), so this job stays uses-only and
# hands the JSON to check-score via an artifact. `publish_results: false`
# means this run neither publishes nor needs OIDC (the `scorecard` job above
# owns publishing).
compute-score:
timeout-minutes: 20
needs: scorecard
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Compute Scorecard score (JSON)
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.json
results_format: json
publish_results: false

- name: Persist score JSON for the gate job
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: scorecard-score-json
path: results.json
retention-days: 1

check-score:
timeout-minutes: 10
needs: compute-score
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Download score JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5.0.0
with:
name: scorecard-score-json
publish_results: true

- name: Check minimum score
run: |
# Parse score from results.json
SCORE=$(jq -r '.score // 0' results.json 2>/dev/null || echo "0")

echo "OpenSSF Scorecard Score: $SCORE"
Expand All @@ -114,12 +45,10 @@ jobs:
exit 1
fi

# Check specific high-priority items
check-critical:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Check SECURITY.md exists
run: |
Expand Down
110 changes: 17 additions & 93 deletions .github/workflows/scorecard-reusable.yml
Original file line number Diff line number Diff line change
@@ -1,111 +1,35 @@
# SPDX-License-Identifier: MPL-2.0
# scorecard-reusable.yml — Reusable OSSF Scorecard analysis.
#
# Consolidates the per-repo `scorecard.yml` workflow (estate-wide:
# 258 top-level deployments, 46 unique blob SHAs, 17.8% structural
# drift — top SHA covers 100/258 (38.8%), top 7 SHAs cover 80%).
# Sampled top + long-tail variants are all the same single-`analysis`-
# job shape (41 lines canonical); 100% of drift is mechanical:
# SPDX-header lag (PMPL-1.0 / PMPL-1.0-or-later / MPL-2.0),
# `github/codeql-action/upload-sarif` SHA-pin drift, and
# `permissions: read-all` vs `permissions: contents: read` wording.
# Zero feature variance across all 46 SHAs.
#
# Plus 626 nested copies in monorepos (asdf-tool-plugins, developer-
# ecosystem, ssg-collection, standards, ambientops, julia-ecosystem,
# etc.). Per Layer-3 caveat (see scripts/sweep-classifiers/README.adoc),
# nested workflows are inert — GitHub Actions only runs the repo-root
# `.github/workflows/` directory. Sweeping nested copies is
# single-source-of-truth cleanup, not security hardening.
#
# Design:
# - One input only: `runs-on` (default ubuntu-latest).
# - No `secrets: inherit` needed — scorecard uses `GITHUB_TOKEN`
# directly, which is available without inherit.
# - Caller MUST grant `security-events: write` and `id-token: write`
# on the calling job. The reusable re-asserts these on its own
# `analysis` job, but called-workflow permissions are CAPPED by
# the caller's permissions block.
# - Caller keeps its own `on:` triggers and `concurrency:` group, so
# the read-only cancel-superseded behaviour stays in the wrapper.
#
# Caller example (wrapper):
# # SPDX-License-Identifier: MPL-2.0
# name: OSSF Scorecard
# on:
# push:
# branches: [main, master]
# schedule:
# - cron: '23 4 * * 1' # Weekly (Monday 04:23 UTC) — match canonical
# workflow_dispatch:
# concurrency:
# group: ${{ github.workflow }}-${{ github.ref }}
# cancel-in-progress: true
# permissions:
# contents: read
# jobs:
# scorecard:
# permissions:
# security-events: write
# id-token: write
# uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@<sha>
#
# CANONICAL SCHEDULE — WEEKLY, NOT DAILY (2026-05-28).
# Estate audit found 180 repos running daily at 04:00 UTC ('0 4 * * *')
# vs 29 on canonical weekly ('23 4 * * 1') — drift driven by an older
# version of the example above. Downstream thin-caller wrappers should
# keep the weekly cadence shown above.
#
# NOTE (2026-06-04): the standards repo itself no longer ships a thin
# `scorecard.yml` caller — it was retired in #372 as a redundant second
# scorecard run. Standards runs OSSF Scorecard directly via
# `scorecard-enforcer.yml` (weekly, Monday 06:00 UTC; publishes + gates
# on MIN_SCORE). This reusable is UNCHANGED and downstream callers are
# unaffected — they remain the canonical thin-caller pattern.
#
# GH Actions budget impact of the drift: 180 daily × (365 − 52) ≈ 56k
# extra runs/year × ~1.5 min/run ≈ ~84k Actions-minutes/year. Fan-out
# to convert the 180 estate repos from daily→weekly is tracked
# separately (GH-Actions budget rebalancing 2026-05-27).
#
# When to deviate: a repo with a very high merge/push cadence AND
# scorecard-sensitive supply-chain churn (heavy dependency refresh)
# MAY opt into more frequent runs. Daily is rarely worth it; the
# scorecard signal moves on the order of weeks, not hours.

name: Scorecard (reusable)
# SPDX-License-Identifier: PMPL-1.0-or-later
name: OSSF Scorecard Reusable Workflow

on:
workflow_call:
inputs:
runs-on:
description: Runner label
type: string
required: false
default: ubuntu-latest

permissions:
contents: read

jobs:
analysis:
timeout-minutes: 20
runs-on: ${{ inputs.runs-on }}
scorecard:
name: Run Scorecard
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Run Scorecard
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.3.1
- name: Run Scorecard Analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
results_file: results.json
results_format: json
publish_results: true

- name: Upload results
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3.31.8
- name: Upload results artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
sarif_file: results.sarif
name: scorecard-results
path: results.json
retention-days: 90
100 changes: 100 additions & 0 deletions scripts/check-workflow-staleness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/bin/bash
# SPDX-License-Identifier: PMPL-1.0-or-later
set -eo pipefail

# Staleness checker script for hyperpolymath estate repositories.
# Ensures that workflows do not use retired patterns or stale pins.

REPO_ROOT="${1:-.}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Determine if we are in standards repo
IS_STANDARDS=false
if [ "$GITHUB_REPOSITORY" = "hyperpolymath/standards" ]; then
IS_STANDARDS=true
else
# Fallback: check Git remote origin URL
if [ -d "$REPO_ROOT/.git" ]; then
REMOTE_URL=$(git -C "$REPO_ROOT" remote get-url origin 2>/dev/null || echo "")
if [[ "$REMOTE_URL" =~ "hyperpolymath/standards" ]]; then
IS_STANDARDS=true
fi
fi
fi

# Determine current approved standards SHA dynamically, or allow override from environment
CURRENT_SHA="${STALENESS_EXPECTED_SHA:-}"
if [ -z "$CURRENT_SHA" ]; then
# Look up the Git commit of the standards repo containing this script
if [ -d "$SCRIPT_DIR/../.git" ]; then
CURRENT_SHA=$(git -C "$SCRIPT_DIR/.." rev-parse HEAD 2>/dev/null || echo "")
elif [ -d "$SCRIPT_DIR/.git" ]; then
CURRENT_SHA=$(git -C "$SCRIPT_DIR" rev-parse HEAD 2>/dev/null || echo "")
fi
fi

if [ -z "$CURRENT_SHA" ]; then
echo "::error::Could not determine current standards SHA. Set STALENESS_EXPECTED_SHA."
exit 1
fi

echo "Staleness Check against Standards SHA: $CURRENT_SHA"

# If no root .github/workflows exists, pass.
if [ ! -d "$REPO_ROOT/.github/workflows" ]; then
echo "No .github/workflows directory found. Passing."
exit 0
fi

FAILED=0

# Rule: no_retired_scorecard_enforcer
if [ "$IS_STANDARDS" = "false" ] && [ -f "$REPO_ROOT/.github/workflows/scorecard-enforcer.yml" ]; then
echo "::error::scorecard-enforcer.yml is retired. Use scorecard.yml -> standards scorecard-reusable.yml instead."
FAILED=1
fi

for wf in "$REPO_ROOT"/.github/workflows/*.yml "$REPO_ROOT"/.github/workflows/*.yaml; do
[ -f "$wf" ] || continue

# Rule: no_scorecard_sarif_code_scanning
if grep -q "ossf/scorecard-action@" "$wf" && grep -q "github/codeql-action/upload-sarif@" "$wf"; then
echo "::error file=$wf::OSSF Scorecard must not upload SARIF to GitHub Code Scanning unless it runs for every PR head commit."
FAILED=1
fi

# Rule: no_stale_scorecard_reusable_pin
if grep -q "scorecard-reusable.yml@" "$wf"; then
PINNED_SHA=$(grep "scorecard-reusable.yml@" "$wf" | sed -E 's/.*scorecard-reusable.yml@([^ #\r\n]+).*/\1/')
if [ "$PINNED_SHA" != "$CURRENT_SHA" ]; then
echo "::error file=$wf::Workflow pins stale Scorecard reusable that publishes SARIF / causes Code Scanning waits. Refresh to current standards SHA."
FAILED=1
fi
fi

# Rule: no_stale_hypatia_reusable_pin
if grep -q "hypatia-scan-reusable.yml@" "$wf"; then
PINNED_SHA=$(grep "hypatia-scan-reusable.yml@" "$wf" | sed -E 's/.*hypatia-scan-reusable.yml@([^ #\r\n]+).*/\1/')
if [ "$PINNED_SHA" != "$CURRENT_SHA" ]; then
echo "::error file=$wf::Workflow pins Hypatia reusable before cache/baseline-delay fix. Refresh to current standards SHA."
FAILED=1
fi
fi

# Rule: estate_pin_freshness for governance.yml
if grep -q "governance-reusable.yml@" "$wf"; then
PINNED_SHA=$(grep "governance-reusable.yml@" "$wf" | sed -E 's/.*governance-reusable.yml@([^ #\r\n]+).*/\1/')
if [ "$PINNED_SHA" != "$CURRENT_SHA" ]; then
echo "::error file=$wf::Workflow pins stale governance reusable. Refresh to current standards SHA."
FAILED=1
fi
fi
done

if [ $FAILED -ne 0 ]; then
echo "::error::Remove legacy scorecard-enforcer.yml, refresh standards reusable pins, and keep Scorecard out of GitHub Code Scanning unless it runs for every PR head commit."
exit 1
fi

echo "All workflow staleness checks passed."
exit 0
Loading
Loading