diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c44651f..c9f561c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -308,37 +308,37 @@ jobs: # automatically — no `pull_request` round-trip needed for non-fork # non-release PRs. required-check: - # The name is conditional on purpose. On an in-repo feature PR the - # `pull_request` run skips this job (the push run owns the status), and - # a skipped job still posts a check run under its name. GitHub treats a - # SKIPPED required check as satisfied — so if the skipped job were named - # `✅ Required PR Check`, it would green-light the ruleset (and enable - # auto-merge) while the push run's real mirror hadn't been created yet - # (it only appears once `ci` finishes), letting a PR merge mid-test or - # even override a red result. An expression name keeps the skipped job - # off the required name: GitHub does not evaluate the name of a skipped - # job, so it shows up as the raw expression text (still not the - # required name), while every path that runs evaluates to - # `✅ Required PR Check`. The condition is written out anyway so the - # name stays correct if GitHub ever starts evaluating it, and must stay - # identical to the `if:` below. - name: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true || github.event.pull_request.head.ref == 'next' || github.event.pull_request.head.ref == 'hotfixes') && '✅ Required PR Check' || '⏭️ Required PR Check (reported by the push run)' }} - needs: ci - # Mirror the `ci` job's gating exactly. The four cases that run: + # Which name this job reports under is the whole point of it. + # + # The ruleset gates merges on `✅ Required PR Check`, so a check with that + # name must only ever exist on a head SHA after the full test matrix for + # that SHA has finished, mirroring its result. `needs: ci` guarantees the + # timing: the job is only created once every Build & Test leg and the + # coverage job are done. + # + # On an in-repo feature PR the `pull_request` run does not own the status + # (the push run on the head branch does), so it must not post + # `✅ Required PR Check` at all. Two traps rule out the obvious shapes: + # - A job SKIPPED by `if:` still posts a check run under its name, and + # GitHub treats a skipped required check as satisfied. Under the real + # name that let PRs merge mid-test (CLDMV/slothlet#553). + # - GitHub does not evaluate the `name:` of a skipped job, so a + # conditional name on a skippable job shows up as the raw expression + # text (#350). + # So the job never skips: it runs on every path, the name expression is + # always evaluated, and the in-repo PR path lands on a readable, + # non-required name and passes as a no-op. The condition below is + # repeated in the step's OWNS_STATUS and must stay identical. The paths + # that own the status: # 1. push events (job needs CI run) # 2. fork PRs (push doesn't cover forks) # 3. release PRs from `next` → master/main (push covers SHA but commit-gate skips chore-bump) # 4. release PRs from `hotfixes` → master/main (same reason) - # In-repo feature PRs targeting `next` / `hotfixes` skip on - # pull_request — the push run on the head branch reports the status - # on the SHA. See the `name:` above for why the skipped job is renamed. - if: | - always() && ( - github.event_name != 'pull_request' || - github.event.pull_request.head.repo.fork == true || - github.event.pull_request.head.ref == 'next' || - github.event.pull_request.head.ref == 'hotfixes' - ) + name: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true || github.event.pull_request.head.ref == 'next' || github.event.pull_request.head.ref == 'hotfixes') && '✅ Required PR Check' || '⏭️ Required PR Check (reported by the push run)' }} + needs: ci + # always(): run even when `ci` is skipped (the in-repo PR path) or failed + # (so the mirror can report red). + if: always() # Match the reusable's runner routing (workflow-ci.yml): private CLDMV # repos run on self-hosted cldmv-runners (GitHub-hosted Actions budget is # private-metered and exhausted), public repos use free GitHub-hosted, and @@ -352,10 +352,19 @@ jobs: steps: - name: Mirror reusable result env: + OWNS_STATUS: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true || github.event.pull_request.head.ref == 'next' || github.event.pull_request.head.ref == 'hotfixes' }} IS_MASTER_SYNC: ${{ needs.ci.outputs.is_master_sync }} DOCS_ONLY: ${{ needs.ci.outputs.docs_only }} CI_RESULT: ${{ needs.ci.result }} run: | + # In-repo feature PR: the push run on the head branch reports + # `✅ Required PR Check`. This job runs under the + # `⏭️ Required PR Check (reported by the push run)` name and + # must not gate anything. + if [ "$OWNS_STATUS" != "true" ]; then + echo "In-repo PR event — the push run reports ✅ Required PR Check for this SHA." + exit 0 + fi echo "ci.result=$CI_RESULT docs_only=$DOCS_ONLY is_master_sync=$IS_MASTER_SYNC" # next/hotfixes was force-synced to master — head SHA matches the # default branch, nothing new to test, green-light without running CI. diff --git a/package-lock.json b/package-lock.json index 6544151..f9ac3c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/git-embedded", - "version": "1.1.10", + "version": "1.1.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/git-embedded", - "version": "1.1.10", + "version": "1.1.11", "license": "Apache-2.0", "dependencies": { "@cldmv/slothlet": "^3.7.0", @@ -140,20 +140,25 @@ } }, "node_modules/@cldmv/eslint-plugin-jsonv": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@cldmv/eslint-plugin-jsonv/-/eslint-plugin-jsonv-1.0.10.tgz", - "integrity": "sha512-BtkGK0Jo6nir7GL3JpY6eEAqX/8XzibMgPbT3S+vkhWulVd+47xx+oUwxvaEvj24XyVaIKqloHzAWYtnqXIGDQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@cldmv/eslint-plugin-jsonv/-/eslint-plugin-jsonv-1.0.13.tgz", + "integrity": "sha512-mz8YQCmwZn5/VGFCSWvR38FzNaNP6HCe/PYGxvq/RZGty/aGEH5xiolpD6iCuHlRXcrd/NWqpPJSTyU9mwZKxA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3" + }, "engines": { - "node": ">=18.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "type": "github", "url": "https://github.com/sponsors/shinrai" }, "peerDependencies": { - "@cldmv/jsonv": "^1.0.2" + "@cldmv/jsonv": "^1.1.0", + "eslint": "^9.13.0 || ^10.0.0" } }, "node_modules/@cldmv/fix-headers": { @@ -187,9 +192,9 @@ } }, "node_modules/@cldmv/jsonv": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@cldmv/jsonv/-/jsonv-1.0.9.tgz", - "integrity": "sha512-L84D+ocPzCg/Q88HvrVjxt7UMQ6PxHe7kbAj6Uor/eFE0/HkB8xuPoyB+ssa2vRwleFVZ+q3RXLxt8BT1ZM0Ow==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@cldmv/jsonv/-/jsonv-1.1.1.tgz", + "integrity": "sha512-2eGO5hc08x3++HSxgzNGJibNzulVIaqn6yoGUUelvKrnL6dMNAlaL/sjIpCdPBToNlD6SwRqtgTDoxqivn5yzw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -201,9 +206,9 @@ } }, "node_modules/@cldmv/prettier-plugin-jsonv": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@cldmv/prettier-plugin-jsonv/-/prettier-plugin-jsonv-1.0.6.tgz", - "integrity": "sha512-eRi160SbGfPUl8CELaNBxH34Kp14F3eQ76pvNBbuV4IlyXbEflcecwnY+WKuYooQbNZveR5/hn+76DH117kB8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cldmv/prettier-plugin-jsonv/-/prettier-plugin-jsonv-1.1.0.tgz", + "integrity": "sha512-hnyn2JxFLzsxLmuLb+XlYaN/YffBo6iv9vKEbHlI8FD8J2Byi8SeZqo7er8JWPxPdANzn4nrO1XCBDwtnA3M+g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -214,14 +219,14 @@ "url": "https://github.com/sponsors/shinrai" }, "peerDependencies": { - "@cldmv/jsonv": "^1.0.0", + "@cldmv/jsonv": "^1.1.0", "prettier": "^3.0.0" } }, "node_modules/@cldmv/slothlet": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@cldmv/slothlet/-/slothlet-3.17.0.tgz", - "integrity": "sha512-rOpJZBPbhDBr3CRG2bjRvlIrw6VTMmnXi8z7CyGHaVK1+AJc0PEh1CKEumsZSxK8WON0za/gh5g5QGCwdbV59A==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@cldmv/slothlet/-/slothlet-3.21.0.tgz", + "integrity": "sha512-/bCrVDOCj9TPPfKxZhB0qGvNGLXGCuDG4DGD63QzqTbNsxq8OJgNboMknUicfYWZQ7hZwIhY87z9/uBfyAnocQ==", "license": "Apache-2.0", "bin": { "slothlet": "bin/slothlet.mjs" @@ -258,9 +263,9 @@ } }, "node_modules/@cldmv/vitest-runner": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@cldmv/vitest-runner/-/vitest-runner-1.2.0.tgz", - "integrity": "sha512-RhmXwFNB68OsgnIFSoQeTWgqEAZt/A+MYfc9lf2JdCUIRhzq2Z3gVeuw1pYOV0LihqfPWRNSmaVVDEEQKa93Tw==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@cldmv/vitest-runner/-/vitest-runner-1.5.1.tgz", + "integrity": "sha512-Q64qMfJe9TbJOOKE382eqyJiEVbyc8ycfkPFheE4nMOUyV/z7+VRkE3K4viSlCAhUjmlFyU+oxD6DDkax84KDA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index de0578d..d7f37ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/git-embedded", - "version": "1.1.10", + "version": "1.1.11", "description": "Manage embedded git repositories (anonymous gitlinks) without .gitmodules. Provides hooks that restore standard git-command ergonomics for embedded children while keeping the child's origin URL out of the public parent repo.", "type": "module", "license": "Apache-2.0",