From ebc6d237683810e977cf44959e370c0ed78fee25 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:47:41 +0100 Subject: [PATCH 1/2] fix(ci): re-fix the scalar-permissions parse-kill, and guard it from recurring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #68 ("repoint codeql-action at a SHA that exists") re-ran the sweep that appends `actions: read` after every `permissions:` line, undoing the fix landed in #64. Four workflows are parse-dead on main RIGHT NOW: zig.yml e2e.yml container-build.yml install-smoke.yml i.e. the Zig FFI gate, the Julia test suite, the container build and the install smoke test. All four declare permissions in the scalar form, so the appended key is invalid YAML: permissions: read-all actions: read # mapping values are not allowed here Actions rejects a malformed workflow at PARSE time — zero jobs, NO check run. Not a red X, an absence. The only visible tell is `gh run list` printing the workflow's PATH instead of its NAME, which is exactly how this was spotted. Same cause both times: the sweep matches `permissions:` as TEXT, not as a YAML node, so it cannot tell the scalar form from the mapping form. The insertion is redundant anyway — `read-all` already grants every read scope, `actions: read` included. Fixing it a second time without a guard would just queue up a third. So this also adds `.githooks/validate-workflow-yaml.sh`, wired as the first job of the dogfood gate: parse every workflow, fail on any that does not parse OR that declares no jobs (a workflow can be valid YAML and still be inert). The guard is proven fail-closed, not assumed: A. clean tree -> exit 0, "all 19 workflows parse" B. `actions: read` reintroduced -> exit 1, names zig.yml and the reason C. restored -> exit 0 A text-only sweep cannot satisfy this check, which is the point. Co-Authored-By: Claude Opus 4.8 --- .githooks/validate-workflow-yaml.sh | 79 +++++++++++++++++++++++++++ .github/workflows/container-build.yml | 1 - .github/workflows/dogfood-gate.yml | 18 ++++++ .github/workflows/e2e.yml | 1 - .github/workflows/install-smoke.yml | 1 - .github/workflows/zig.yml | 1 - 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100755 .githooks/validate-workflow-yaml.sh diff --git a/.githooks/validate-workflow-yaml.sh b/.githooks/validate-workflow-yaml.sh new file mode 100755 index 0000000..9d760fc --- /dev/null +++ b/.githooks/validate-workflow-yaml.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# validate-workflow-yaml.sh — every .github/workflows/*.yml must PARSE. +# +# WHY THIS EXISTS +# --------------- +# A malformed workflow is not a red check. GitHub Actions rejects it at PARSE +# time, which produces zero jobs and NO CHECK RUN AT ALL — the gate silently +# ceases to exist while the board still reads green. `gh pr checks` shows +# nothing wrong. The only visible tell is that `gh run list` starts printing the +# workflow's PATH instead of its NAME. +# +# This repo has hit that twice in one day, from the same cause both times: a +# sweep that appends `actions: read` after every `permissions:` line. Four +# workflows declare permissions in the SCALAR form — +# +# permissions: read-all +# +# — and hanging a mapping key under a scalar is invalid YAML: +# +# permissions: read-all +# actions: read # "mapping values are not allowed here" +# +# It was fixed in PR #64 and reintroduced by PR #68, because the sweep matched +# `permissions:` as TEXT rather than as a YAML node. The insertion is redundant +# regardless: `read-all` already grants every read scope, `actions: read` +# included. +# +# So this check is deliberately dumb and total: parse every workflow, fail on +# any that does not. It cannot be satisfied by a sweep that only looks at text. + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)" + +if ! command -v python3 >/dev/null 2>&1; then + echo "validate-workflow-yaml: python3 not found — cannot verify workflows" >&2 + exit 1 +fi + +python3 - "$@" <<'PY' +import glob, sys + +try: + import yaml +except ImportError: + print("validate-workflow-yaml: PyYAML not installed — cannot verify workflows", + file=sys.stderr) + sys.exit(1) + +files = sorted(glob.glob(".github/workflows/*.yml") + glob.glob(".github/workflows/*.yaml")) +if not files: + print("validate-workflow-yaml: no workflow files found", file=sys.stderr) + sys.exit(1) + +bad = [] +for f in files: + try: + doc = yaml.safe_load(open(f)) + except Exception as e: + bad.append((f, str(e).splitlines()[0])) + continue + # A workflow that parses but has no jobs is equally inert. + if not isinstance(doc, dict) or not doc.get("jobs"): + bad.append((f, "parses but declares no jobs — would run nothing")) + +if bad: + print("ERROR: unparseable or inert workflow(s) — these produce NO check run,") + print(" not a red X, so CI would look green while the gate is dead:\n") + for f, err in bad: + print(f" {f}\n {err}") + print("\nIf this is `actions: read` under `permissions: read-all`, delete the") + print("added line: `read-all` already grants every read scope.") + sys.exit(1) + +print(f"validate-workflow-yaml: all {len(files)} workflows parse and declare jobs") +PY diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml index f568eee..ef425c0 100644 --- a/.github/workflows/container-build.yml +++ b/.github/workflows/container-build.yml @@ -30,7 +30,6 @@ on: workflow_dispatch: permissions: read-all - actions: read concurrency: group: container-build-${{ github.ref }} diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index a6fc97c..37612e2 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -17,6 +17,24 @@ permissions: contents: read jobs: + # --------------------------------------------------------------------------- + # Job 0: every workflow must PARSE + # + # A malformed workflow is not a red check — Actions rejects it at parse time, + # yielding zero jobs and NO check run, so the board reads green while the gate + # is dead. This repo hit that twice in one day from the same sweep (fixed in + # #64, reintroduced by #68). This job is the backstop. + # --------------------------------------------------------------------------- + workflow-yaml: + name: Workflows parse (no silent-dead gates) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Validate every workflow parses and declares jobs + run: ./.githooks/validate-workflow-yaml.sh + # --------------------------------------------------------------------------- # Job 1: A2ML manifest validation # --------------------------------------------------------------------------- diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2fd946b..3b41276 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -26,7 +26,6 @@ on: workflow_dispatch: permissions: read-all - actions: read concurrency: group: e2e-${{ github.ref }} diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 14221e8..148e62e 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -23,7 +23,6 @@ on: workflow_dispatch: permissions: read-all - actions: read concurrency: group: install-smoke-${{ github.ref }} diff --git a/.github/workflows/zig.yml b/.github/workflows/zig.yml index 9ae2c47..0a65d9f 100644 --- a/.github/workflows/zig.yml +++ b/.github/workflows/zig.yml @@ -24,7 +24,6 @@ on: workflow_dispatch: permissions: read-all - actions: read concurrency: group: zig-${{ github.ref }} From 3a33ef194bcbee3c68dda00dc5a94f32211fad7b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:49:49 +0100 Subject: [PATCH 2/2] docs: restore the governance content #68 deleted; retire push-email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clean-ups, both closing loose ends left by earlier sweeps. 1. GOVERNANCE — recover what was lost, fix the dangling references ------------------------------------------------------------------ #68 deleted GOVERNANCE.adoc as a duplicate of GOVERNANCE.md. It was not one. Fifteen sections existed ONLY in the .adoc, and went with it: Contribution Lifecycle · Conflict Resolution · Project Policies · Repository-Specific Conventions · Governance Evolution · See Also · Changelog "Repository-Specific Conventions" is the load-bearing one — it is where the signing requirement, the SPDX rule, the contractile layout and the .machine_readable/6a2/ convention were written down. That content is recovered here from 5501ec6^ and merged into GOVERNANCE.md, converted from AsciiDoc to Markdown. GOVERNANCE.md is now genuinely the single source, which is what #68 intended but did not achieve. The deletion also left two dangling references, now repointed: .machine_readable/contractiles/Trustfile.a2ml:74 areas: GOVERNANCE.adoc .machine_readable/contractiles/Mustfile.a2ml:38 "GOVERNANCE.adoc ... must exist" The Mustfile one is worth flagging: it is a `severity: critical` contract whose `run:` is literally test -f GOVERNANCE.adoc && test -f MAINTAINERS.adoc && test -f .github/CODEOWNERS That assertion has been FALSE since #68 and nothing went red — because nothing executes it. grep finds no runner for Mustfile contracts; validate-a2ml.sh only parses them as TOML. So a critical contract silently became false. Repointed to GOVERNANCE.md, and the assertion now genuinely holds — but the contracts being declarative-only is a separate gap, filed rather than papered over here. 2. push-email-notify.yml — deleted, not left disabled ----------------------------------------------------- Estate dual-use policy removes push-email everywhere except 007, robodog-defensive-systems-lab and defensive-multiplicity. statistikles is not one of the three, so it was already `disabled_manually`. Verified dormant: PUSH_EMAIL_ENABLED is not set on this repo, and nothing else references it. Deleting beats leaving it disabled — a disabled-but-present workflow reads as an oversight to the next person, and it is one re-run of a farm sweep away from being re-enabled. Verified: all 18 remaining workflows parse and declare jobs. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/push-email-notify.yml | 34 ---------- .machine_readable/contractiles/Mustfile.a2ml | 4 +- .machine_readable/contractiles/Trustfile.a2ml | 2 +- GOVERNANCE.md | 65 +++++++++++++++++++ 4 files changed, 68 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/push-email-notify.yml diff --git a/.github/workflows/push-email-notify.yml b/.github/workflows/push-email-notify.yml deleted file mode 100644 index c08ea16..0000000 --- a/.github/workflows/push-email-notify.yml +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Dormant push-email notification. ARMED by setting the repo variable -# PUSH_EMAIL_ENABLED=true (the single on/off switch). Addresses are pre-filled; -# sending needs the org SMTP secrets (SMTP_HOST/PORT/USER/PASS). Inherited by -# new repos from the template; placed on existing repos by the farm sweep. -name: Push email notification -on: - push: {} -permissions: - actions: read - contents: read -jobs: - notify: - name: Email on push - if: ${{ vars.PUSH_EMAIL_ENABLED == 'true' }} - runs-on: ubuntu-latest - steps: - - name: Send push notification email - uses: dawidd6/action-send-mail@12335b969ae3fb71bee5f2c6b829744261aec34c # pinned - with: - server_address: ${{ secrets.SMTP_HOST }} - server_port: ${{ secrets.SMTP_PORT }} - secure: true - username: ${{ secrets.SMTP_USER }} - password: ${{ secrets.SMTP_PASS }} - from: "GitHub Push <${{ secrets.SMTP_USER }}>" - to: "jonathan.jewell@gmail.com j.d.a.jewell@open.ac.uk" - subject: "[${{ github.repository }}] push to ${{ github.ref_name }} by ${{ github.actor }}" - body: | - Repository: ${{ github.repository }} - Branch: ${{ github.ref_name }} - Pusher: ${{ github.actor }} - Compare: ${{ github.event.compare }} - Head msg: ${{ github.event.head_commit.message }} diff --git a/.machine_readable/contractiles/Mustfile.a2ml b/.machine_readable/contractiles/Mustfile.a2ml index 55f8ab4..4cb9feb 100644 --- a/.machine_readable/contractiles/Mustfile.a2ml +++ b/.machine_readable/contractiles/Mustfile.a2ml @@ -35,8 +35,8 @@ hooks fail if any check fails. - severity: critical ### governance-docs -- description: GOVERNANCE.adoc, MAINTAINERS.adoc, CODEOWNERS must exist -- run: test -f GOVERNANCE.adoc && test -f MAINTAINERS.adoc && test -f .github/CODEOWNERS +- description: GOVERNANCE.md, MAINTAINERS.adoc, CODEOWNERS must exist +- run: test -f GOVERNANCE.md && test -f MAINTAINERS.adoc && test -f .github/CODEOWNERS - severity: critical ### machine-readable-dir diff --git a/.machine_readable/contractiles/Trustfile.a2ml b/.machine_readable/contractiles/Trustfile.a2ml index e2028b5..2821b05 100644 --- a/.machine_readable/contractiles/Trustfile.a2ml +++ b/.machine_readable/contractiles/Trustfile.a2ml @@ -71,7 +71,7 @@ Current trust level: maximal - severity: advisory - areas: - .machine_readable/ - - GOVERNANCE.adoc + - GOVERNANCE.md - MAINTAINERS.adoc - .github/CODEOWNERS diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 990aadd..460a1fb 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -157,4 +157,69 @@ with the community before adoption, even though the BDFL retains final authority --- +## Contribution Lifecycle + +| Stage | Process | +|---|---| +| **Ideation** | Open issue, discuss feasibility | +| **Development** | Fork, implement, test thoroughly | +| **Review** | Submit PR, maintainer reviews within 7 days | +| **Merge** | Maintainer merges or requests changes | +| **Release** | Maintainer publishes according to project conventions | + +## Conflict Resolution + +In case of disagreements: + +1. Discuss in the relevant GitHub issue or PR. +2. Provide technical justification for positions. +3. Maintainer mediates and makes the final decision. +4. The decision is documented and can be revisited later. + +## Project Policies + +This repository adheres to hyperpolymath estate-wide policies: + +- **Licence** — MPL-2.0 for code, CC-BY-SA-4.0 for prose (per `standards/LICENCE-POLICY.adoc`) +- **Code of Conduct** — follows hyperpolymath `CODE_OF_CONDUCT.md` +- **Security** — follows hyperpolymath `SECURITY.md` +- **Contributing** — follows hyperpolymath `CONTRIBUTING.adoc` conventions + +## Repository-Specific Conventions + +| Convention | Description | +|---|---| +| **Signing** | All commits must be signed (SSH or GPG) | +| **SPDX headers** | All source files must carry SPDX licence identifiers | +| **Contractiles** | `Mustfile`, `Trustfile`, `Intendfile`, `Adjustfile` in root | +| **Machine readable** | `META.a2ml` in `.machine_readable/6a2/` | +| **CI/CD** | GitHub Actions workflows in `.github/workflows/` | + +## Governance Evolution + +As the project grows, this governance model may evolve: + +- **Adding co-maintainers** — when contribution volume warrants it +- **Forming a team** — for complex multi-maintainer projects +- **Adopting TPCF** — for large, multi-repository projects (see `rhodium-standard-repositories`) + +Changes to this document require the same process as Significant Changes above. + +## See Also + +- [Maintainers](MAINTAINERS.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Contributing Guide](CONTRIBUTING.md) +- [Estate Licence Policy](https://github.com/hyperpolymath/standards/blob/main/LICENCE-POLICY.adoc) +- [rhodium-standard-repositories (TPCF)](https://github.com/hyperpolymath/standards) + +## Changelog + +| Date | Change | By | +|---|---|---| +| 2026-06-07 | Initial governance model established | @hyperpolymath | +| 2026-07-27 | Merged in the sections that had lived only in `GOVERNANCE.adoc`, which was deleted in #68; this file is now the single source. | @hyperpolymath | + +--- + Copyright (c) 2026 hyperpolymath. Licensed under MPL-2.0.