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/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/.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 }} 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.