diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6162a31..2b5d385 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,37 +302,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 @@ -346,10 +346,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 aa87693..c930fb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/jsonv", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/jsonv", - "version": "1.1.2", + "version": "1.1.3", "license": "Apache-2.0", "devDependencies": { "@cldmv/configs": "^1.2.0", @@ -132,20 +132,52 @@ } }, "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/eslint-plugin-jsonv/node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@cldmv/eslint-plugin-jsonv/node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@cldmv/fix-headers": { @@ -179,9 +211,9 @@ } }, "node_modules/@cldmv/jsonv": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@cldmv/jsonv/-/jsonv-1.0.2.tgz", - "integrity": "sha512-YfJtIoiBf43eeiqqtUYcF0RJUJfpS+Ixzdm8yaQnLG7xlOG9xHSMcqfkPvgjoJiA3hPmHbAeAo4dSJZmwS6orQ==", + "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", "peer": true, diff --git a/package.json b/package.json index 0ace1ba..eac629c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/jsonv", - "version": "1.1.2", + "version": "1.1.3", "description": "Modern JSON parser extending JSON5 with ES2015-2025 features, year-based API", "type": "module", "main": "./dist/cjs/index.cjs",