diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fd8a75..79fcebb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,29 +5,64 @@ on: push: branches: [develop, main] +permissions: + contents: read + jobs: pytest: - runs-on: ubuntu-latest - services: - postgres: - image: postgres:16 - env: - POSTGRES_USER: agent - POSTGRES_HOST_AUTH_METHOD: trust - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U agent" - --health-interval 5s - --health-timeout 5s - --health-retries 10 + runs-on: [self-hosted] env: - AGENT_TEST_PG: host=127.0.0.1 port=5432 user=agent dbname=postgres A38_TEST_DOCKER: '1' steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-python@v5 with: python-version: "3.12" - - run: pip install -e ".[test]" - - run: pytest + - name: Preflight runner tools + run: | + set -euo pipefail + # Functional Homebrew toolpaths when present (macOS). Not private hosts. + if [ -d /opt/homebrew/bin ]; then + echo "/opt/homebrew/bin" >> "$GITHUB_PATH" + export PATH="/opt/homebrew/bin:$PATH" + fi + if [ -d /opt/homebrew/opt/postgresql@17/bin ]; then + echo "/opt/homebrew/opt/postgresql@17/bin" >> "$GITHUB_PATH" + export PATH="/opt/homebrew/opt/postgresql@17/bin:$PATH" + elif [ -d /opt/homebrew/opt/postgresql@16/bin ]; then + echo "/opt/homebrew/opt/postgresql@16/bin" >> "$GITHUB_PATH" + export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH" + fi + # Linux runners use already-installed system PostgreSQL/Docker paths. + if ! command -v initdb >/dev/null; then + echo "PostgreSQL binaries (initdb) must already be on PATH on this self-hosted runner." >&2 + echo "Install them outside CI; this workflow does not run apt or brew." >&2 + exit 1 + fi + if ! command -v pg_ctl >/dev/null; then + echo "PostgreSQL binaries (pg_ctl) must already be on PATH on this self-hosted runner." >&2 + echo "Install them outside CI; this workflow does not run apt or brew." >&2 + exit 1 + fi + if ! command -v docker >/dev/null; then + echo "Working Docker must already be installed on this self-hosted runner." >&2 + echo "Install and start it outside CI; this workflow does not run apt or brew." >&2 + exit 1 + fi + if ! docker info >/dev/null 2>&1; then + echo "Docker is installed but the daemon is not reachable on this self-hosted runner." >&2 + exit 1 + fi + - name: Install and test in isolated venv + run: | + set -euo pipefail + VENV="${RUNNER_TEMP}/agent-test-venv-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + python -m venv "$VENV" + # shellcheck disable=SC1091 + source "$VENV/bin/activate" + python -m pip install --disable-pip-version-check -e ".[test]" + # Use tests/conftest.py ensure_cluster (temporary local PostgreSQL). + unset AGENT_TEST_PG + pytest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d2754c..7c154d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,20 @@ - Public repository: English for commits and comments. The visible pull-request summary is an `EN:` block, optionally followed by a labeled `DE:` block. - Do not name private repositories, internal hostnames, or internal infrastructure. - Add or update tests in the same change. -- Run `pytest` on the exact clean signed final head before Ready for review. Tests need PostgreSQL (`AGENT_TEST_PG` or a local `initdb`). Full pytest is not a gate for the first draft publication. +- Run `pytest` on the exact clean signed final head before Ready for review. With `AGENT_TEST_PG` unset, `tests/conftest.py` starts a temporary local PostgreSQL cluster via `ensure_cluster` (needs `initdb` / `pg_ctl` on `PATH`). Full pytest is not a gate for the first draft publication. - Pytest (or any green local suite) is a **check**, not Ready for review and not completion. +## Self-hosted GitHub Actions runners + +This repository's workflows use `runs-on: [self-hosted]` only (no GitHub-hosted fallback). GitHub orchestrates jobs and shows statuses; the process runs on an adopter-operated machine. Register and label runners outside this package — there is no production runner installer here. + +CI prerequisites on every runner that executes `.github/workflows/test.yml`: + +- PostgreSQL client/server binaries already on `PATH` (`initdb`, `pg_ctl`, and related tools). On macOS, existing Homebrew paths such as `/opt/homebrew/bin` and `/opt/homebrew/opt/postgresql@17/bin` (or `@16` when that is what is installed) are accepted when present. On Linux, use the distribution's already-installed PostgreSQL tool paths. +- A working Docker installation with a reachable daemon (`A38_TEST_DOCKER=1` is mandatory in CI and must not be skipped). + +The workflow preflight fails loudly when those tools are missing. It does **not** run `apt`, `brew`, or other package installs. Keep shared global Python environments untouched: CI creates an isolated venv under `RUNNER_TEMP` keyed by `GITHUB_RUN_ID` / `GITHUB_RUN_ATTEMPT`. + ## Ready for review A draft plus local tests is not done. Do not claim the pull request is finished, done, or completed at that point — including after leave-draft. Draft timing and CI ownership while the draft is open are defined in [docs/pull-request-lifecycle.md](docs/pull-request-lifecycle.md). @@ -21,7 +32,7 @@ Ready for review requires all of: 3. Codex runs only if both grok dimensions are approved. If a vendor cannot run, abort loudly; do not record `approved`; do not substitute another vendor. 4. Zero findings only after an explicit complete pass. Empty, partial, timeout, or unavailable output is not zero findings. Iterate until all four lane verdicts on this head are approved. 5. Inner implement/review rounds (`review-loop`) are not the PR reviews (`pr-review`). -6. CI green on **this** head. This public repository uses GitHub Actions. `skipped` and `cancelled` are not green unless the workflow documents that skip. The local-CI comment schema for **private** product repositories is defined in [docs/local-ci-v1.md](docs/local-ci-v1.md) and verified by `agent local-ci verify`. +6. CI green on **this** head. This public repository uses GitHub Actions with self-hosted runners for job execution. `skipped` and `cancelled` are not green unless the workflow documents that skip. The local-CI comment schema for **private** product repositories is defined in [docs/local-ci-v1.md](docs/local-ci-v1.md) and verified by `agent local-ci verify`. 7. Stay draft until the reviews and CI above hold on this head. Then one comment whose review-pass count is those four `approved` verdicts on this head, then mark the GitHub pull request ready for review (`isDraft=false`). When spine and pr-review are attached, `agent allow --action pr-ready` only checks task state (`pushing` or `pr-review`); it is not the leave-draft verdict. Do not mark ready if it denies. Ready for review is still not merge and not completion. 8. A human merges. Claim pull-request completion only after that merge is verified. When spine is attached, `agent allow --action task-done` still needs the workflow checklist and both summary sentences; that ledger state is not proof of pull-request completion. diff --git a/docs/a38-guard.md b/docs/a38-guard.md index 4018160..237c4c3 100644 --- a/docs/a38-guard.md +++ b/docs/a38-guard.md @@ -6,6 +6,8 @@ dfx pr guard explains a repository's centrally defined [A38 rules](a38.md), chec Install the [example workflow](../examples/a38-guard.yml) on the target repository's default branch. Replace `USES_REF_PIN_ME` with a reviewed, published **full commit SHA** of this repository. The example is not deployable until that placeholder is replaced. Keep the guard's executable action pinned even when approving policy migrations. +The example job uses `runs-on: [self-hosted]` so the guard process runs on an adopter-operated machine. GitHub still orchestrates the workflow and displays check statuses; it must not be the execution host. Adopters may append their own runner labels or groups after `self-hosted`. That routing belongs in the adopter's workflow configuration only — the guard runtime never hardcodes runner labels, hostnames, or private repository names. Do not put private infrastructure names into public documentation or the shared example. + The [composite action](../.github/actions/a38-guard/action.yml) uses pinned setup-python and PyYAML 6.0.2, and imports only the trusted action's sources through `github.action_path/../../../src`. Both Python steps run from the trusted action directory with safe-path mode (`python -P`), and replace inherited `PYTHONPATH` with the trusted source path, preventing consumer modules from shadowing the guard or its installer. It does not install dependencies or run scripts from the consumer checkout. Install the package's declared dependencies for standalone use; there is no fallback YAML parser. The guard's comment and JSON expose three different immutable links: @@ -22,14 +24,14 @@ Standalone execution accepts the same explicit trusted `A38_RUNTIME_REVISION`. W The token requires contents read, pull requests write, issues write and statuses write. Publishing the guard comment on a pull request needs `pull-requests: write` for `GITHUB_TOKEN`; `issues: write` alone is not enough and yields 403. Policy migrations also require permission to read collaborators' effective repository permissions. If that API is unavailable, the migration fails closed. Use a dedicated GitHub App or service account with the necessary repository access for external operation. Tokens are taken from `GH_TOKEN` or `GITHUB_TOKEN` and never printed. -Actions must actually be available for event-driven operation. When Actions are blocked or unavailable, run the same reconciler on a trusted external host: +Actions must actually be available for event-driven operation, with jobs scheduled onto the adopter's self-hosted runners. When Actions are blocked or no suitable self-hosted runner is registered, run the same reconciler on a trusted machine the adopter controls: ```sh agent pr-guard --repo OWNER/NAME --all-open --dry-run agent pr-guard --repo OWNER/NAME --all-open ``` -Schedule that command externally when Actions are unavailable; no daemon is installed. The example workflow also reconciles all open PRs at minutes 17 and 47 of every hour and serializes all bot runs for the repository. Its manual dispatch accepts either a PR number or `all_open=true`. GitHub Actions does not guarantee delivery of every pending concurrency event, so scheduled reconciliation recovers missed events, base changes and permission changes. Immutable SHA-addressed contents and trees are cached within the API client, up to 128 entries; comments, reviews, permissions and PR snapshots are never cached. +Schedule that command externally when Actions are unavailable; no daemon is installed and this package does not ship a production runner installer. The example workflow also reconciles all open PRs at minutes 17 and 47 of every hour and serializes all bot runs for the repository. Its manual dispatch accepts either a PR number or `all_open=true`. GitHub Actions does not guarantee delivery of every pending concurrency event, so scheduled reconciliation recovers missed events, base changes and permission changes. Immutable SHA-addressed contents and trees are cached within the API client, up to 128 entries; comments, reviews, permissions and PR snapshots are never cached. ## Trust and policy diff --git a/examples/a38-guard.yml b/examples/a38-guard.yml index 894ed4d..4b7e3f4 100644 --- a/examples/a38-guard.yml +++ b/examples/a38-guard.yml @@ -1,6 +1,8 @@ # Example consumer workflow for dfx pr guard. # Before rollout: replace USES_REF_PIN_ME with a pinned commit SHA of DFXswiss/agent. # Do not check out the pull-request head for this job. +# runs-on starts at [self-hosted]. Adopters may append their own runner labels or +# groups; routing is adopter configuration, not hardcoded in the guard runtime. name: dfx pr guard @@ -35,7 +37,7 @@ permissions: jobs: guard: - runs-on: ubuntu-latest + runs-on: [self-hosted] steps: - name: dfx pr guard uses: DFXswiss/agent/.github/actions/a38-guard@USES_REF_PIN_ME