diff --git a/.github/workflows/maintainer-app-readiness.yml b/.github/workflows/maintainer-app-readiness.yml new file mode 100644 index 000000000..807591724 --- /dev/null +++ b/.github/workflows/maintainer-app-readiness.yml @@ -0,0 +1,199 @@ +name: maintainer-app-readiness +run-name: Noema Maintainer App pre-activation audit + +on: + # repository_dispatch is evaluated only from the default branch. A caller + # cannot select unreviewed workflow code for these privileged token mints. + repository_dispatch: + types: [maintainer-app-readiness] + +concurrency: + group: noema-maintainer-app-readiness + cancel-in-progress: true + +# GITHUB_TOKEN is used only by the trusted checkout. Audit API calls use the +# repository-scoped Maintainer token; the Reviewer token is never exposed to a +# script and supplies only authenticated App slug/installation identity output. +permissions: + contents: read + +jobs: + preflight: + name: maintainer-app-pre-activation-audit + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: checkout event-bound default-branch commit + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # For repository_dispatch, github.sha is the default-branch commit + # bound when the event was created. Do not re-resolve a moving branch. + ref: ${{ github.sha }} + persist-credentials: false + + - name: mint repository-scoped Maintainer App token + id: maintainer_app + continue-on-error: true + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.NOEMA_MAINTAINER_APP_CLIENT_ID }} + private-key: ${{ secrets.NOEMA_MAINTAINER_APP_PRIVATE_KEY }} + owner: ContextualWisdomLab + repositories: noema + permission-actions: read + permission-checks: read + permission-contents: write + permission-metadata: read + permission-pull-requests: write + permission-statuses: read + + - name: mint repository-scoped Reviewer App identity token + id: reviewer_app + continue-on-error: true + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} + owner: ContextualWisdomLab + repositories: noema + permission-metadata: read + + - name: setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" + + - name: audit active main governance + id: governance + continue-on-error: true + env: + GH_TOKEN: ${{ steps.maintainer_app.outputs.token }} + NOEMA_GOVERNANCE_AUDIT_PATH: artifacts/governance/main-governance-audit.json + run: node scripts/main-governance-audit.mjs + + - name: audit effective Maintainer App identity and access + id: readiness + if: always() + continue-on-error: true + env: + GH_TOKEN: ${{ steps.maintainer_app.outputs.token }} + NOEMA_MAINTAINER_APP_SLUG: ${{ steps.maintainer_app.outputs.app-slug }} + NOEMA_MAINTAINER_INSTALLATION_ID: ${{ steps.maintainer_app.outputs.installation-id }} + NOEMA_REVIEWER_APP_SLUG: ${{ steps.reviewer_app.outputs.app-slug }} + NOEMA_REVIEWER_INSTALLATION_ID: ${{ steps.reviewer_app.outputs.installation-id }} + NOEMA_REVIEWER_LOGIN: ${{ vars.NOEMA_REVIEWER_LOGIN }} + NOEMA_MAINTENANCE_ENABLED: ${{ vars.NOEMA_MAINTENANCE_ENABLED }} + NOEMA_GOVERNANCE_AUDIT_PATH: artifacts/governance/main-governance-audit.json + NOEMA_MAINTAINER_READINESS_PATH: artifacts/operations/maintainer-app-readiness.json + run: node scripts/maintainer-app-readiness.mjs + + - name: inspect commercial-readiness loop without writes + id: dry_run + if: always() + continue-on-error: true + env: + GH_TOKEN: ${{ steps.maintainer_app.outputs.token }} + NOEMA_REVIEWER_LOGIN: ${{ vars.NOEMA_REVIEWER_LOGIN }} + MAINTAINER_APP_OUTCOME: ${{ steps.maintainer_app.outcome }} + run: | + set -euo pipefail + report_path="artifacts/operations/commercial-readiness-loop-dry-run.json" + write_failure_report() { + local reasonCode="$1" + local reasonDetail="$2" + FAILURE_CODE="$reasonCode" FAILURE_DETAIL="$reasonDetail" REPORT_PATH="$report_path" \ + node --input-type=module <<'NODE' + import { mkdirSync, writeFileSync } from "node:fs"; + import { dirname } from "node:path"; + + const reportPath = process.env.REPORT_PATH; + const reasonCode = process.env.FAILURE_CODE; + const reasonDetail = process.env.FAILURE_DETAIL; + const report = { + schemaVersion: 1, + repository: process.env.GITHUB_REPOSITORY || "ContextualWisdomLab/noema", + generatedAt: new Date().toISOString(), + apply: false, + openPullRequestCount: null, + remainingOpenPullRequestCount: null, + results: [ + { + number: null, + result: "operational_error", + reasons: [{ code: reasonCode, detail: reasonDetail }], + }, + ], + }; + mkdirSync(dirname(reportPath), { recursive: true }); + writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); + NODE + } + + if [ "$MAINTAINER_APP_OUTCOME" != "success" ]; then + reasonCode="maintainer_token_unavailable" + write_failure_report \ + "$reasonCode" \ + "Maintainer App token mint did not succeed; no GitHub API requests were attempted." + exit 1 + fi + + set +e + node scripts/hourly-commercial-readiness.mjs --report "$report_path" + loop_status=$? + set -e + if [ "$loop_status" -ne 0 ] && [ ! -s "$report_path" ]; then + reasonCode="commercial_loop_failed" + write_failure_report \ + "$reasonCode" \ + "Commercial-readiness dry run failed before it could retain its bounded report." + fi + exit "$loop_status" + + - name: upload main governance evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: main-governance-audit + path: artifacts/governance/main-governance-audit.json + if-no-files-found: error + retention-days: 90 + + - name: upload Maintainer App readiness evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: maintainer-app-readiness + path: artifacts/operations/maintainer-app-readiness.json + if-no-files-found: error + retention-days: 90 + + - name: upload no-write commercial loop evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: commercial-readiness-loop-dry-run + path: artifacts/operations/commercial-readiness-loop-dry-run.json + if-no-files-found: error + retention-days: 90 + + - name: enforce pre-activation gates + if: always() + env: + MAINTAINER_APP_OUTCOME: ${{ steps.maintainer_app.outcome }} + REVIEWER_APP_OUTCOME: ${{ steps.reviewer_app.outcome }} + GOVERNANCE_OUTCOME: ${{ steps.governance.outcome }} + READINESS_OUTCOME: ${{ steps.readiness.outcome }} + DRY_RUN_OUTCOME: ${{ steps.dry_run.outcome }} + run: | + set -euo pipefail + failed=0 + for gate in MAINTAINER_APP_OUTCOME REVIEWER_APP_OUTCOME GOVERNANCE_OUTCOME READINESS_OUTCOME DRY_RUN_OUTCOME; do + value="${!gate}" + if [ "$value" != "success" ]; then + printf '::error::%s was %s, not success.\n' "$gate" "$value" + failed=1 + fi + done + exit "$failed" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af9cded1..9e08ecb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- 전용 Maintainer GitHub App 활성화 전에 effective token을 기계적으로 감사하는 default-branch-only `maintainer-app-readiness` workflow와 `operations:preflight`를 추가. exact Maintainer/reviewer bot identity 분리, 단일 `ContextualWisdomLab/noema` repository scope, required Actions/checks/statuses/PR/contents read probes, live `main` governance PASS, no-write commercial-loop dry run을 실패-폐쇄 검증하고 bounded JSON evidence를 90일 보존한다. 별도의 Metadata-read Reviewer App token에서 인증된 `app-slug`·`installation-id`를 받아 `NOEMA_REVIEWER_LOGIN`이 실제 Reviewer App의 `[bot]` identity와 일치하는지 검증하며 Reviewer token 값은 script에 노출하지 않는다. 이 증빙은 해당 run의 scoped token과 reviewer credential binding만 입증하며 complete App registration, key ownership, administrator bypass, break-glass ownership은 #29/#27의 독립 검토 대상으로 명시한다. - credential-bearing GitHub API와 OIDC discovery/JWKS subrequest에 요청별 10초 deadline을 추가. `Request.signal`과 호출자 `RequestInit.signal`을 `AbortSignal.any()`로 보존하면서 독립 timeout signal을 결합하고, upstream stall은 bodyless `504 blocked-timeout` 정책 응답으로 실패-폐쇄하며 timer는 모든 종료 경로에서 정리한다. - `/exchange`의 `application/json` request body를 UTF-8 wire bytes 기준 8,192 bytes로 제한. 신뢰 가능한 `Content-Length` 초과는 body read 전에 413으로 차단하고, 길이 헤더가 없거나 잘못된 요청도 stream을 bounded-read하여 chunked 우회를 막는다. 검증된 작은 body만 재구성해 downstream parser로 전달하며 OIDC/JWKS 조회·GitHub App private-key 사용·GitHub API 호출 전에 실패-폐쇄하고 body 원문은 응답·로그에 남기지 않는다. - credential-bearing GitHub API와 OIDC subrequest에 `redirect: "manual"`을 강제하고, `3xx`/redirected response를 bodyless `502`로 치환하는 fail-closed egress wrapper를 추가. exact `api.github.com` origin과 pinned GitHub Actions discovery/JWKS endpoint 외 destination은 network call 전에 차단하며, wrapper 설치 실패나 runtime 교체 감지는 `/exchange` credential 처리 전에 `503 ERR_GITHUB_API`로 중단한다. diff --git a/docs/maintainer-app-readiness-audit.md b/docs/maintainer-app-readiness-audit.md new file mode 100644 index 000000000..54f6b875a --- /dev/null +++ b/docs/maintainer-app-readiness-audit.md @@ -0,0 +1,90 @@ +# Maintainer App pre-activation audit + +## Purpose + +Noema's hourly commercial-readiness loop can dispatch exact-head reviews and perform SHA-bound merges only through a dedicated Maintainer GitHub App. The write path remains intentionally disabled until operators can prove that the effective Maintainer token, authenticated Reviewer App identity, and live `main` governance satisfy the repository's fail-closed policy. In this document, **effective installation token** means the repository-scoped token actually minted and exercised during the audited workflow run. + +The default-branch-only `.github/workflows/maintainer-app-readiness.yml` workflow produces that machine-readable preflight evidence without dispatching a review, merging a pull request, changing repository configuration, or enabling `NOEMA_MAINTENANCE_ENABLED`. + +## Trigger + +The workflow accepts only the `maintainer-app-readiness` `repository_dispatch` event. GitHub evaluates `repository_dispatch` from the default branch, so a caller cannot select workflow code from an unreviewed branch. Checkout is pinned to `github.sha`, the event-bound default-branch commit, rather than re-resolving the moving branch name after the run starts. + +```bash +gh api repos/ContextualWisdomLab/noema/dispatches \ + --method POST \ + --input - <<'JSON' +{"event_type":"maintainer-app-readiness"} +JSON +``` + +## Required configuration + +Configure these repository values before dispatching the preflight: + +- variable `NOEMA_MAINTAINER_APP_CLIENT_ID`; +- secret `NOEMA_MAINTAINER_APP_PRIVATE_KEY`; +- variable `NOEMA_GITHUB_APP_CLIENT_ID` for the existing Reviewer App; +- secret `NOEMA_GITHUB_APP_PRIVATE_KEY` for the existing Reviewer App; +- variable `NOEMA_REVIEWER_LOGIN`, including the exact `[bot]` suffix. + +Leave `NOEMA_MAINTENANCE_ENABLED` unset or different from `true` until the preflight, independently reviewed App registration evidence, and an approved activation run all pass. The preflight verifies this state and fails with `maintenance_already_enabled` if the write path has been enabled prematurely. + +The workflow creates two separately scoped installation tokens: + +1. The **Maintainer App token** is scoped to `ContextualWisdomLab/noema` with only the effective permissions needed by the commercial loop: Actions read, Checks read, Contents write, Metadata read, Pull requests write, and Commit statuses read. It is the only token exposed to governance, API-probe, and no-write dry-run scripts as `GH_TOKEN`. +2. The **Reviewer App identity token** is scoped to the same repository with Metadata read only. Its token value is never passed to a script. Only the pinned action's authenticated `app-slug` and `installation-id` outputs are supplied to the readiness evaluator so the configured reviewer login can be bound to the actual Reviewer App credentials. + +The job-level `GITHUB_TOKEN` remains Contents read and is not a write fallback. The audited commands use only Node.js built-ins and repository scripts, so the privileged job does not run `npm ci`, install repository dependencies, execute lifecycle scripts, or perform a package-manager audit. + +Both token-mint steps are allowed to continue only so the workflow can generate bounded failure evidence. Their exact outcomes remain mandatory inputs to the final gate. A failed Maintainer mint produces governance and readiness failure reports plus a fixed, no-network `maintainer_token_unavailable` dry-run artifact; it can never be interpreted as a successful preflight. + +## Enforced checks + +`npm run operations:preflight` fails closed unless all of the following are true: + +1. automated maintenance remains disabled during the pre-activation audit; +2. the Maintainer token action reports a positive installation identifier and a valid Maintainer App slug; +3. the observed Maintainer bot login exactly equals `[bot]` and is a GitHub `Bot`; +4. the Reviewer token action reports a positive installation identifier and valid Reviewer App slug; +5. `NOEMA_REVIEWER_LOGIN` exactly equals `[bot]`, and the public user lookup returns that same GitHub `Bot` login; +6. the Maintainer and reviewer bot identities differ; +7. the effective Maintainer token can enumerate exactly one repository, `ContextualWisdomLab/noema`; +8. repository metadata reports read and scoped write access but not administrator access; +9. Actions, checks, commit statuses, pull requests, and contents read probes all succeed; +10. the retained live governance report is bound to `ContextualWisdomLab/noema`, branch `main`, and status `PASS`; +11. the existing commercial-readiness loop completes without `--apply`; +12. both pinned App token actions themselves complete successfully. + +A configured bot account by itself is not sufficient reviewer authentication. A mismatch between `NOEMA_REVIEWER_LOGIN` and the authenticated Reviewer App slug fails with `reviewer_app_login_mismatch`; a missing or malformed Reviewer App installation identity fails with `reviewer_installation_id_invalid` or `reviewer_app_slug_invalid`. + +The Maintainer installation repository endpoint is fully paginated at 100 records per page. The collector retains only bounded identity, scope, permission, probe, governance, activation-state, App-binding, and commit-binding fields. If scope is broader than the expected repository, the report retains only the effective repository count and does not persist unexpected repository names. It does not retain API response bodies, access tokens, private keys, authorization headers, or secret-derived fingerprints. GitHub CLI subprocesses are shell-free, output-bounded, pinned to `github.com`, and terminated after 20 seconds so a stalled upstream cannot consume the entire preflight window. + +The `permissions.admin` value must be explicitly present as `false`; a missing or non-boolean value remains unknown and fails closed rather than being coerced into evidence of least privilege. + +The public `GET /users/{username}` response is used only to confirm the bounded login and GitHub `Bot` account type after the configured login has already been bound to the authenticated Reviewer App slug. GitHub does not document installation `suspended_at` state in that user-profile schema, so the preflight does not infer an App installation's suspension state from a missing user field. Successful token minting and the required API probes establish that the scoped tokens are operational for the audited run; the complete installation records and suspension states remain separate administrator evidence. + +## Evidence artifacts + +Every run attempts to retain these artifacts for 90 days, including token-mint and policy failures after checkout: + +- `main-governance-audit`; +- `maintainer-app-readiness`; +- `commercial-readiness-loop-dry-run`. + +The primary JSON report is `artifacts/operations/maintainer-app-readiness.json`. It records the Maintainer App slug and installation identifier, Reviewer App slug and installation identifier, configured reviewer login, effective Maintainer repository count and exact expected scope when valid, coarse permissions, API probes, governance binding, and stable pass/failure codes. Missing artifact files are themselves workflow errors; a green preflight cannot omit its machine-readable evidence. + +## Evidence boundary + +A passing report proves the effective Maintainer installation token minted for that workflow run, the live APIs exercised with it, and that the configured reviewer bot login matches the authenticated Reviewer App slug and positive installation identifier produced from the configured Reviewer App credentials. It does **not** prove the complete underlying GitHub App registration for either App, all repositories available to either installation before token scoping, installation suspension state, private-key ownership and rotation, administrator bypass policy, or break-glass ownership. Those facts remain independently reviewed operational evidence under issue #29 and issue #27. + +A failed report must never be converted into an activation approval by weakening permissions, removing probes, substituting `GITHUB_TOKEN`, trusting an arbitrary bot login, exposing the Reviewer token to scripts, enabling maintenance before approval, or bypassing governance. Correct the external configuration and rerun the default-branch workflow. + +## Authoritative references + +- GitHub Actions `repository_dispatch`: +- GitHub App token action: +- Public user lookup schema: +- Installation repository enumeration: +- Installation token permissions and repository scoping: +- Installation record and suspension fields: diff --git a/test/maintainer-app-readiness-workflow-hardening.test.ts b/test/maintainer-app-readiness-workflow-hardening.test.ts new file mode 100644 index 000000000..ce372bf10 --- /dev/null +++ b/test/maintainer-app-readiness-workflow-hardening.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync( + ".github/workflows/maintainer-app-readiness.yml", + "utf8", +); + +describe("maintainer App readiness workflow hardening", () => { + it("invokes reviewed Node entrypoints directly without npm lifecycle hooks", () => { + expect(workflow).not.toContain("npm ci"); + expect(workflow).not.toContain("npm install"); + expect(workflow).not.toContain("npm run"); + expect(workflow).toContain("node scripts/main-governance-audit.mjs"); + expect(workflow).toContain("node scripts/maintainer-app-readiness.mjs"); + }); + + it("continues after either App token mint fails so bounded failure artifacts can be written", () => { + const maintainerStart = workflow.indexOf("mint repository-scoped Maintainer App token"); + const reviewerStart = workflow.indexOf("mint repository-scoped Reviewer App identity token"); + const setupStart = workflow.indexOf("setup Node.js"); + const maintainerBlock = workflow.slice(maintainerStart, reviewerStart); + const reviewerBlock = workflow.slice(reviewerStart, setupStart); + + expect(maintainerBlock).toContain("continue-on-error: true"); + expect(reviewerBlock).toContain("continue-on-error: true"); + expect(workflow).toContain("MAINTAINER_APP_OUTCOME: ${{ steps.maintainer_app.outcome }}"); + expect(workflow).toContain("REVIEWER_APP_OUTCOME: ${{ steps.reviewer_app.outcome }}"); + expect(workflow).toContain( + "for gate in MAINTAINER_APP_OUTCOME REVIEWER_APP_OUTCOME GOVERNANCE_OUTCOME READINESS_OUTCOME DRY_RUN_OUTCOME", + ); + }); + + it("writes bounded evidence when no Maintainer token exists or the dry-run command fails early", () => { + expect(workflow).toContain('code: reasonCode'); + expect(workflow).toContain('reasonCode="maintainer_token_unavailable"'); + expect(workflow).toContain('reasonCode="commercial_loop_failed"'); + expect(workflow).toContain( + "artifacts/operations/commercial-readiness-loop-dry-run.json", + ); + expect(workflow).toContain('if [ "$MAINTAINER_APP_OUTCOME" != "success" ]'); + expect(workflow).toContain('if [ "$loop_status" -ne 0 ] && [ ! -s "$report_path" ]'); + expect(workflow).toContain('exit "$loop_status"'); + }); +}); diff --git a/test/maintainer-app-readiness-workflow.test.ts b/test/maintainer-app-readiness-workflow.test.ts new file mode 100644 index 000000000..be0a72173 --- /dev/null +++ b/test/maintainer-app-readiness-workflow.test.ts @@ -0,0 +1,86 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync(".github/workflows/maintainer-app-readiness.yml", "utf8"); + +describe("maintainer App readiness workflow", () => { + it("runs only from an event-bound default-branch commit", () => { + expect(workflow).toContain("repository_dispatch:"); + expect(workflow).toContain("types: [maintainer-app-readiness]"); + expect(workflow).not.toContain("workflow_dispatch:"); + expect(workflow).toContain("ref: ${{ github.sha }}"); + expect(workflow).not.toContain("ref: ${{ github.event.repository.default_branch }}"); + expect(workflow).toContain("persist-credentials: false"); + }); + + it("mints separate repository-scoped Maintainer and Reviewer App tokens", () => { + expect(workflow.match(/actions\/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1/g)).toHaveLength(2); + expect(workflow).toContain("client-id: ${{ vars.NOEMA_MAINTAINER_APP_CLIENT_ID }}"); + expect(workflow).toContain("private-key: ${{ secrets.NOEMA_MAINTAINER_APP_PRIVATE_KEY }}"); + expect(workflow).toContain("client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }}"); + expect(workflow).toContain("private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }}"); + expect(workflow.match(/owner: ContextualWisdomLab/g)).toHaveLength(2); + expect(workflow.match(/repositories: noema/g)).toHaveLength(2); + expect(workflow).toContain("permission-actions: read"); + expect(workflow).toContain("permission-checks: read"); + expect(workflow).toContain("permission-contents: write"); + expect(workflow).toContain("permission-metadata: read"); + expect(workflow).toContain("permission-pull-requests: write"); + expect(workflow).toContain("permission-statuses: read"); + expect(workflow).not.toContain("permission-administration"); + + const reviewerStart = workflow.indexOf("mint repository-scoped Reviewer App identity token"); + const setupStart = workflow.indexOf("setup Node.js"); + const reviewerBlock = workflow.slice(reviewerStart, setupStart); + expect(reviewerStart).toBeGreaterThan(0); + expect(setupStart).toBeGreaterThan(reviewerStart); + expect(reviewerBlock).toContain("permission-metadata: read"); + expect(reviewerBlock).not.toContain("permission-contents: write"); + expect(reviewerBlock).not.toContain("permission-pull-requests: write"); + expect(workflow).not.toContain("GH_TOKEN: ${{ steps.reviewer_app.outputs.token }}"); + }); + + it("passes authenticated Reviewer App identity outputs to the evaluator", () => { + expect(workflow).toContain("NOEMA_REVIEWER_APP_SLUG: ${{ steps.reviewer_app.outputs.app-slug }}"); + expect(workflow).toContain("NOEMA_REVIEWER_INSTALLATION_ID: ${{ steps.reviewer_app.outputs.installation-id }}"); + expect(workflow).toContain("NOEMA_REVIEWER_LOGIN: ${{ vars.NOEMA_REVIEWER_LOGIN }}"); + }); + + it("orders gates and requires all evidence artifacts", () => { + const governance = workflow.indexOf("audit active main governance"); + const readiness = workflow.indexOf("audit effective Maintainer App identity and access"); + const dryRun = workflow.indexOf("inspect commercial-readiness loop without writes"); + const enforcement = workflow.indexOf("enforce pre-activation gates"); + + expect(governance).toBeGreaterThan(0); + expect(readiness).toBeGreaterThan(governance); + expect(dryRun).toBeGreaterThan(readiness); + expect(enforcement).toBeGreaterThan(dryRun); + expect(workflow).toContain("node scripts/maintainer-app-readiness.mjs"); + expect(workflow).toContain("NOEMA_MAINTENANCE_ENABLED: ${{ vars.NOEMA_MAINTENANCE_ENABLED }}"); + expect(workflow).toContain("scripts/hourly-commercial-readiness.mjs"); + expect(workflow).not.toContain("--apply"); + expect(workflow.match(/if-no-files-found: error/g)).toHaveLength(3); + expect(workflow).not.toContain("if-no-files-found: warn"); + expect(workflow).toContain("retention-days: 90"); + }); + + it("documents the evidence boundary and operational rollback posture", () => { + const documentation = readFileSync("docs/maintainer-app-readiness-audit.md", "utf8"); + + expect(documentation).toContain("effective installation token"); + expect(documentation).toContain("complete underlying GitHub App registration"); + expect(documentation).toContain("installation suspension state"); + expect(documentation).toContain("Reviewer App"); + expect(documentation).toContain("reviewer_app_login_mismatch"); + expect(documentation).toContain("event-bound default-branch commit"); + expect(documentation).toContain("maintenance_already_enabled"); + expect(documentation).toContain("issue #29"); + expect(documentation).toContain("issue #27"); + expect(documentation).toContain("GITHUB_TOKEN"); + expect(documentation).toContain("--apply"); + expect(documentation).toContain("100 records per page"); + expect(documentation).toContain("does not persist unexpected repository names"); + expect(documentation).toContain("does not run `npm ci`"); + }); +});