diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..b6d7219 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +branch="$(git symbolic-ref --quiet --short HEAD || true)" +case "$branch" in + main|master) + echo "Direct commits to $branch are blocked; create a feature branch." >&2 + exit 1 + ;; +esac + +staged="$(git diff --cached --name-only --diff-filter=ACMR)" +if printf '%s\n' "$staged" | grep -Eq '(^|/)\.env($|\.)|(^|/)(__pycache__|\.pytest_cache)(/|$)|\.py[co]$'; then + echo "Refusing to commit runtime environment or Python cache files." >&2 + exit 1 +fi + +bash scripts/ci/run-fast-checks.sh diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..3cc53a9 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +self-hosted-runner: + labels: + - self-hosted + - linux + - shell-only + - public diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac85cf8..89f1012 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: push: branches: - main + workflow_dispatch: permissions: contents: read @@ -21,26 +22,30 @@ jobs: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Validate JSON schemas - run: python -m json.tool schemas/issue-contract.schema.json >/dev/null && python -m json.tool schemas/pr-contract.schema.json >/dev/null && python -m json.tool schemas/public-repository-standard-v1.schema.json >/dev/null && python -m json.tool schemas/provenance-manifest-v1.schema.json >/dev/null + - name: Run fast checks + run: bash scripts/ci/run-fast-checks.sh - - name: Validate public repository policy bundle - run: python scripts/flow/validate_policy_bundle.py - - - name: Validate executable transition policy - run: python scripts/flow/validate_transition.py - - - name: Validate security and provenance policy - run: python scripts/flow/validate_provenance.py - - - name: Validate contribution lifecycle policy - run: python scripts/flow/evaluate_contribution.py + workflow-lint: + name: Workflow Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Build and verify policy release offline - run: python scripts/flow/build_policy_release.py --output /tmp/flow-policy-release-ci --version 1.0.0 --verify + - name: Run actionlint with repository runner labels + run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 - - name: Test public repository policy bundle - run: python -m unittest discover -s tests -v + extended-validation: + name: Extended Validation + if: github.event_name != 'pull_request' + runs-on: + - self-hosted + - linux + - shell-only + - public + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Compile flow inspector - run: python -m py_compile scripts/flow/inspector_core.py scripts/flow/inspect_repo_flow.py + - name: Build and verify release contract + run: bash scripts/ci/run-extended-validation.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac6a333 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ diff --git a/AGENTS.md b/AGENTS.md index 54003c8..c2f0454 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ - Always work on a feature branch. Hooks block commits to `main` and `master`; enable them with `git config core.hooksPath .githooks`. - Stack baseline: Generic polyglot. - CI baseline: fast PR checks stay cheap and shell-safe; extended validation runs on `main`, nightly, or manual dispatch. -- Self-hosted runner policy: shell-safe jobs may use `[self-hosted, synology, shell-only, public]`; anything needing Docker, service containers, browser infra, or `container:` must stay on GitHub-hosted runners. +- Self-hosted runner policy: shell-safe jobs may use `[self-hosted, linux, shell-only, public]`; anything needing Docker, service containers, browser infra, or `container:` must stay on GitHub-hosted runners. - Add or update tests for every interactive, branching, or operator-facing behavior change. - PRs must use the generated pull request template. The required PR gate validates summary, issue linkage, validation evidence, and risk notes. - Never commit real secrets, runtime auth, or machine-local env files. Use templates and GitHub environments instead. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 424a3d2..3676ca2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Contributions should start from a GitHub issue that is assigned or explicitly en - Install dependencies for the selected stack before changing code. - Enable repo hooks with `git config core.hooksPath .githooks`; they block direct commits to `main` and catch committed runtime env files. - Use `project.bootstrap.yaml` as the source of truth for governance, CI, environments, and bootstrap-managed guidance files. +- Review `docs/bootstrap/onboarding.md` before the first merge after governance changes. ## Change Expectations @@ -17,7 +18,8 @@ Contributions should start from a GitHub issue that is assigned or explicitly en ## Validation -- Run the relevant local checks before opening a PR. +- Run `bash scripts/ci/run-fast-checks.sh` before opening a PR. +- Run `bash scripts/ci/run-extended-validation.sh` for release, policy-package, or mainline validation changes. - For this bootstrap contract, the required PR check surface is `CI Gate`. - Document any skipped checks in the PR with a concrete reason. diff --git a/README.md b/README.md index e3c008c..961ea3d 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,13 @@ Open PR clearance has priority over starting new implementation work: ## Validation ```sh -python -m json.tool schemas/issue-contract.schema.json >/dev/null -python -m json.tool schemas/pr-contract.schema.json >/dev/null -python -m py_compile scripts/flow/inspector_core.py scripts/flow/inspect_repo_flow.py -python -m unittest discover -s tests -v +bash scripts/ci/run-fast-checks.sh +``` + +For mainline or release-contract validation, including an offline deterministic policy build: + +```sh +bash scripts/ci/run-extended-validation.sh ``` Inspect a repository without mutating GitHub or local assignment state: diff --git a/docs/bootstrap/onboarding.md b/docs/bootstrap/onboarding.md new file mode 100644 index 0000000..7814471 --- /dev/null +++ b/docs/bootstrap/onboarding.md @@ -0,0 +1,33 @@ +# Bootstrap onboarding + +Review this file before the first merge after Bootstrap changes repository governance. + +## Managed and product-owned boundaries + +`project.bootstrap.yaml` is the repository control plane. Its `repo.managedPaths` list identifies Bootstrap-managed guidance and GitHub templates. Product code, policy data, validators, CI scripts, and canonical `github/` inputs remain product-owned unless the manifest says otherwise. + +Run Bootstrap in plan mode and review the managed/product-owned inventory before apply. Flow issue [#13](https://github.com/OMT-Global/flow/issues/13) records the current plan evidence and the external resolver/projection blocker. + +## Review and merge gates + +- Governing work must link a GitHub issue. +- `CI Gate` is the required repository check. +- One non-author approval from `OMT-Codeowners` is required. +- The PR author enables squash auto-merge when checks, review, and conversation gates can converge safely. +- Policy releases follow the exact-pin and independent-review rules in [Policy releases and upgrades](../policy-release-and-upgrades.md). + +## Runner policy + +The manifest's `hybrid-safe` policy maps Flow's shell-safe CI Gate to `[self-hosted, linux, shell-only, public]`. The live job used to verify this contract on 2026-07-15 ran in runner group `linux-public` with those exact labels. Jobs requiring Docker, service containers, browser infrastructure, or a workflow-level `container:` declaration stay on GitHub-hosted runners. + +If the workflow selector, this document, `AGENTS.md`, or live job metadata disagree, stop and reconcile the trust boundary before merging. + +## Local checks + +```sh +git config core.hooksPath .githooks +bash scripts/ci/run-fast-checks.sh +bash scripts/ci/run-extended-validation.sh +``` + +The fast check validates local Markdown targets, canonical-to-projected governance drift, workflow runner labels, release-pin guidance, policy data, and unit tests. Extended validation additionally builds and verifies the deterministic policy release offline. diff --git a/github/ISSUE_TEMPLATE/flow-blocker.yml b/github/ISSUE_TEMPLATE/flow-blocker.yml index 6cc6c6a..e4142e6 100644 --- a/github/ISSUE_TEMPLATE/flow-blocker.yml +++ b/github/ISSUE_TEMPLATE/flow-blocker.yml @@ -1,7 +1,8 @@ name: Flow blocker -about: Record a blocked unit of work with a next actor and unblock target. +description: Record a blocked unit of work with a next actor and unblock target title: "Flow blocker: " -labels: ["state:blocked-infra"] +labels: + - state:blocked-infra body: - type: textarea id: blocked_item @@ -33,7 +34,14 @@ body: id: next_actor attributes: label: Next actor - options: [Pheidon, Apollo, Ares, Daedalus, Hephaestus, Hermes, Human] + options: + - Pheidon + - Apollo + - Ares + - Daedalus + - Hephaestus + - Hermes + - Human validations: required: true - type: textarea diff --git a/github/ISSUE_TEMPLATE/implementation.yml b/github/ISSUE_TEMPLATE/implementation.yml index fb7cc28..c7df5e9 100644 --- a/github/ISSUE_TEMPLATE/implementation.yml +++ b/github/ISSUE_TEMPLATE/implementation.yml @@ -1,12 +1,9 @@ name: Implementation work -about: Durable contract for autonomous or review-gated implementation work. +description: Durable contract for autonomous or review-gated implementation work title: "" -labels: ["state:intake"] +labels: + - state:intake body: - - type: markdown - attributes: - value: | - Use this form for work that should be executable by Pheidon or a worker lane. - type: textarea id: problem attributes: @@ -19,9 +16,6 @@ body: attributes: label: Acceptance criteria description: Concrete conditions that make this done. - placeholder: | - - ... - - ... validations: required: true - type: textarea @@ -29,9 +23,6 @@ body: attributes: label: Validation commands description: Commands or checks the worker should run. - placeholder: | - - npm test - - cargo test --locked validations: required: true - type: dropdown @@ -60,13 +51,3 @@ body: - Human validations: required: true - - type: textarea - id: dependencies - attributes: - label: Dependencies / blockers - description: Known blockers, prerequisites, or related issues/PRs. - - type: textarea - id: out_of_scope - attributes: - label: Out of scope - description: Explicitly excluded work. diff --git a/github/ISSUE_TEMPLATE/release-train.yml b/github/ISSUE_TEMPLATE/release-train.yml new file mode 100644 index 0000000..3d113c6 --- /dev/null +++ b/github/ISSUE_TEMPLATE/release-train.yml @@ -0,0 +1,68 @@ +name: Release Train +description: Governed release train for an OMT-Global repository +title: "Release: vX.Y.Z" +labels: + - release:train + - review:release +body: + - type: input + id: version + attributes: + label: Version + placeholder: v1.2.3 or v1.2.3-rc.1 + validations: + required: true + + - type: dropdown + id: channel + attributes: + label: Channel + options: + - rc + - beta + - stable + - maintenance + validations: + required: true + + - type: input + id: release_branch + attributes: + label: Release branch + placeholder: release/1.2 + + - type: input + id: target_sha + attributes: + label: Target SHA + placeholder: Full commit SHA for the release candidate + + - type: textarea + id: scope + attributes: + label: Scope + description: User-facing changes, fixes, risks, exclusions. + validations: + required: true + + - type: textarea + id: gates + attributes: + label: Gates + value: | + - [ ] Release branch created + - [ ] Scope locked + - [ ] Changelog/release notes prepared + - [ ] Version surfaces updated + - [ ] Preflight passed + - [ ] preflight_run_id recorded: + - [ ] Full validation passed + - [ ] validation_run_id recorded: + - [ ] Exact tag created + - [ ] Publish approval granted + - [ ] Artifacts published + - [ ] GitHub Release created or updated + - [ ] Release evidence uploaded + - [ ] Postpublish verification passed + - [ ] Floating tags/channels promoted if applicable + - [ ] Release issue closed diff --git a/github/pull_request_template.md b/github/pull_request_template.md index fe57cdb..57bb1ba 100644 --- a/github/pull_request_template.md +++ b/github/pull_request_template.md @@ -2,27 +2,41 @@ - -## Linked issue +## Governing Issue -Closes # +Refs # -## Flow contract +## Validation + +- [ ] Relevant local checks passed +- [ ] Required PR checks are expected to satisfy `CI Gate` +- [ ] Skipped checks are explained below + +## Bootstrap Governance + +- [ ] Changes are scoped to the linked issue +- [ ] Contributor or PR guidance changes are reflected in `CONTRIBUTING.md`, `.github/PULL_REQUEST_TEMPLATE.md`, and `docs/bootstrap/onboarding.md` when applicable +- [ ] PR author enabled auto-merge where GitHub allows it, or GitHub plan-limit evidence/unavailable reason is recorded and the fallback merge-readiness policy applies +- [ ] No real secrets, runtime auth, or machine-local env files are committed + +## Flow Contract - Owner lane: - Repair owner: - Autonomy class: - Risk class: -## Validation +## Flow Merge Readiness -Commands/checks run: +- [ ] Every blocker has a next actor and next action +- [ ] No active blocking requested changes remain +- [ ] Non-author approval is present when required +- [ ] PR author enabled auto-merge where GitHub allows it, or recorded why it is unavailable/unsafe -- [ ] +## Merge Automation -## Merge readiness +- [ ] PR author enabled auto-merge with `gh pr merge --auto --squash`, or the reason it is unavailable/unsafe is noted below -- [ ] Linked issue or no-issue rationale is present -- [ ] Required checks pass or are intentionally non-blocking -- [ ] No active blocking requested changes remain -- [ ] Non-author approval is present if required -- [ ] Auto-merge is appropriate when gates pass +## Notes + +- diff --git a/scripts/ci/run-extended-validation.sh b/scripts/ci/run-extended-validation.sh new file mode 100755 index 0000000..08adec3 --- /dev/null +++ b/scripts/ci/run-extended-validation.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +bash scripts/ci/run-fast-checks.sh + +artifact_dir="$(mktemp -d "${TMPDIR:-/tmp}/flow-policy-release.XXXXXX")" +trap 'rm -rf "$artifact_dir"' EXIT + +python3 scripts/flow/build_policy_release.py \ + --output "$artifact_dir" \ + --version 1.0.0 \ + --verify diff --git a/scripts/ci/run-fast-checks.sh b/scripts/ci/run-fast-checks.sh new file mode 100755 index 0000000..6d11a9b --- /dev/null +++ b/scripts/ci/run-fast-checks.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PYTHONDONTWRITEBYTECODE=1 + +python3 -m json.tool schemas/issue-contract.schema.json >/dev/null +python3 -m json.tool schemas/pr-contract.schema.json >/dev/null +python3 -m json.tool schemas/public-repository-standard-v1.schema.json >/dev/null +python3 -m json.tool schemas/provenance-manifest-v1.schema.json >/dev/null + +python3 scripts/ci/validate_repository_contract.py +python3 scripts/flow/validate_policy_bundle.py +python3 scripts/flow/validate_transition.py +python3 scripts/flow/evaluate_contribution.py +python3 scripts/flow/validate_provenance.py +python3 -m unittest discover -s tests -v + +python3 -B -c 'from pathlib import Path; [compile(path.read_text(), str(path), "exec") for path in sorted(Path("scripts").rglob("*.py"))]' diff --git a/scripts/ci/validate_repository_contract.py b/scripts/ci/validate_repository_contract.py new file mode 100755 index 0000000..4ee44fd --- /dev/null +++ b/scripts/ci/validate_repository_contract.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Validate Flow's product-owned and projected repository contract.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import Iterable, Sequence +from urllib.parse import unquote + + +ROOT = Path(__file__).resolve().parents[2] + +GENERATED_FILE_PAIRS = ( + ("github/pull_request_template.md", ".github/PULL_REQUEST_TEMPLATE.md"), + ("github/ISSUE_TEMPLATE/implementation.yml", ".github/ISSUE_TEMPLATE/implementation.yml"), + ("github/ISSUE_TEMPLATE/flow-blocker.yml", ".github/ISSUE_TEMPLATE/flow_blocker.yml"), + ("github/ISSUE_TEMPLATE/release-train.yml", ".github/ISSUE_TEMPLATE/release_train.yml"), +) + +REQUIRED_LOCAL_PATHS = ( + ".githooks/pre-commit", + "docs/bootstrap/onboarding.md", + "scripts/ci/run-fast-checks.sh", + "scripts/ci/run-extended-validation.sh", +) + +RUNNER_LABELS = ("self-hosted", "linux", "shell-only", "public") +MARKDOWN_LINK = re.compile(r"\[[^]]*\]\(([^)]+)\)") + + +def check_generated_file_pairs( + root: Path, + pairs: Sequence[tuple[str, str]] = GENERATED_FILE_PAIRS, +) -> list[str]: + errors: list[str] = [] + for canonical_name, projected_name in pairs: + canonical = root / canonical_name + projected = root / projected_name + if not canonical.is_file(): + errors.append(f"missing canonical governance input: {canonical_name}") + continue + if not projected.is_file(): + errors.append(f"missing projected governance file: {projected_name}") + continue + if canonical.read_bytes() != projected.read_bytes(): + errors.append( + f"generated governance drift: {canonical_name} != {projected_name}" + ) + return errors + + +def markdown_files(root: Path) -> Iterable[Path]: + yield from sorted(root.glob("*.md")) + yield from sorted((root / "docs").rglob("*.md")) + + +def check_markdown_links(root: Path) -> list[str]: + errors: list[str] = [] + for document in markdown_files(root): + for raw_target in MARKDOWN_LINK.findall(document.read_text()): + target = raw_target.strip().strip("<>") + if not target or target.startswith(("#", "https://", "http://", "mailto:")): + continue + path_text = unquote(target.split("#", 1)[0]) + if path_text and not (document.parent / path_text).exists(): + errors.append( + f"missing Markdown target in {document.relative_to(root)}: {path_text}" + ) + return errors + + +def check_required_paths(root: Path) -> list[str]: + return [ + f"operator guidance target is missing: {path}" + for path in REQUIRED_LOCAL_PATHS + if not (root / path).exists() + ] + + +def check_runner_contract(root: Path) -> list[str]: + errors: list[str] = [] + workflow = (root / ".github/workflows/ci.yml").read_text() + runner_block = re.search( + r"(?ms)^ ci-gate:.*?^ runs-on:\s*\n((?:^ - [^\n]+\n)+)", + workflow, + ) + if runner_block is None: + errors.append("CI Gate does not declare a list-form runner selector") + else: + labels = tuple( + line.removeprefix(" - ").strip() + for line in runner_block.group(1).splitlines() + ) + if labels != RUNNER_LABELS: + errors.append( + f"CI Gate runner labels {labels!r} do not match {RUNNER_LABELS!r}" + ) + + documented_selector = f"[{', '.join(RUNNER_LABELS)}]" + for path in ("AGENTS.md", "docs/bootstrap/onboarding.md"): + if documented_selector not in (root / path).read_text(): + errors.append(f"{path} does not document runner selector {documented_selector}") + + manifest = (root / "project.bootstrap.yaml").read_text() + if "runnerPolicy: hybrid-safe" not in manifest: + errors.append("project.bootstrap.yaml must declare ci.runnerPolicy: hybrid-safe") + + actionlint = (root / ".github/actionlint.yaml").read_text() + for label in RUNNER_LABELS: + if f" - {label}\n" not in actionlint: + errors.append(f"actionlint is missing custom runner label: {label}") + return errors + + +def check_release_guidance(root: Path) -> list[str]: + errors: list[str] = [] + readme = (root / "README.md").read_text() + release_guide = (root / "docs/policy-release-and-upgrades.md").read_text() + if "Production consumers must pin an exact SemVer release or immutable commit SHA" not in readme: + errors.append("README must require an exact immutable production policy pin") + if "Compatibility aliases may exist for discovery, but never replace immutable production pins" not in release_guide: + errors.append("release guidance must limit floating aliases to discovery") + return errors + + +def validate(root: Path = ROOT) -> list[str]: + checks = ( + check_generated_file_pairs, + check_markdown_links, + check_required_paths, + check_runner_contract, + check_release_guidance, + ) + return [error for check in checks for error in check(root)] + + +def main() -> int: + errors = validate() + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 1 + print("repository contract valid") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py new file mode 100644 index 0000000..3fb183d --- /dev/null +++ b/tests/test_repository_contract.py @@ -0,0 +1,39 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / "scripts" / "ci" / "validate_repository_contract.py" +SPEC = importlib.util.spec_from_file_location("repository_contract", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +repository_contract = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(repository_contract) + + +class RepositoryContractTests(unittest.TestCase): + def test_repository_contract_is_current(self): + self.assertEqual(repository_contract.validate(ROOT), []) + + def test_generated_file_drift_fails(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + canonical = root / "canonical.txt" + projected = root / "projected.txt" + canonical.write_text("current\n") + projected.write_text("stale\n") + + errors = repository_contract.check_generated_file_pairs( + root, + (("canonical.txt", "projected.txt"),), + ) + + self.assertEqual( + errors, + ["generated governance drift: canonical.txt != projected.txt"], + ) + + +if __name__ == "__main__": + unittest.main()