From eb97eeeebdec482c0ecae1fb972b81fe4d005511 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 10:40:58 +0200 Subject: [PATCH] chore: run RepoWise only through manual dispatch --- .github/workflows/repowise.yml | 34 +++---- docs/development/release-process.mdx | 3 +- docs/development/testing.mdx | 36 ++++--- docs/features.mdx | 2 +- docs/issues/deployment-orchestration.mdx | 2 +- .../security/self-hosted-runners.mdx | 12 ++- scripts/validate-workflow-contract.cjs | 48 +++++++--- specs/CATALOG.md | 8 +- specs/catalog.json | 6 +- specs/local-web-setup-assistant.md | 20 +++- specs/merge-queue-readiness.md | 94 ++++++++++++++++++- .../validate_workflow_contract.test.ts | 74 ++++++++++++--- 12 files changed, 254 insertions(+), 85 deletions(-) diff --git a/.github/workflows/repowise.yml b/.github/workflows/repowise.yml index 02c7efed0..938c4b642 100644 --- a/.github/workflows/repowise.yml +++ b/.github/workflows/repowise.yml @@ -1,32 +1,27 @@ name: RepoWise Code Health on: - push: - branches: [master] - pull_request: - types: [opened, synchronize, reopened] - merge_group: - types: [checks_requested] workflow_dispatch: concurrency: - group: repowise-${{ github.event.pull_request.number || github.ref }} + group: repowise-${{ github.ref }} cancel-in-progress: true permissions: contents: read +defaults: + run: + shell: bash + jobs: code-health: name: RepoWise code health - # A merge group can contain fork code without exposing the originating PR here. - runs-on: ${{ fromJSON((github.event_name == 'merge_group' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) && '["ubuntu-latest"]' || '["self-hosted","codex","Linux"]') }} + runs-on: [self-hosted, codex, Linux] timeout-minutes: 30 env: REPOWISE_VENV_NAME: repowise-venv-${{ github.run_id }}-${{ github.run_attempt }}-code-health REPOWISE_REPORT_NAME: repowise-report-${{ github.run_id }}-${{ github.run_attempt }}-code-health - REPOWISE_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }} - REPOWISE_EVENT_NAME: ${{ github.event_name }} steps: - name: Verify Ubuntu runner shell: bash @@ -66,6 +61,8 @@ jobs: report_dir="$RUNNER_TEMP/$REPOWISE_REPORT_NAME" repowise_bin="$RUNNER_TEMP/$REPOWISE_VENV_NAME/bin/repowise" mkdir -p "$report_dir" + analyzed_sha="$(git rev-parse HEAD)" + printf '%s\n' "$analyzed_sha" > "$report_dir/revision.txt" cleanup() { rm -rf .repowise .mcp.json rm -f .vscode/mcp.json .vscode/extensions.json @@ -90,23 +87,14 @@ jobs: fi } advisory_scan dead-code "$repowise_bin" dead-code --safe-only --no-workspace --format json . - if [ "$REPOWISE_EVENT_NAME" = workflow_dispatch ]; then - echo 'security scope: full reachable history (manual dispatch)' >> "$report_dir/advisory-status.txt" - advisory_scan security "$repowise_bin" security scan --history --path . --format json - elif [[ "$REPOWISE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]] \ - && git cat-file -e "$REPOWISE_BASE_SHA^{commit}" \ - && git merge-base --is-ancestor "$REPOWISE_BASE_SHA" HEAD; then - printf 'security scope: %s..%s\n' "$REPOWISE_BASE_SHA" "$(git rev-parse HEAD)" >> "$report_dir/advisory-status.txt" - advisory_scan security "$repowise_bin" security scan --history --since "$REPOWISE_BASE_SHA" --to HEAD --path . --format json - else - echo 'security: incomplete (event base unavailable or not an ancestor)' >> "$report_dir/advisory-status.txt" - echo '::warning::RepoWise security advisory scope could not be verified; no full-history fallback was attempted.' - fi + echo 'security scope: full reachable history (manual dispatch)' >> "$report_dir/advisory-status.txt" + advisory_scan security "$repowise_bin" security scan --history --path . --format json { echo '## RepoWise code health' echo echo 'Advisory reports are retained as workflow artifacts for this commit.' echo + printf 'Analyzed revision: `%s`\n\n' "$analyzed_sha" sed -n '1,100p' "$report_dir/health.md" echo echo '### Advisory scan status' diff --git a/docs/development/release-process.mdx b/docs/development/release-process.mdx index c13dc9b4d..b2660e44b 100644 --- a/docs/development/release-process.mdx +++ b/docs/development/release-process.mdx @@ -36,7 +36,8 @@ prepare release -> production PR -> accepted production SHA For unattended completion, enable repository auto-merge and active required validation on both production and development targets. This repository requires -GitHub Actions `CI Check` on `master` and `develop`; RepoWise is advisory. +GitHub Actions `CI Check` on `master` and `develop`; RepoWise is optional and +runs only through manual dispatch. Default `auto` mode enables native auto-merge while a managed PR must wait. When GitHub already reports the PR ready, Copilot requests a normal merge pinned to its verified head SHA. This also handles pending advisory checks, without diff --git a/docs/development/testing.mdx b/docs/development/testing.mdx index 157088acf..834e23dd4 100644 --- a/docs/development/testing.mdx +++ b/docs/development/testing.mdx @@ -104,20 +104,28 @@ local topology before an architecture review with Graphify, and use the RepoWise index-only health/dead-code reports to distinguish current complexity from history-based churn signals: -The required `RepoWise code health` workflow publishes its index and health -reports. Its dead-code and history-security scans have separate time and memory limits; -the Job Summary and artifact status identify an incomplete advisory scan -instead of claiming that it passed. -For PRs, pushes and merge groups, history-security scans cover the native event -base through the checked-out HEAD, including commits that introduced and then -removed a finding. The base must be a valid commit and an ancestor of HEAD; -missing or unrelated evidence produces an incomplete advisory result. The -artifact records both immutable bounds. Manual dispatch scans the complete -reachable history within the same limits; a successful event-range scan is -not evidence of a complete historical audit. -Repository-owned PRs, pushes and merge groups run RepoWise and `CI Check` on -self-hosted Ubuntu `codex` runners. Fork PRs use GitHub-hosted Ubuntu for -these checks. The automatic setup fixture matrix remains hosted on three +`RepoWise Code Health` is an optional manual workflow. PRs, pushes and merge +groups do not launch it, and it must not be a required branch check. To run an +analysis, open **Actions → RepoWise Code Health → Run workflow**, select a +reviewed repository branch, and launch it. The CLI equivalent for development is +`gh workflow run repowise.yml --ref develop`. The workflow already exists on the +default branch, as required for [manual dispatch](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow). + +Open the run's Job Summary for the analyzed commit and scan status. Download +the `repowise-code-health` artifact for the index log, Markdown/JSON health +reports, `revision.txt` and `advisory-status.txt`; artifacts are retained for +14 days. History security scans cover the complete reachable history of that +revision. Dead-code and security scans each have a 90-second, 2 GiB limit +inside the 30-minute job budget. An incomplete scan produces a warning and an +incomplete status while preserving completed health reports. A failed health +step fails the run; available files are still uploaded. Inspect the failure +before retrying. A newer manual run for the same ref cancels the older one; +cancellation or runner loss can prevent artifact publication. No manual run +blocks the automatic CI path. + +Manual RepoWise runs use self-hosted Ubuntu `codex` runners. `CI Check` uses +that pool for repository-owned PRs and pushes; fork PRs and all merge groups +use disposable GitHub-hosted Ubuntu runners. The automatic setup fixture matrix remains hosted on three operating systems, with service-runner fixtures available by manual dispatch. Runner routing is contract-checked, but actual runner availability and remote results require evidence from the corresponding Actions runs. diff --git a/docs/features.mdx b/docs/features.mdx index 9f950a751..2af9d6035 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -221,7 +221,7 @@ jobs: project-ids: '2,3' ``` -Durable mutation workflows deliberately do not use a GitHub Actions `concurrency` block: native cancellation retains only one pending run and can discard intermediate issue, comment, or release mutations. The Commit and Pull Request templates are the deliberate exception for their own replaceable branch revisions, but they use distinct groups and cannot cancel one another. Native concurrency also remains appropriate for replaceable read-only workflows such as CI and RepoWise. +Durable mutation workflows deliberately do not use a GitHub Actions `concurrency` block: native cancellation retains only one pending run and can discard intermediate issue, comment, or release mutations. The Commit and Pull Request templates are the deliberate exception for their own replaceable branch revisions, but they use distinct groups and cannot cancel one another. Native concurrency also remains appropriate for replaceable read-only workflows such as CI and the optional, manually dispatched RepoWise analysis. --- diff --git a/docs/issues/deployment-orchestration.mdx b/docs/issues/deployment-orchestration.mdx index ba7f71e26..6c4e06fae 100644 --- a/docs/issues/deployment-orchestration.mdx +++ b/docs/issues/deployment-orchestration.mdx @@ -58,7 +58,7 @@ Complete this once before starting the first production-first operation: ancestry relationship; - activate required validation on every promotion/reconciliation target. This project requires GitHub Actions `CI Check` on both `master` and `develop`; - RepoWise remains advisory; + RepoWise is optional and runs only through manual dispatch; - enable repository auto-merge if selecting `auto-merge`, or configure every required check for `merge_group` if the target requires a merge queue, and add an exact attestation only for a producer Copilot cannot inspect; diff --git a/docs/security-operations/security/self-hosted-runners.mdx b/docs/security-operations/security/self-hosted-runners.mdx index f2925d0e4..3130652bd 100644 --- a/docs/security-operations/security/self-hosted-runners.mdx +++ b/docs/security-operations/security/self-hosted-runners.mdx @@ -22,9 +22,8 @@ package-manager caching. This keeps a successful full validation from timing out in the post-job archive step; hosted fixture caching is retained. See the [setup-node cache inputs](https://github.com/actions/setup-node#usage). -This repository runs `CI Check` and `RepoWise code health` on self-hosted -Ubuntu `codex` runners for its own PRs and pushes. Fork PRs and all merge -groups keep the same quality checks on disposable GitHub-hosted Ubuntu +This repository runs `CI Check` on self-hosted Ubuntu `codex` runners for its +own PRs and pushes. Fork PRs and all merge groups run CI on disposable GitHub-hosted Ubuntu runners. A merge group can contain fork code without exposing its origin to the runner selector; never route it to a persistent service runner. The trusted-default-branch PR approval observer also runs on a self-hosted Ubuntu `codex` runner. The @@ -48,7 +47,12 @@ self-hosted runners. RepoWise obtains a complete Python 3.12 distribution via `actions/setup-python`, creates its virtual environment and reports in run-specific temporary directories, and removes them after artifact upload. Its runner needs a writable Actions tool cache; the workflow does not install -Ubuntu system packages. +Ubuntu system packages. RepoWise runs only through manual dispatch on +`[self-hosted, codex, Linux]`, using the reviewed repository ref selected by the +maintainer. It is optional and must not be required by branch protections or +rulesets. It no longer consumes this pool on every PR or push. See +[testing and manual analysis](/development/testing) for launch instructions, +revision evidence, scan limits, artifacts, and retry behavior. The `[self-hosted, codex]` pool routes agent jobs to macOS and Ubuntu. Windows services MUST omit `codex` on registration; recreate existing registrations to diff --git a/scripts/validate-workflow-contract.cjs b/scripts/validate-workflow-contract.cjs index 096a9f50e..06f2e5447 100644 --- a/scripts/validate-workflow-contract.cjs +++ b/scripts/validate-workflow-contract.cjs @@ -259,15 +259,38 @@ function assertRepoWiseTemporaryCleanup(relativeFile, job) { throw new Error(`${relativeFile} must select complete Python, isolate RepoWise files, and remove them after artifact upload.`); } const reports = (job.steps ?? []).find(step => step?.name === 'Generate RepoWise reports'); - if (job.env?.REPOWISE_BASE_SHA !== "${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }}" - || job.env?.REPOWISE_EVENT_NAME !== '${{ github.event_name }}' - || !reports?.run?.includes('[[ "$REPOWISE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]') - || !reports.run.includes('git cat-file -e "$REPOWISE_BASE_SHA^{commit}"') - || !reports.run.includes('git merge-base --is-ancestor "$REPOWISE_BASE_SHA" HEAD') - || !reports.run.includes('--since "$REPOWISE_BASE_SHA" --to HEAD') - || !reports.run.includes('[ "$REPOWISE_EVENT_NAME" = workflow_dispatch ]') - || !reports.run.includes('security: incomplete (event base unavailable or not an ancestor)')) { - throw new Error(`${relativeFile} must verify and report event-scoped security history; only manual dispatch may scan full history.`); + if (!reports?.run?.includes('advisory_scan security "$repowise_bin" security scan --history --path . --format json') + || !reports.run.includes('security scope: full reachable history (manual dispatch)') + || !reports.run.includes('ulimit -v 2097152; timeout --kill-after=10s 90s') + || !reports.run.includes('incomplete (exit %s)') + || !reports.run.includes('health --no-workspace --refactoring-targets --format md') + || !reports.run.includes('health --no-workspace --refactoring-targets --format json') + || !reports.run.includes('analyzed_sha="$(git rev-parse HEAD)"') + || !reports.run.includes('printf \'%s\\n\' "$analyzed_sha" > "$report_dir/revision.txt"') + || !reports.run.includes('printf \'Analyzed revision: `%s`\\n\\n\' "$analyzed_sha"') + || !reports.run.includes('>> "$GITHUB_STEP_SUMMARY"') + || upload.if !== '${{ always() }}' + || upload.with?.['retention-days'] !== 14) { + throw new Error(`${relativeFile} must preserve bounded manual history scans, health reports, revision evidence, summary and unconditional artifacts.`); + } +} + +function assertManualRepoWiseWorkflow(file, workflow) { + const relativeFile = relativeWorkflow(file); + if (relativeFile !== '.github/workflows/repowise.yml') return; + if (Object.keys(workflow.on ?? {}).join(',') !== 'workflow_dispatch') { + throw new Error(`${relativeFile} must run only through workflow_dispatch.`); + } + const job = workflow.jobs?.['code-health']; + const checkout = (job?.steps ?? []).find(step => step?.uses === CHECKOUT_ACTION); + if (job?.['timeout-minutes'] !== 30 + || JSON.stringify(workflow.permissions) !== JSON.stringify({ contents: 'read' }) + || job.permissions !== undefined + || checkout?.with?.['fetch-depth'] !== 0 + || checkout.with?.ref !== undefined + || workflow.concurrency?.group !== 'repowise-${{ github.ref }}' + || workflow.concurrency?.['cancel-in-progress'] !== true) { + throw new Error(`${relativeFile} must retain its manual job budget, read-only permissions, selected revision and per-ref concurrency.`); } } @@ -451,8 +474,9 @@ function assertRunner(file, workflow) { } else if (relativeFile === '.github/workflows/copilot_runner_diagnostics.yml') { if (jobId !== 'linux') throw new Error(`${relativeFile} contains an unsupported Codex diagnostic job.`); expected = ['self-hosted', 'codex', 'Linux']; - } else if (relativeFile === '.github/workflows/ci_check.yml' - || relativeFile === '.github/workflows/repowise.yml') { + } else if (relativeFile === '.github/workflows/repowise.yml') { + expected = ['self-hosted', 'codex', 'Linux']; + } else if (relativeFile === '.github/workflows/ci_check.yml') { expected = [QUALITY_RUNNER_BY_TRUST]; } const labels = runnerLabels(job['runs-on']); @@ -1233,7 +1257,6 @@ function assertMergeQueueWorkflowSupport(file, workflow) { const relativeFile = relativeWorkflow(file); const required = new Set([ '.github/workflows/ci_check.yml', - '.github/workflows/repowise.yml', '.github/workflows/copilot_pull_request_merge_queue.yml', 'setup/workflows/copilot_pull_request_merge_queue.yml', ]); @@ -1379,6 +1402,7 @@ function assertIssueBranchConfigurationInputs(file, workflow) { function validateWorkflow(file, workflow) { if (!workflow || typeof workflow !== 'object') throw new Error('workflow document is empty.'); + assertManualRepoWiseWorkflow(file, workflow); assertDirectEventTriggers(file, workflow); assertApprovalObserverWorkflow(file, workflow); assertPullRequestMergeQueueWorkflow(file, workflow); diff --git a/specs/CATALOG.md b/specs/CATALOG.md index b5608fbec..1cabdc39b 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -12,12 +12,12 @@ debt or convert unknown historic intent into a design decision. |---|---|---|---|---| | `github-communication-experience` | Implemented | English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces | [Semantic GitHub communication and repository localization](./semantic-github-publication-and-notification.md) + 3 companion | 277 paths · 2026-10-20 | | `release-orchestration` | Implemented | Release and hotfix promotion, publication, bounded merge readiness recovery, and receipt-verified reconciliation without republication | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 59 paths · 2026-10-16 | -| `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 24 paths · 2026-10-15 | +| `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support; optional manual-only RepoWise analysis with independent CI and verified required-check migration | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 24 paths · 2026-10-21 | | `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 59 paths · 2026-10-11 | | `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 97 paths · 2026-10-14 | | `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-10-13 | | `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 5 companion | 326 paths · 2026-10-20 | -| `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 1 companion | 238 paths · 2026-10-20 | +| `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 1 companion | 238 paths · 2026-10-21 | | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 36 paths · 2026-10-18 | | `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 116 paths · 2026-10-19 | @@ -59,7 +59,7 @@ debt or convert unknown historic intent into a design decision. ### `merge-queue-readiness` — Merge queue readiness and effective target rules - Owner: Copilot maintainers -- Last verified: 2026-10-15 +- Last verified: 2026-10-21 - Specifications: [`specs/merge-queue-readiness.md`](./merge-queue-readiness.md) - Workflows: [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) · [`.github/workflows/repowise.yml`](../.github/workflows/repowise.yml) · [`.github/workflows/copilot_pull_request_merge_queue.yml`](../.github/workflows/copilot_pull_request_merge_queue.yml) · [`setup/workflows/copilot_pull_request_merge_queue.yml`](../setup/workflows/copilot_pull_request_merge_queue.yml) · [`.github/workflows/copilot_deployment_orchestration.yml`](../.github/workflows/copilot_deployment_orchestration.yml) - Entrypoints: [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) @@ -114,7 +114,7 @@ debt or convert unknown historic intent into a design decision. ### `local-web-setup-assistant` — Local web setup assistant - Owner: Copilot maintainers -- Last verified: 2026-10-20 +- Last verified: 2026-10-21 - Specifications: [`specs/local-web-setup-assistant.md`](./local-web-setup-assistant.md) · [`specs/setup-configuration-management.md`](./setup-configuration-management.md) - Workflows: [`.github/workflows/ci_check.yml`](../.github/workflows/ci_check.yml) · [`.github/workflows/repowise.yml`](../.github/workflows/repowise.yml) · [`.github/workflows/setup_platform_smoke.yml`](../.github/workflows/setup_platform_smoke.yml) · [`.github/workflows/copilot_pull_request_approval.yml`](../.github/workflows/copilot_pull_request_approval.yml) - Entrypoints: [`src/cli/commands/setup.ts`](../src/cli/commands/setup.ts) · [`package.json`](../package.json) · [`web/src/main.ts`](../web/src/main.ts) diff --git a/specs/catalog.json b/specs/catalog.json index 73491e557..5aa5def10 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -388,9 +388,9 @@ "id": "merge-queue-readiness", "title": "Merge queue readiness and effective target rules", "status": "implemented", - "scope": "Fail-closed validation of required checks and merge-group workflow support", + "scope": "Fail-closed validation of required checks and merge-group workflow support; optional manual-only RepoWise analysis with independent CI and verified required-check migration", "owner": "Copilot maintainers", - "lastVerified": "2026-10-15", + "lastVerified": "2026-10-21", "specs": [ "specs/merge-queue-readiness.md" ], @@ -1082,7 +1082,7 @@ "status": "proposed", "scope": "Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification", "owner": "Copilot maintainers", - "lastVerified": "2026-10-20", + "lastVerified": "2026-10-21", "specs": [ "specs/local-web-setup-assistant.md", "specs/setup-configuration-management.md" diff --git a/specs/local-web-setup-assistant.md b/specs/local-web-setup-assistant.md index ef167c155..72f8a0840 100644 --- a/specs/local-web-setup-assistant.md +++ b/specs/local-web-setup-assistant.md @@ -2415,8 +2415,9 @@ called secure. That external policy review remains an open security gate. The 2026-10-05 runner-cost decision supersedes only the blanket hosted-runner requirement for the repository's `CI Check` and `RepoWise code health` jobs. -Both MUST use an Ubuntu self-hosted `codex` runner for repository-owned PRs, -pushes, and manual runs. A fork PR or any merge-group event MUST instead use +As amended by issue #639 on 2026-10-21, `CI Check` MUST use an Ubuntu +self-hosted `codex` runner for repository-owned PRs, pushes, and manual runs. +A fork PR or any merge-group event MUST instead use disposable `ubuntu-latest`: a merge group can contain fork code but its event does not expose the original PR trust classification to the runner selector. The quality checks still run without executing that code on a persistent @@ -2445,6 +2446,16 @@ exception. A fresh run on each selected runner type is required before calling the routing operationally verified; static workflow checks alone do not prove runner availability or the outcome of remote jobs. +Issue #639 supersedes RepoWise's automatic routing cases above. RepoWise MUST +now run only under `workflow_dispatch` on Ubuntu `codex` with the native selected +repository ref; it MUST NOT run on PR, push or merge-group events or be required +for them. The retained CI routing cases still prove the fork/merge-group trust +boundary. The [manual-only migration contract](./merge-queue-readiness.md#manual-only-repowise-migration-2026-10-21-issue-639) +defines the 24-case regression floor, live required-check evidence, immutable +revision reporting, unchanged scan budgets and cleanup, operator journey, +failure/retry behavior and final-revision manual acceptance. This is a workflow +scheduling change with no setup-session, PAT, web UX or new configuration change. + The first [RepoWise run on `e197cb0c`](https://github.com/vypdev/copilot/actions/runs/37289899480) failed workflow validation before any job started. Its job-level `env` used `runner.temp`, which GitHub's [context availability table](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability) @@ -2613,8 +2624,9 @@ pass; the prior manual result does not substitute for it. recovery, and PAT cleanup/renewal guidance are linked and validated. - [ ] Build, lint, typecheck, coverage, architecture, workflow, package, documentation, catalog generation, and `validate:specifications` pass. -- [ ] Repository-owned CI and RepoWise run on self-hosted Ubuntu, fork PRs on - hosted Ubuntu, and the approval observer on self-hosted `codex`. +- [ ] Repository-owned CI and manual-only RepoWise run on self-hosted Ubuntu; + CI for fork PRs/merge groups runs on hosted Ubuntu, and the approval + observer runs on self-hosted `codex`. The self-hosted macOS, Ubuntu and Windows fixture matrix is manual only. npm publication remains hosted for Trusted Publishing. All six platform jobs pass their full fixture, package, coverage and validator contracts; diff --git a/specs/merge-queue-readiness.md b/specs/merge-queue-readiness.md index 3506bf285..5cdc3f774 100644 --- a/specs/merge-queue-readiness.md +++ b/specs/merge-queue-readiness.md @@ -4,7 +4,7 @@ complete; clean-tree bundle validation and controlled live queue validation remain external completion gates - Date: 2026-09-10 -- Last updated: 2026-09-15 +- Last updated: 2026-10-21 - Owners: `vypdev/copilot` product and engineering maintainers - Scope: discover the effective merge policy of every deployment target and prevent a managed pull request from entering an unusable merge queue. @@ -89,7 +89,11 @@ longer exhibit these behaviors. 6. Enqueueing does not pass GraphQL `expectedHeadOid`, and queue membership is not read before a retry. -### 2.3 Live repository evidence captured on 2026-09-10 +### 2.3 Historical repository evidence captured on 2026-09-10 + +The RepoWise required-check and automatic-trigger facts below are historical. +The manual-only migration below supersedes them; queue readiness still inspects +live effective rules rather than assuming that this snapshot remains current. - `GET /repos/vypdev/copilot/branches/master/protection` returned `404 Branch not protected` because no classic protection rule applies. @@ -116,6 +120,9 @@ longer exhibit these behaviors. ### Event-scoped advisory history correction (2026-10-14) +This event-range contract is retained as historical evidence. The manual-only +migration below retires the automatic event-base path. + Dogfooding [#496](https://github.com/vypdev/copilot/issues/496) reproduced a RepoWise 0.45.0 `MemoryError` while batch-reading all historical blobs in [run 38472995406](https://github.com/vypdev/copilot/actions/runs/38472995406). @@ -151,6 +158,88 @@ historical audit. Run workflow/specification/documentation validators and the workflow regression suite; no coverage threshold is reduced. A fresh exact-head run must prove the producer reaches artifact upload and reports any incomplete advisory scan honestly; it is still an open gate. + +### Manual-only RepoWise migration (2026-10-21, issue #639) + +RepoWise MUST run only through `workflow_dispatch`. PRs, pushes, merge groups, +schedules and reusable-workflow calls MUST NOT launch it. CI and merge queue +MUST complete without a RepoWise result. This supersedes the earlier required +RepoWise producer and event-range scanning contract, without changing runtime +queue-readiness policy or any consumer's required checks. + +Live GitHub inspection on 2026-10-21 found active repository ruleset +`25205979` (`Release automation required CI`) requiring only GitHub Actions +`CI Check` on both `master` and `develop`. Classic protection on `master` is +absent (404); classic protection on `develop` also requires only `CI Check`. +The old `Master Push Check` and `Develop Push Check` rulesets are disabled. +RepoWise therefore needs no live protection change for this migration. Operators +MUST recheck effective rules before rollout; if RepoWise becomes required again, +remove only that requirement before retiring automatic triggers, preserving all +other checks. A manual-only required producer remains unsupported by the existing +fail-closed merge-queue preflight; no attestation may bypass that known fact. + +```text +Maintainer -> Actions / RepoWise Code Health / Run workflow -> select reviewed ref + -> bounded analysis -> commit-bound Job Summary and report artifact +PR / push / merge group -> normal CI -> no RepoWise dependency +``` + +Text equivalent: maintainers explicitly select a reviewed repository branch and +inspect that run's summary and artifact; automatic change validation does not +start or await RepoWise. The native selector uses the repository default branch +unless another ref is selected. No new input, secret, runtime option or durable +application state is introduced. Dispatch is available because the workflow +already exists on the default branch. CLI example: +`gh workflow run repowise.yml --ref develop`. + +The workflow owns event admission, Ubuntu `codex` runner selection, read-only +contents permission and a 30-minute job budget. Checkout MUST retain the selected +revision and full history without credentials. Each advisory subprocess retains +its 90-second and 2 GiB limits; manual security scanning covers full reachable +history. Per-ref `repowise-` concurrency cancels an older manual run for the same +ref without sharing CI's group. Python environments and reports remain isolated +by run/attempt, with unconditional upload followed by cleanup. No domain, use +case, port, provider DTO or composition change is needed: the repository workflow +is the adapter boundary, enforced by the existing parsed-YAML contract validator. + +| Visible state | Meaning and next action | Retained evidence | +|---|---|---| +| Queued/running | Analysis requested; open this Actions run, no automatic CI wait | Native run/ref identity | +| Complete | Read `Analyzed revision: ` and download `repowise-code-health` | `revision.txt`, index log, health Markdown/JSON, advisory status; 14 days | +| Partial | `security: incomplete (exit )`; inspect the warning before retrying | Completed health reports remain; no complete-audit claim | +| Failed | A core setup/index/health step failed; inspect logs and retry after correction | Available files upload even after failure | +| Canceled | A newer run for the same ref replaced this one, or an operator canceled it | Native cancellation state; runner loss/cancellation can prevent upload | + +The existing English Job Summary adds only the immutable commit SHA; it does not +render an untrusted branch name. Native GitHub status and artifact links provide +navigation in narrow/light/dark views. There are no new bot comments or locale +inputs. Restoring automatic execution would require coordinated workflow, +contract, documentation and required-check review; restoring only a required +RepoWise rule would block change validation. + +Regression floor: **24 distinct workflow-contract cases**: one valid manual +dispatch, five forbidden automatic/reusable triggers, one missing dispatch, +seven budget/permission/checkout/concurrency mutations, one Ubuntu runner +selection, and nine scan/report/revision/summary/artifact mutations. Retain the +existing Python-isolation, cleanup and incomplete-scan cases and all repository +coverage thresholds. These deterministic YAML mutations cover admission, +execution bounds and reporting; runtime policy, localization and new provider +adapter tests are not applicable because those contracts do not change. + +Acceptance and traceability: + +| Observable requirement | Evidence | Documentation | +|---|---|---| +| Automatic events do not launch RepoWise | Parsed trigger mutations plus final-head PR run inventory | `docs/development/testing.mdx` | +| Manual selected-revision reports remain useful and bounded | Execution/report mutations plus final-head manual run and artifact | testing and self-hosted runner guides | +| CI/merge queue can complete independently | Live rules above, retained merge-group contracts and final-head CI | deployment and release-process guides | + +Run workflow regressions, workflow/documentation/specification validators, and +Graphify update. Completion requires a real manual run on the final revision and +review of its summary/artifact; a queued run or static test is insufficient. +Catalog evidence and the runner appendix in the setup-assistant SDD MUST agree. + +### Other merge-queue evidence from the original implementation - Copilot's own required PR context is produced for merge groups by the dedicated `copilot_pull_request_merge_queue.yml` workflow. Keeping that lightweight producer separate avoids a skipped duplicate check on every @@ -549,7 +638,6 @@ Queue rule: required Producer Source Evidence CI Check GitHub Actions Verified · ci_check.yml handles merge_group -RepoWise code health GitHub Actions Verified · repowise.yml handles merge_group No action required. Rerun `copilot doctor` after changing rules, workflows, or attestations and before the next release. diff --git a/src/tooling/__tests__/validate_workflow_contract.test.ts b/src/tooling/__tests__/validate_workflow_contract.test.ts index 6d4873010..2a6329c37 100644 --- a/src/tooling/__tests__/validate_workflow_contract.test.ts +++ b/src/tooling/__tests__/validate_workflow_contract.test.ts @@ -494,7 +494,6 @@ describe('workflow contract validator', () => { it.each([ '.github/workflows/ci_check.yml', - '.github/workflows/repowise.yml', '.github/workflows/copilot_pull_request_merge_queue.yml', 'setup/workflows/copilot_pull_request_merge_queue.yml', ])('requires merge-group checks in %s', (relativeFile) => { @@ -985,7 +984,6 @@ describe('workflow contract validator', () => { ['setup/workflows', 'copilot_credential_health.yml'], ['setup/workflows', 'copilot_issue.yml'], ['.github/workflows', 'ci_check.yml'], - ['.github/workflows', 'repowise.yml'], ])('prevents grouped routing from overriding the hosted/trust role in %s/%s', (directory, name) => { const { file, workflow } = loadMutationWorkflow(directory, name); Object.values(workflow.jobs)[0]['runs-on'] = { group: 'vypdev', labels: ['self-hosted', 'codex', 'Linux'] }; @@ -1270,9 +1268,6 @@ describe('workflow contract validator', () => { ['ci_check.yml', 'ci-check', 'pull_request', 'vypdev/copilot', '["self-hosted","codex","Linux"]'], ['ci_check.yml', 'ci-check', 'pull_request', 'outside/fork', '["ubuntu-latest"]'], ['ci_check.yml', 'ci-check', 'merge_group', 'vypdev/copilot', '["ubuntu-latest"]'], - ['repowise.yml', 'code-health', 'pull_request', 'vypdev/copilot', '["self-hosted","codex","Linux"]'], - ['repowise.yml', 'code-health', 'pull_request', 'outside/fork', '["ubuntu-latest"]'], - ['repowise.yml', 'code-health', 'merge_group', 'vypdev/copilot', '["ubuntu-latest"]'], ])('routes %s %s code from %s to %s', (fileName, jobId, eventName, headRepository, expectedRunner) => { const file = path.join(process.cwd(), '.github', 'workflows', fileName); const workflow = yaml.load(readFileSync(file, 'utf8')) as MutationWorkflow; @@ -1423,7 +1418,7 @@ describe('workflow contract validator', () => { } }); - it('bounds RepoWise advisory scans and preserves required health artifacts', () => { + it('bounds RepoWise advisory scans and preserves manual health artifacts', () => { const file = path.join(process.cwd(), '.github', 'workflows', 'repowise.yml'); const workflow = yaml.load(readFileSync(file, 'utf8')) as MutationWorkflow; const steps = workflow.jobs['code-health'].steps; @@ -1436,18 +1431,67 @@ describe('workflow contract validator', () => { expect(upload.if).toBe('${{ always() }}'); }); - it.each(['event-base', 'ancestry', 'range', 'manual-only'] as const)( - 'rejects loss of the RepoWise history scope boundary: %s', (boundary) => { - const file = path.join(process.cwd(), '.github', 'workflows', 'repowise.yml'); - const workflow = yaml.load(readFileSync(file, 'utf8')) as MutationWorkflow; + it('accepts RepoWise manual dispatch with native branch selection and isolated concurrency', () => { + const { file, workflow } = loadMutationWorkflow('.github/workflows', 'repowise.yml'); + expect(workflow.on).toEqual({ workflow_dispatch: null }); + expect(workflow.concurrency).toEqual({ group: 'repowise-${{ github.ref }}', 'cancel-in-progress': true }); + expect(() => validateWorkflow(file, workflow)).not.toThrow(); + }); + + it.each(['push', 'pull_request', 'merge_group', 'workflow_call', 'schedule'])( + 'rejects the RepoWise automatic or reusable trigger %s', event => { + const { file, workflow } = loadMutationWorkflow('.github/workflows', 'repowise.yml'); + workflow.on[event] = event === 'schedule' ? [{ cron: '0 0 * * *' }] : {}; + expect(() => validateWorkflow(file, workflow)).toThrow('must run only through workflow_dispatch'); + }, + ); + + it('rejects removal of RepoWise manual dispatch', () => { + const { file, workflow } = loadMutationWorkflow('.github/workflows', 'repowise.yml'); + delete workflow.on.workflow_dispatch; + expect(() => validateWorkflow(file, workflow)).toThrow('must run only through workflow_dispatch'); + }); + + it.each(['timeout', 'permissions', 'job-permissions', 'history', 'selected-ref', 'concurrency', 'cancellation'])( + 'preserves the RepoWise manual execution boundary: %s', boundary => { + const { file, workflow } = loadMutationWorkflow('.github/workflows', 'repowise.yml'); + const job = workflow.jobs['code-health']; + const checkout = job.steps.find((step: { uses?: string }) => step.uses === 'actions/checkout@v5'); + if (boundary === 'timeout') job['timeout-minutes'] = 60; + else if (boundary === 'permissions') workflow.permissions.contents = 'write'; + else if (boundary === 'job-permissions') job.permissions = { contents: 'write' }; + else if (boundary === 'history') checkout.with['fetch-depth'] = 1; + else if (boundary === 'selected-ref') checkout.with.ref = 'master'; + else if (boundary === 'concurrency') workflow.concurrency.group = 'ci-${{ github.ref }}'; + else workflow.concurrency['cancel-in-progress'] = false; + expect(() => validateWorkflow(file, workflow)).toThrow('manual job budget, read-only permissions, selected revision and per-ref concurrency'); + }, + ); + + it('keeps RepoWise manual runs on Ubuntu codex runners', () => { + const { file, workflow } = loadMutationWorkflow('.github/workflows', 'repowise.yml'); + expect(workflow.jobs['code-health']['runs-on']).toEqual(['self-hosted', 'codex', 'Linux']); + expect(() => validateWorkflow(file, workflow)).not.toThrow(); + workflow.jobs['code-health']['runs-on'] = ['ubuntu-latest']; + expect(() => assertRunner(file, workflow)).toThrow('must use runs-on self-hosted, codex, Linux'); + }); + + it.each(['full-history', 'scan-budget', 'incomplete-status', 'revision-file', 'summary', 'upload', 'retention', 'health-md', 'health-json'])( + 'rejects loss of RepoWise manual report evidence: %s', boundary => { + const { file, workflow } = loadMutationWorkflow('.github/workflows', 'repowise.yml'); expect(() => validateWorkflow(file, workflow)).not.toThrow(); const job = workflow.jobs['code-health']; const reports = job.steps.find((step: { name?: string }) => step.name === 'Generate RepoWise reports'); - if (boundary === 'event-base') job.env.REPOWISE_BASE_SHA = '${{ github.sha }}'; - else if (boundary === 'ancestry') reports.run = reports.run.replace('git merge-base --is-ancestor "$REPOWISE_BASE_SHA" HEAD', 'true'); - else if (boundary === 'range') reports.run = reports.run.replace('--since "$REPOWISE_BASE_SHA" --to HEAD', ''); - else reports.run = reports.run.replace('[ "$REPOWISE_EVENT_NAME" = workflow_dispatch ]', 'true'); - expect(() => validateWorkflow(file, workflow)).toThrow('event-scoped security history'); + const upload = job.steps.find((step: { name?: string }) => step.name === 'Upload RepoWise reports'); + if (boundary === 'full-history') reports.run = reports.run.replace('--history --path .', '--history --since HEAD --path .'); + else if (boundary === 'scan-budget') reports.run = reports.run.replace('ulimit -v 2097152;', ''); + else if (boundary === 'incomplete-status') reports.run = reports.run.replace('incomplete (exit %s)', 'complete'); + else if (boundary === 'revision-file') reports.run = reports.run.replace('> "$report_dir/revision.txt"', '> /dev/null'); + else if (boundary === 'summary') reports.run = reports.run.replace('>> "$GITHUB_STEP_SUMMARY"', '> /dev/null'); + else if (boundary === 'upload') delete upload.if; + else if (boundary === 'retention') upload.with['retention-days'] = 1; + else reports.run = reports.run.replace(`health --no-workspace --refactoring-targets --format ${boundary === 'health-md' ? 'md' : 'json'}`, 'status'); + expect(() => validateWorkflow(file, workflow)).toThrow('bounded manual history scans, health reports, revision evidence'); }, );