From 3f832395800efac06690616b322443ca96e55109 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:39:04 +0900 Subject: [PATCH] ci(grc): stage central self-hosted product producer --- .github/workflows/grc-product.yml | 81 ++++++++++ .../grc-product-central-execution.md | 115 +++++++++++++++ docs/templates/grc-product.yml.in | 33 +++++ ..._grc_product_reusable_workflow_contract.py | 139 ++++++++++++++++++ 4 files changed, 368 insertions(+) create mode 100644 .github/workflows/grc-product.yml create mode 100644 docs/doctoring/grc-product-central-execution.md create mode 100644 docs/templates/grc-product.yml.in create mode 100644 tests/test_grc_product_reusable_workflow_contract.py diff --git a/.github/workflows/grc-product.yml b/.github/workflows/grc-product.yml new file mode 100644 index 0000000000..6e3fbee3ec --- /dev/null +++ b/.github/workflows/grc-product.yml @@ -0,0 +1,81 @@ +# Central execution only. Consumers retain trigger/concurrency declarations. +# Proposed until linux-cluster-ops#326 and QSR#136/#137 admit this caller. +name: GRC Product + +on: + workflow_call: + +permissions: + contents: read + +jobs: + product: + name: Lint, tests, and coverage + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + timeout-minutes: 20 + defaults: + run: + shell: bash --noprofile --norc -e -o pipefail {0} + steps: + - name: Admit GRC caller before checkout + env: + CALLER_REPOSITORY: ${{ github.repository }} + CALLER_EVENT: ${{ github.event_name }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + run: | + test "$CALLER_REPOSITORY" = "ContextualWisdomLab/governance-risk-compliance" + case "$CALLER_EVENT" in + pull_request) + test "$HEAD_REPOSITORY" = "$CALLER_REPOSITORY" + ;; + push|workflow_dispatch) ;; + *) exit 1 ;; + esac + - name: Harden runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 + with: + egress-policy: audit + - name: Checkout exact source revision + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Verify exact source head + env: + SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$SOURCE_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - name: Set up uv without a shared cache + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.5" + enable-cache: false + cache-local-path: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + - name: Install the hash-locked product + run: uv sync --locked --extra dev + - name: Lint + run: uv run ruff check cwl_grc tests + - name: Docstrings + run: uv run interrogate --fail-under 100 cwl_grc + - name: Tests and coverage + run: >- + uv run pytest --cov=cwl_grc --cov-branch + --cov-report=term-missing --cov-fail-under=100 + - name: Compile + run: uv run python -m compileall -q cwl_grc tests + - name: Verify lock freshness + run: uv lock --check + - name: Verify clean product tree + run: | + git diff --check + test -z "$(git status --porcelain)" + - name: Remove job-private uv cache + if: always() + env: + UV_CACHE_DIR: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + run: rm -rf -- "$UV_CACHE_DIR" diff --git a/docs/doctoring/grc-product-central-execution.md b/docs/doctoring/grc-product-central-execution.md new file mode 100644 index 0000000000..1998bd2c26 --- /dev/null +++ b/docs/doctoring/grc-product-central-execution.md @@ -0,0 +1,115 @@ +# GRC Product central execution — staged rollout + +Status: **Implemented source / cloud policy-tested / not activated**. + +## Ownership and existing delivery + +The GRC policy/control/evidence kernel remains in +`ContextualWisdomLab/governance-risk-compliance`. This control plane owns only +`.github/workflows/grc-product.yml`, its policy tests, `.github/actionlint.yaml` +(custom label validation), and the thin-caller template. +There is no new identity, risk, audit, scanner, database, or officer UI body here. + +- `ContextualWisdomLab/.github#2565` already owns conversion of all existing + central workflows to self-hosted. Do not duplicate its 73-job change. +- `ContextualWisdomLab/linux-cluster-ops#326` owns S1 placement, registration, + access custody, image provisioning, reset and actual canary. +- `ContextualWisdomLab/quarantine-sandbox-runtime#136` and + `ContextualWisdomLab/quarantine-sandbox-runtime#137` own reachability-attestation + contracts. Limited point probes are not complete CIDR or clean-job evidence. +- GRC privacy #69/#70; Keyverse #38, #55–#58, revision #73 and coverage #74; + PostgreSQL #18; version-one API #53; officer workspace #34/#54; readiness #17; + domain roadmap #27–#30; and baseline refresh #75 retain their existing owners. + All those references are in `ContextualWisdomLab/governance-risk-compliance`. + +A useful commercial GRC must distinguish artifact presence from tested control +operating effectiveness. The existing roadmap's dependency sequence remains: +identity/tenancy, PostgreSQL, evidence recovery, operations and stable API; +then internal controls, obligations/catalog, risk/audit and connected officer +workflows. This CI increment does not close any of those product issues. + +## Central contract + +The workflow accepts no executable caller input or secret inheritance. It runs +only the fixed GRC repository on same-repository PRs, pushes and manual runs. +Foreign callers, external PR heads, missing PR repository identity and unsupported +events fail in an actual pre-checkout shell; they are not skipped into success. +This guard executes after runner assignment and is not pre-lease isolation proof. + +Only `CWL CI isolated` AND `[self-hosted, Linux, X64, cwlab-ci-isolated]` may run +the product job. No hosted fallback, control/scanner pool or existing runner +relabeling is introduced. The original exact-head checkout, immutable Actions, +`contents: read`, no persisted Git credentials, Python 3.12, locked uv install, +Ruff, docstrings 100%, production statement/branch coverage 100%, compile, lock +freshness and dirty-tree gates remain. UV cache is job-private, not shared or +uploaded, and cleanup runs even on failure. VM cancellation/termination reset, +HOME/tool-cache/workspace cleanup and network isolation remain operator gates. + +StepSecurity harden-runner is retained, not silently removed. Its self-hosted +agent and subscription/image acceptance must be established by the operator; +a successful/no-op action is not monitoring or isolation evidence. No subscription +purchase is authorized here. References: GitHub's reusable-workflow runner context +reference and StepSecurity's self-hosted/how-it-works documentation. + +## Consumer rollout, without dangling remote pins + +`docs/templates/grc-product.yml.in` preserves GRC trigger/path exclusions and +workflow-scope PR-only cancellation. ARCHITECTURE.md is deliberately not ignored. +Do not add the same concurrency group to the reusable workflow: it can cancel +its calling run. Changing to a reusable job may change check-context display +names; inspect actual contexts and reconcile through the existing protection owner, +never remove required gates as a shortcut. + +1. Independently review and publish this central source through the ordinary + protected path. This session does not authorize commit, push or merge. +2. Obtain a real accepted isolated group, caller repository/workflow/ref/event/fork + eligibility, disposable image and cleanup proof under ops #326 and QSR. + Runner availability and central workflow availability are separate gates. +3. Replace `__CENTRAL_PRODUCT_COMMIT__` in the template with the exact reviewed, + published 40-hex central commit. Verify that commit contains this workflow. + Copy only the rendered caller to GRC `.github/workflows/product.yml` in an + owner-reviewed PR; do not use a fabricated hash, mutable ref or unresolved pin. +4. Update GRC's dirty-tree workflow regression to validate the released central + contract rather than require execution commands in the caller. Keep its + concurrency regression and all product quality thresholds unchanged. +5. Have existing #18, #34 and #17 owners inventory and migrate PostgreSQL, UI + and readiness executable jobs using independently reviewed central contracts. + Those contracts are not implemented by this initial Product slice. +6. Inspect GitHub-managed dynamic CodeQL/Code Quality configuration through its + current owner. Moving YAML cannot move those platform-managed jobs. +7. On the unchanged caller/central source pair, record actual run/job ID, runner + name/group, tool-image identity, all executed checks, cleanup and qualifying + independent approval. Only that evidence permits a runtime-complete claim. + +## Observed boundary — October 5, 2026 + +The initial GRC branch is `529cf321f134e26c0cd379ee53c06ab5297363b6`. +Central baseline is `37b10243cec3d160ecc9c1be75c71428b160a703`. +The GRC repository runner inventory returned zero. The original GRC worktree is +unchanged. Central preparation is in a dedicated cloud clone on S1, not another +writer's checkout. The operator infrastructure and production containers were +not modified. No remote central approval, protected integration, self-hosted +GitHub Product run, full-organizational migration or release is claimed. + +## Execution evidence + +New central-contract existence test first failed with +`GRC Product execution must be centrally owned`. The implemented workflow then +passed 8 policy tests normally and 8 with `GITHUB_ACTIONS=true` on S1. The +admission regression executes the actual shell for allowed and rejected tuples. +This is cloud source validation, not registered runner execution. + +Additional actual S1 acceptance: actionlint 1.7.12 (official archive digest verified), +new policy-test Ruff E9/F/I and `git diff --check` pass. The unchanged GRC baseline +ran in a resource-bounded, non-root, read-only Python 3.12.15 container with no +host socket, service mounts or credentials. Locked uv 0.12.5 installation, +Ruff, docstrings, compile and lock freshness pass; 48 tests cover all 769 +production statements and 152 branches. The run emitted 36 existing dependency +deprecation warnings (Starlette HTTPX and SQLite datetime adapter), which remain +visible rather than being suppressed. The product tree remains clean and the +container was removed. The bootstrap uv package was fetched by exact version, +not a hash-locked bootstrap; this receipt is not runner-image supply-chain approval. +Full central repository tests, actual GitHub Actions, protected integration and +release remain NOT_RUN. Further gate +results must be reported from their actual command output, not inherited from +central #2565 or any product feature PR. diff --git a/docs/templates/grc-product.yml.in b/docs/templates/grc-product.yml.in new file mode 100644 index 0000000000..b70f342ec0 --- /dev/null +++ b/docs/templates/grc-product.yml.in @@ -0,0 +1,33 @@ +# Render __CENTRAL_PRODUCT_COMMIT__ to the reviewed published 40-hex commit. +# Do not activate before the dedicated runner/access/isolation canary passes. +name: Product + +on: + push: + branches: + - develop + paths-ignore: + - "docs/**" + - "AGENTS.md" + - "CHANGELOG.md" + - "CLAUDE.md" + - "README.md" + pull_request: + paths-ignore: + - "docs/**" + - "AGENTS.md" + - "CHANGELOG.md" + - "CLAUDE.md" + - "README.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + product: + uses: ContextualWisdomLab/.github/.github/workflows/grc-product.yml@__CENTRAL_PRODUCT_COMMIT__ diff --git a/tests/test_grc_product_reusable_workflow_contract.py b/tests/test_grc_product_reusable_workflow_contract.py new file mode 100644 index 0000000000..27bfdbefea --- /dev/null +++ b/tests/test_grc_product_reusable_workflow_contract.py @@ -0,0 +1,139 @@ +"""Contracts for centrally owned, isolated GRC Product execution.""" + +import os +import re +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = REPO_ROOT / ".github/workflows/grc-product.yml" + + +def workflow_text() -> str: + """Read the central contract, failing explicitly before implementation.""" + assert WORKFLOW.is_file(), "GRC Product execution must be centrally owned" + return WORKFLOW.read_text(encoding="utf-8") + + +def admission_script() -> str: + """Extract the actual pre-checkout shell rather than emulate its decision.""" + source = workflow_text().split( + " - name: Admit GRC caller before checkout\n", 1 + )[1] + block = source.split(" run: |\n", 1)[1].split(" - name:", 1)[0] + return "\n".join(line[10:] for line in block.splitlines()) + + +def test_central_grc_product_contract_exists() -> None: + """Execution is reusable and has no schedule or privileged trigger.""" + source = workflow_text() + assert "on:\n workflow_call:\n" in source + assert "inputs:" not in source + assert "pull_request_target" not in source + assert "secrets:" not in source + + +def test_only_dedicated_self_hosted_group_is_admitted() -> None: + """No hosted fallback or privileged central pool executes product code.""" + source = workflow_text() + assert "group: CWL CI isolated" in source + assert "labels: [self-hosted, Linux, X64, cwlab-ci-isolated]" in source + assert len(re.findall(r"^ runs-on:", source, re.M)) == 1 + assert "ubuntu-24.04" not in source + assert "timeout-minutes: 20" in source + config = REPO_ROOT / ".github/actionlint.yaml" + assert config.is_file(), "Custom runner labels require repository actionlint config" + assert " - cwlab-ci-isolated" in config.read_text(encoding="utf-8") + assert "permissions:\n contents: read" in source + + +def test_actual_precheckout_admission_accepts_only_supported_caller_events() -> None: + """Unsupported callers/forks/events fail, not skip to a false green.""" + script = admission_script() + subprocess.run(["bash", "-n"], input=script, text=True, check=True) + repository = "ContextualWisdomLab/governance-risk-compliance" + for event, caller, head, accepted in ( + ("pull_request", repository, repository, True), + ("push", repository, "", True), + ("workflow_dispatch", repository, "", True), + ("pull_request", repository, "external/grc", False), + ("pull_request", repository, "", False), + ("push", "external/grc", "", False), + ("repository_dispatch", repository, "", False), + ("schedule", repository, "", False), + ): + result = subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], + env={ + **os.environ, + "CALLER_REPOSITORY": caller, + "CALLER_EVENT": event, + "HEAD_REPOSITORY": head, + }, + capture_output=True, + text=True, + check=False, + ) + assert (result.returncode == 0) is accepted, (event, caller, head) + + +def test_exact_head_and_checkout_identity_are_preserved() -> None: + """Checkout uses caller context with no credential persistence.""" + source = workflow_text() + assert source.index("Admit GRC caller before checkout") < source.index( + "actions/checkout@" + ) + assert "ref: ${{ github.event.pull_request.head.sha || github.sha }}" in source + assert "persist-credentials: false" in source + assert "SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}" in source + assert 'test "$(git rev-parse HEAD)" = "$SOURCE_SHA"' in source + for ref in re.findall(r"uses: (\S+)", source): + assert re.fullmatch(r"[^@]+@[0-9a-f]{40}", ref), ref + + +def test_original_quality_and_clean_tree_gates_are_not_lowered() -> None: + """All original commands and thresholds execute centrally.""" + source = workflow_text() + for command in ( + "uv sync --locked --extra dev", + "uv run ruff check cwl_grc tests", + "uv run interrogate --fail-under 100 cwl_grc", + "uv run pytest --cov=cwl_grc --cov-branch", + "--cov-report=term-missing --cov-fail-under=100", + "uv run python -m compileall -q cwl_grc tests", + "uv lock --check", + "git diff --check", + 'test -z "$(git status --porcelain)"', + ): + assert command in source, command + assert "continue-on-error" not in source + assert 'python-version: "3.12"' in source + + +def test_concurrency_lives_only_in_caller() -> None: + """Reusable/caller concurrency cannot cancel its own parent run.""" + assert "concurrency:" not in workflow_text() + + +def test_uv_cache_is_job_private_and_removed_on_failure() -> None: + """No persisted PR cache crosses execution trust contexts.""" + source = workflow_text() + assert "enable-cache: false" in source + assert 'version: "0.12.5"' in source + assert ( + "cache-local-path: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-" + "${{ github.run_attempt }}-${{ github.job }}" + ) in source + assert ( + "UV_CACHE_DIR: ${{ runner.temp }}/grc-uv-${{ github.run_id }}-" + "${{ github.run_attempt }}-${{ github.job }}" + ) in source + assert " - name: Remove job-private uv cache\n if: always()" in source + assert 'rm -rf -- "$UV_CACHE_DIR"' in source + + +def test_existing_hardening_action_is_retained() -> None: + """Self-hosted image-agent acceptance stays an operator prerequisite.""" + source = workflow_text() + assert "step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40" in source + assert "egress-policy: audit" in source