diff --git a/.coderabbit.yaml b/.coderabbit.yaml index bd4daaab801..04c111621fe 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,7 +1,8 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # CodeRabbit is the first-pass reviewer for every PR, including drafts and -# fork PRs. CircleCI pr-check runs the builds and tests (fork PRs need the -# ci:run label, see .github/workflows/fork-ci-mirror.yml). UI proof on a real +# fork PRs. CircleCI pr-check runs the builds and tests. Fork PRs are checked +# with main's -Slice ci steps in an isolated, credential-free Windows +# environment (.github/CI.md). UI proof on a real # Windows build stays a human or agent responsibility (AGENTS.md). language: en-US reviews: diff --git a/.github/CI.md b/.github/CI.md index 2b05fa06874..a32d4080777 100644 --- a/.github/CI.md +++ b/.github/CI.md @@ -21,11 +21,13 @@ credential. The hosted PR check delegates to scripts/local-check.ps1 -Slice ci: ~~~powershell +powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File scripts\circleci-pr.tests.ps1 cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace pnpm --dir apps/desktop-tauri install --frozen-lockfile pnpm --dir apps/desktop-tauri run lint +pnpm --dir apps/desktop-tauri run check:anti-slop pnpm --dir apps/desktop-tauri run test:anti-slop pnpm --dir apps/desktop-tauri test pnpm --dir apps/desktop-tauri run build @@ -38,22 +40,23 @@ second tag trigger. ## Fork pull requests -CircleCI GitHub App pipelines never fire for pull requests from forks. To -build one: - -1. Read the fork PR's diff. The build runs the contributor's code on CircleCI - with this project's environment variables. -2. Add the `ci:run` label. .github/workflows/fork-ci-mirror.yml pushes - exactly that head commit to ci/pr- in this repository. -3. CircleCI builds the ci/pr- push. The trigger gate in - scripts/circleci-pr-common.ps1 lets those branches through, and the - docs-only gate finds the PR base from the number. The check lands on the - same commit, so it shows on the fork PR. - -Any new push to the fork PR removes the label; re-add it after reviewing the -new commits. Closing the PR removes the mirror branch. GITHUB_TOKEN cannot -push commits that change .github/workflows/**, so the mirror fails for those -PRs; review them by hand and use the manual Blacksmith workflow if needed. +CircleCI GitHub App pipelines never fire for pull requests from forks, and +this project does not trigger hosted CircleCI runs any other way (no API +token). Validate a fork PR locally instead: + +1. Read the whole diff first, including build.rs, Cargo and package + manifests, scripts and tests. Building and testing a fork runs its code. +2. Run the check in a disposable, credential-free Windows environment, such + as Windows Sandbox or a throwaway VM. It must have no GitHub, CircleCI, + SignPath or provider logins, and no access to your profile. A worktree on + your own machine is not isolation. +3. In that environment, fetch the head (`git fetch origin pull//head`), + check it out (`git checkout --detach FETCH_HEAD`), and run the commands + listed under "CircleCI validation" above one by one. Take the list from + main's copy of scripts/local-check.ps1, and don't run either copy of that + script; the fork can change its own. +4. Post the result on the PR. The PR has no `ci/circleci: pr-check`, so a + maintainer merges it with an admin override. ## Code review diff --git a/.github/workflows/fork-ci-mirror.yml b/.github/workflows/fork-ci-mirror.yml deleted file mode 100644 index 0a2d7051594..00000000000 --- a/.github/workflows/fork-ci-mirror.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Fork PR CI mirror - -# CircleCI GitHub App pipelines never fire for pull requests from forks, so -# fork PRs used to merge with no hosted Windows check. A maintainer approves a -# fork PR's current head by adding the `ci:run` label. This workflow then -# copies exactly that commit to ci/pr- in this repository. CircleCI -# builds the push (scripts/circleci-pr-common.ps1 lets ci/pr- through), -# and its check lands on the same commit, so it shows on the fork PR. -# -# Safety: -# - pull_request_target runs this file from the base branch, never the fork's -# copy. No step checks out, installs, or executes fork code here; the job -# only moves a git ref. The fork code runs later on CircleCI, which is the -# reason for the label gate. -# - Only users with triage access or higher can add labels. -# - Any new push to the fork PR removes the label, so every new head needs a -# fresh maintainer approval before CircleCI builds it. -# - The mirror branch is removed when the PR closes. -# - GITHUB_TOKEN cannot push commits that change .github/workflows/**; such -# fork PRs fail the mirror step and need a manual review path. -on: - pull_request_target: - types: [labeled, synchronize, closed] - -permissions: {} - -concurrency: - group: fork-ci-mirror-${{ github.event.pull_request.number }} - cancel-in-progress: false - -jobs: - mirror: - name: Mirror approved fork head - if: >- - github.event.action == 'labeled' && - github.event.label.name == 'ci:run' && - github.event.pull_request.head.repo.full_name != github.repository && - github.event.pull_request.state == 'open' && - vars.CI_BUDGET_MODE != 'off' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - name: Push the approved commit to ci/pr- - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - set -euo pipefail - git init --quiet mirror - cd mirror - auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 -w0)" - git remote add origin "https://github.com/${REPO}.git" - # Fetch the approved commit by SHA, not the moving pull ref, so a - # push that races the label can never be mirrored unapproved. - git -c http.extraheader="$auth" fetch --quiet --no-tags --depth=1 origin "$HEAD_SHA" - test "$(git rev-parse FETCH_HEAD)" = "$HEAD_SHA" - # The mirror branch belongs to this workflow; a re-approval replaces it. - git -c http.extraheader="$auth" push --quiet --force origin "${HEAD_SHA}:refs/heads/ci/pr-${PR_NUMBER}" - echo "Mirrored ${HEAD_SHA} to ci/pr-${PR_NUMBER}; CircleCI pr-check will report on this commit." >> "$GITHUB_STEP_SUMMARY" - - revoke: - name: Require re-approval after a new push - if: >- - github.event.action == 'synchronize' && - github.event.pull_request.head.repo.full_name != github.repository && - contains(github.event.pull_request.labels.*.name, 'ci:run') - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - pull-requests: write - steps: - - name: Remove the ci:run label - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - # The event's label list can be stale; a label already removed is fine. - if ! gh api --method DELETE "repos/${REPO}/issues/${PR_NUMBER}/labels/ci:run" --silent 2>err.txt; then - grep -q "Label does not exist\|HTTP 404" err.txt || { cat err.txt; exit 1; } - fi - echo "New commits on a fork PR: ci:run removed. Re-add it after reviewing the new head." >> "$GITHUB_STEP_SUMMARY" - - cleanup: - name: Remove the mirror branch - if: >- - github.event.action == 'closed' && - github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - steps: - - name: Delete ci/pr- if present - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - if gh api "repos/${REPO}/git/ref/heads/ci/pr-${PR_NUMBER}" --silent 2>/dev/null; then - gh api --method DELETE "repos/${REPO}/git/refs/heads/ci/pr-${PR_NUMBER}" --silent - echo "Removed ci/pr-${PR_NUMBER}." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/AGENTS.md b/AGENTS.md index 7cf33313339..c5eabab249d 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,8 +128,8 @@ pnpm run tauri:build - Rust: prefer focused `#[cfg(test)]` unit tests near the changed module. Run both manifests after Rust changes. - Frontend: Vitest 3 + jsdom + Testing Library. From `apps/desktop-tauri`: `pnpm test` (`src/**/*.{test,spec}.{ts,tsx}`). -- **Hosted PR check**: CircleCI Windows is primary and runs `scripts/local-check.ps1 -Slice ci` for ordinary/canonical PRs and `main`; micro PRs targeting `port/upstream-*` intentionally skip hosted Windows compute; a micro-named PR targeting `main` does not. `.github/workflows/pr-check.yml` is manual Blacksmith Windows reserve only. Budget details: `CONTEXT.md`, `.github/CI.md`, and ADRs under `docs/adr/`. -- **Fork PRs and reviews**: CircleCI never triggers on fork PRs by itself. After reading the diff, a maintainer adds the `ci:run` label; `fork-ci-mirror.yml` copies that head to `ci/pr-` for CircleCI, and any new push removes the label. CodeRabbit (`.coderabbit.yaml`) reviews every PR, drafts included, against these rules; fix or answer its findings before merging. +- **Hosted PR check**: CircleCI Windows is primary and runs `scripts/local-check.ps1 -Slice ci` for same-repository ordinary/canonical PRs and `main` (never for fork PRs); micro PRs targeting `port/upstream-*` intentionally skip hosted Windows compute; a micro-named PR targeting `main` does not. `.github/workflows/pr-check.yml` is manual Blacksmith Windows reserve only. Budget details: `CONTEXT.md`, `.github/CI.md`, and ADRs under `docs/adr/`. +- **Fork PRs and reviews**: CircleCI never runs on fork PRs, and this project adds no CircleCI API token. Read the full diff, then run the `-Slice ci` steps from main's copy of `scripts/local-check.ps1` against the fork head (`pull//head`) in a disposable, credential-free Windows environment (Windows Sandbox or a throwaway VM; a worktree is not isolation), and post the result (see `.github/CI.md`). CodeRabbit (`.coderabbit.yaml`) reviews every PR, drafts included, against these rules; fix or answer its findings before merging. - **Hosted mirror**: `.\scripts\local-check.ps1 -Slice ci`. The default no-parameter developer slice remains available and does not run full installer/smoke unless requested. - Parser / fetcher changes: add deterministic samples or fixtures where practical. - No coverage thresholds are configured — do not invent any. @@ -174,7 +174,7 @@ Then follow post-install instructions (permissions / accessibility as prompted). - Commands run (`cargo test`, `pnpm test`, `.\scripts\local-check.ps1`, etc.) - Screenshots / GIFs for UI changes (Windows) - Linked issue / reference when relevant -- Hosted PR check exists: `ci/circleci: pr-check` is the primary Windows gate; `.github/workflows/pr-check.yml` is manual Blacksmith backup. Porting micro PRs targeting `port/upstream-*` use focused local evidence and intentionally skip automatic hosted Windows CI; `main`-bound PRs always get the hosted gate. +- Hosted PR check exists: `ci/circleci: pr-check` is the primary Windows gate; `.github/workflows/pr-check.yml` is manual Blacksmith backup. Porting micro PRs targeting `port/upstream-*` use focused local evidence and intentionally skip automatic hosted Windows CI; same-repository `main`-bound PRs always get the hosted gate; fork PRs never do (see Fork PRs and reviews). - UI / tray / settings / float-bar / visual PRs: **CUA Driver proof is the default** ([trycua/cua](https://github.com/trycua/cua)) after a **fresh local rebuild** — see [UI validation with CUA](#ui-validation-with-cua-trycuacua). If CUA cannot be used, explain why and attach equivalent manual proof (PR template checkboxes). - Before non-trivial merge: thermo-nuclear structure review when the project process requires it. diff --git a/scripts/circleci-pr-common.ps1 b/scripts/circleci-pr-common.ps1 index 53427742daf..daaeda5574b 100644 --- a/scripts/circleci-pr-common.ps1 +++ b/scripts/circleci-pr-common.ps1 @@ -50,37 +50,21 @@ function Get-TriggerGateDecision { # every other branch push skips. CircleCI delivers same-repo PR builds # as branch pipelines, so PR association comes from the compile-time # GitHub App pipeline value pipeline.event.context.github.pr_url. - # GitHub App pipelines never fire for fork PRs, so the fork-ci-mirror - # workflow copies a maintainer-approved fork head to ci/pr-; - # those pushes run the checks too. $isPr = -not [string]::IsNullOrWhiteSpace($PrUrl) $isMainPush = $Branch -in @('main', 'master') - $isForkMirror = -not [string]::IsNullOrWhiteSpace((Get-ForkMirrorPrNumber -Branch $Branch)) - if (-not $isPr -and -not $isMainPush -and -not $isForkMirror) { + if (-not $isPr -and -not $isMainPush) { return [pscustomobject]@{ Skip = $true - Reason = "Branch push to '$Branch' (not a PR, not main/master, not a ci/pr- fork mirror): hosted pr-check skips." + Reason = "Branch push to '$Branch' (not a PR, not main/master): hosted pr-check skips." } } return [pscustomobject]@{ Skip = $false - Reason = "Trigger gate passed (PR: $isPr, fork mirror: $isForkMirror, branch: '$Branch')." + Reason = "Trigger gate passed (PR: $isPr, branch: '$Branch')." } } -<# -.SYNOPSIS - PR number of a ci/pr- fork-mirror branch, or '' for any other - branch. The fork-ci-mirror workflow is the only writer of these branches. -#> -function Get-ForkMirrorPrNumber { - param([AllowEmptyString()][string]$Branch) - - if ($Branch -match '^ci/pr-([1-9]\d*)$') { return $Matches[1] } - return '' -} - <# .SYNOPSIS Extract a SHA-256 digest for FileName from official checksum text. diff --git a/scripts/circleci-pr-gates.ps1 b/scripts/circleci-pr-gates.ps1 index 37db10d4995..bd229fe1d2a 100644 --- a/scripts/circleci-pr-gates.ps1 +++ b/scripts/circleci-pr-gates.ps1 @@ -94,7 +94,7 @@ if ($isMainPush) { # PR association without a populated event value (e.g. api trigger): # resolve the base from the public GitHub pulls API. try { - $prNumber = Get-ForkMirrorPrNumber -Branch $Branch + $prNumber = '' if ($PrUrl -match '/pull/(\d+)') { $prNumber = $Matches[1] } if ([string]::IsNullOrWhiteSpace($prNumber)) { throw 'PR number not available from pipeline values.' } if ([string]::IsNullOrWhiteSpace($env:CIRCLE_PROJECT_USERNAME) -or [string]::IsNullOrWhiteSpace($env:CIRCLE_PROJECT_REPONAME)) { diff --git a/scripts/circleci-pr.tests.ps1 b/scripts/circleci-pr.tests.ps1 index 20338cd0e0f..a776beda5d1 100644 --- a/scripts/circleci-pr.tests.ps1 +++ b/scripts/circleci-pr.tests.ps1 @@ -88,16 +88,6 @@ Assert-True $decision.Skip 'non-PR topic branch skips' Assert-True ($decision.Reason -match 'codex/topic') 'topic branch skip reason' $decision = Get-TriggerGateDecision -BudgetMode 'off' -Branch 'main' -PrUrl '' Assert-True $decision.Skip 'budget off wins over main push' -$decision = Get-TriggerGateDecision -BudgetMode 'normal' -Branch 'ci/pr-750' -PrUrl '' -Assert-True (-not $decision.Skip) 'fork mirror branch runs' -$decision = Get-TriggerGateDecision -BudgetMode 'off' -Branch 'ci/pr-750' -PrUrl '' -Assert-True $decision.Skip 'budget off wins over fork mirror' -foreach ($branch in @('ci/pr-', 'ci/pr-0', 'ci/pr-12x', 'ci/pr-7/extra', 'x/ci/pr-7', 'ci/pr-7-old')) { - $decision = Get-TriggerGateDecision -BudgetMode 'normal' -Branch $branch -PrUrl '' - Assert-True $decision.Skip "look-alike mirror branch '$branch' skips" -} -Assert-Equal (Get-ForkMirrorPrNumber -Branch 'ci/pr-750') '750' 'mirror PR number parsed' -Assert-Equal (Get-ForkMirrorPrNumber -Branch 'main') '' 'non-mirror branch has no PR number' Write-Host '==> Hosted Node provisioning guard' $prRunnerText = Get-Content -Raw -LiteralPath (Join-Path $scriptRoot 'run-circleci-pr-check.ps1')