From 1fdc3c245ea5fdbd1e8658bc7276b88eb545b522 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 8 Oct 2026 21:14:31 +0000 Subject: [PATCH 1/5] ci(repo): fail CI until main's rules require every pull-request check U3 and U4 of the gates-bind plan (R22, AE10). A rules job in ci.yml reads GET /repos/{owner}/{repo}/rules/branches/main with the workflow's GITHUB_TOKEN, decodes it with Effect Schema and fails while the active rules do not require the six check legs, journeys, lint PR commits, the changeset check and rules itself. .github/rulesets/gates.json is that ruleset (active, no bypass actors, up-to-date branches required) and the README's new Getting Started step installs it, per conductor rulings 13 and 14 --- .github/rulesets/gates.json | 42 +++++++++++++++ .github/workflows/ci.yml | 18 +++++++ README.md | 12 ++++- package.json | 3 +- scripts/check-branch-rules.test.ts | 80 +++++++++++++++++++++++++++ scripts/check-branch-rules.ts | 87 ++++++++++++++++++++++++++++++ scripts/deno.json | 3 +- scripts/deno.lock | 11 +++- 8 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 .github/rulesets/gates.json create mode 100644 scripts/check-branch-rules.test.ts create mode 100755 scripts/check-branch-rules.ts diff --git a/.github/rulesets/gates.json b/.github/rulesets/gates.json new file mode 100644 index 0000000..c5c7849 --- /dev/null +++ b/.github/rulesets/gates.json @@ -0,0 +1,42 @@ +{ + "name": "gates", + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "rules": [ + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "required_status_checks": [ + { "context": "check (format)" }, + { "context": "check (lint)" }, + { "context": "check (typecheck)" }, + { "context": "check (test)" }, + { "context": "check (dist)" }, + { "context": "check (sfs-sources)" }, + { "context": "journeys" }, + { "context": "lint PR commits" }, + { "context": "changeset · shared tooling / a publishable-package change needs an intent" }, + { "context": "rules" } + ] + } + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b4a285..c0c24d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,3 +80,21 @@ jobs: apps/site-e2e/artifacts/ .journeys/dev.log if-no-files-found: ignore + + rules: + name: rules + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: ./.github/actions/dev-shell + with: + install: "false" + - name: Main's active rules require every pull-request check + env: + GITHUB_TOKEN: ${{ github.token }} + run: nix develop --command pnpm check:branch-rules diff --git a/README.md b/README.md index 669b22f..c70243e 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,17 @@ To remove it: Removal leaves a deployed D1 as it is: the `guestbook_entries` table and its `0001_create_guestbook_entries.sql` row in `__alchemy_migrations` stay; drop them from the D1 console in the Cloudflare dashboard with `DROP TABLE guestbook_entries; DELETE FROM __alchemy_migrations WHERE name = '0001_create_guestbook_entries.sql';`. -### 5. Deploy +### 5. Make the Gates Block Merges + +A copy gets the template's files, not its repository settings, so nothing stops a pull request with failing checks from merging until `main` has a ruleset that requires them. Install the "gates" ruleset from [`.github/rulesets/gates.json`](.github/rulesets/gates.json) once, with your own GitHub credentials: + +```bash +gh api -X POST repos/{owner}/{repo}/rulesets --input .github/rulesets/gates.json +``` + +Until it is installed, CI's `rules` check fails on every pull request and every push to `main`, and the production deploy waits on it. + +### 6. Deploy Your copy deploys to your own Cloudflare account; the template holds no credentials. Set `CLOUDFLARE_API_TOKEN` (a token that can edit Workers and D1) and `CLOUDFLARE_ACCOUNT_ID`, and optionally `SITE_DOMAIN` (a hostname in a zone on that account) to serve production there instead of on `workers.dev`: diff --git a/package.json b/package.json index 508bd11..94f0de6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dev": "sandbox --publish 1337:1337 -- pnpm --filter @endgame/site dev", "journeys": "sandbox --listen 1337 --pass-env PLAYWRIGHT_BROWSERS_PATH -- ./bin/journeys", "check:ci": "s=0; pnpm format:check || s=1; pnpm check:sfs-sources || s=1; TURBO_CONCURRENCY=${TURBO_CONCURRENCY:-100%} pnpm gate:tasks || s=1; pnpm gate:dist || s=1; pnpm test:sandbox || s=1; exit $s", + "check:branch-rules": "sandbox --allow-host api.github.com --allow-host registry.npmjs.org --pass-env GITHUB_TOKEN --pass-env GITHUB_REPOSITORY -- ./scripts/check-branch-rules.ts", "check:sfs-sources": "sandbox --allow-host jsr.io -- ./scripts/check-sfs-sources.ts", "clean": "sandbox -- turbo clean", "deploy": "./bin/cloud pnpm --filter @endgame/site exec alchemy deploy --stage \"${ALCHEMY_STAGE:-prod}\" --no-input --yes", @@ -41,7 +42,7 @@ "test:sandbox": "nix run .#sandbox-proofs && deno test --config=scripts/deno.json --allow-run=git,sandbox,sh --allow-env --allow-read --allow-write sandbox-proofs/", "typecheck": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue typecheck typecheck:node", "typecheck:node": "./bin/sandboxed -- tsc -p tsconfig.node.json --noEmit", - "test:scripts": "./bin/sandboxed --allow-host jsr.io -- deno test --config=scripts/deno.json --allow-read --allow-write --allow-import=jsr.io scripts/" + "test:scripts": "./bin/sandboxed --allow-host jsr.io --allow-host registry.npmjs.org -- deno test --config=scripts/deno.json --allow-read --allow-write --allow-import=jsr.io scripts/" }, "type": "module" } diff --git a/scripts/check-branch-rules.test.ts b/scripts/check-branch-rules.test.ts new file mode 100644 index 0000000..902e227 --- /dev/null +++ b/scripts/check-branch-rules.test.ts @@ -0,0 +1,80 @@ +import { assert, assertEquals } from '@std/assert' +import { Result } from 'effect' + +import { branchRulesVerdict, decodeBranchRules, REQUIRED_CHECKS } from './check-branch-rules.ts' + +const R22_CHECKS = [ + 'check (format)', + 'check (lint)', + 'check (typecheck)', + 'check (test)', + 'check (dist)', + 'check (sfs-sources)', + 'journeys', + 'lint PR commits', + 'changeset · shared tooling / a publishable-package change needs an intent', + 'rules', +] + +const RULESET_SOURCE = { + ruleset_source_type: 'Repository', + ruleset_source: 'systemfsoftware/effect-endgame-starter-kit', + ruleset_id: 22783333, +} + +const DELETION = { type: 'deletion', ...RULESET_SOURCE } +const NON_FAST_FORWARD = { type: 'non_fast_forward', ...RULESET_SOURCE } +const PULL_REQUEST = { + type: 'pull_request', + ...RULESET_SOURCE, + parameters: { + required_approving_review_count: 1, + dismiss_stale_reviews_on_push: true, + required_reviewers: [], + require_code_owner_review: false, + dismissal_restriction: { enabled: false, allowed_actors: [] }, + require_last_push_approval: false, + required_review_thread_resolution: false, + require_extra_approval_for_unattributed_changes: false, + allowed_merge_methods: ['merge', 'squash'], + }, +} + +const statusChecks = (contexts: readonly string[]) => ({ + type: 'required_status_checks', + ruleset_source_type: 'Repository', + ruleset_source: 'owner/copy', + ruleset_id: 1, + parameters: { + strict_required_status_checks_policy: false, + required_status_checks: contexts.map((context) => ({ context, integration_id: 15368 })), + }, +}) + +const verdictOn = (body: unknown, requiredChecks: readonly string[]): readonly string[] => { + const decoded = decodeBranchRules(body) + assert(Result.isSuccess(decoded), `GitHub's documented rule list failed to decode: ${JSON.stringify(body)}`) + return branchRulesVerdict(decoded.success, requiredChecks) +} + +Deno.test('the verdict names exactly the required checks the status-check rules omit, whatever else main enforces', () => { + for (let mask = 0; mask < 2 ** R22_CHECKS.length; mask++) { + const omitted = R22_CHECKS.filter((_, index) => (mask & (1 << index)) !== 0) + const enforced = R22_CHECKS.filter((_, index) => (mask & (1 << index)) === 0) + const half = Math.ceil(enforced.length / 2) + const layouts = [ + [statusChecks(enforced)], + [statusChecks(enforced.slice(0, half)), statusChecks(enforced.slice(half))], + [DELETION, statusChecks(enforced), NON_FAST_FORWARD, PULL_REQUEST, statusChecks(['preview · deploy'])], + ] + for (const rules of layouts) assertEquals(verdictOn(rules, R22_CHECKS), omitted, JSON.stringify(rules)) + } +}) + +Deno.test('a main with no active rules is refused, naming every required check', () => { + assertEquals(verdictOn([], REQUIRED_CHECKS), R22_CHECKS) +}) + +Deno.test("the template's rules today, deletion + non_fast_forward + pull_request, are refused, naming every required check", () => { + assertEquals(verdictOn([DELETION, NON_FAST_FORWARD, PULL_REQUEST], REQUIRED_CHECKS), R22_CHECKS) +}) diff --git a/scripts/check-branch-rules.ts b/scripts/check-branch-rules.ts new file mode 100755 index 0000000..d095751 --- /dev/null +++ b/scripts/check-branch-rules.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env -S deno run --config=scripts/deno.json --allow-net=api.github.com --allow-env=GITHUB_TOKEN,GITHUB_REPOSITORY +import { Result, Schema as S } from 'effect' + +export const REQUIRED_CHECKS = [ + 'check (format)', + 'check (lint)', + 'check (typecheck)', + 'check (test)', + 'check (dist)', + 'check (sfs-sources)', + 'journeys', + 'lint PR commits', + 'changeset · shared tooling / a publishable-package change needs an intent', + 'rules', +] as const + +const StatusChecksRule = S.Struct({ + type: S.Literal('required_status_checks'), + parameters: S.Struct({ + required_status_checks: S.Array(S.Struct({ context: S.String, integration_id: S.optionalKey(S.Int) })), + }), +}) +type StatusChecksRule = S.Schema.Type + +const OtherRule = S.Struct({ + type: S.String.pipe(S.check(S.makeFilter((type: string) => type !== 'required_status_checks'))), +}) + +const BranchRules = S.Array(S.Union([StatusChecksRule, OtherRule])) + +export type BranchRule = S.Schema.Type[number] + +export const decodeBranchRules = S.decodeUnknownResult(BranchRules) + +const isStatusChecksRule = (rule: BranchRule): rule is StatusChecksRule => rule.type === 'required_status_checks' + +export const branchRulesVerdict = ( + rules: readonly BranchRule[], + requiredChecks: readonly string[], +): readonly string[] => { + const enforced = new Set( + rules.filter(isStatusChecksRule).flatMap((rule) => + rule.parameters.required_status_checks.map((check) => check.context) + ), + ) + return requiredChecks.filter((name) => !enforced.has(name)) +} + +if (import.meta.main) { + const repository = Deno.env.get('GITHUB_REPOSITORY') + const token = Deno.env.get('GITHUB_TOKEN') + if (repository === undefined || token === undefined) { + console.error('check-branch-rules: set GITHUB_REPOSITORY (owner/repo) and GITHUB_TOKEN') + Deno.exit(1) + } + const path = `/repos/${repository}/rules/branches/main` + const response = await fetch(`https://api.github.com${path}?per_page=100`, { + headers: { + accept: 'application/vnd.github+json', + authorization: `Bearer ${token}`, + 'user-agent': 'check-branch-rules', + 'x-github-api-version': '2022-11-28', + }, + }) + console.log(`check-branch-rules: GET ${path} -> HTTP ${response.status}`) + if (!response.ok) { + console.error(`check-branch-rules: ${await response.text()}`) + Deno.exit(1) + } + const decoded = decodeBranchRules(await response.json()) + if (Result.isFailure(decoded)) { + console.error(`check-branch-rules: the response is not GitHub's documented rule list:\n${decoded.failure.message}`) + Deno.exit(1) + } + const rules = decoded.success + console.log(`check-branch-rules: active rule types: ${rules.map((rule) => rule.type).join(', ') || '(none)'}`) + const missing = branchRulesVerdict(rules, REQUIRED_CHECKS) + if (missing.length > 0) { + console.error( + `check-branch-rules: main's active rules do not require ${missing.length} of the ${REQUIRED_CHECKS.length} checks a pull request must pass; install .github/rulesets/gates.json (README, Getting Started):\n${ + missing.map((name) => ` ${name}`).join('\n') + }`, + ) + Deno.exit(1) + } + console.log(`check-branch-rules: main's active rules require all ${REQUIRED_CHECKS.length} checks`) +} diff --git a/scripts/deno.json b/scripts/deno.json index 8e7df68..754a4f3 100644 --- a/scripts/deno.json +++ b/scripts/deno.json @@ -8,6 +8,7 @@ "@std/cli/parse-args": "jsr:@std/cli@1/parse-args", "@std/fs/expand-glob": "jsr:@std/fs@1/expand-glob", "@std/path": "jsr:@std/path@1", - "@std/yaml": "jsr:@std/yaml@1" + "@std/yaml": "jsr:@std/yaml@1", + "effect": "npm:effect@4.0.1" } } diff --git a/scripts/deno.lock b/scripts/deno.lock index 91104ec..d0ec799 100644 --- a/scripts/deno.lock +++ b/scripts/deno.lock @@ -8,7 +8,8 @@ "jsr:@std/internal@^1.0.14": "1.0.14", "jsr:@std/path@1": "1.1.6", "jsr:@std/path@^1.1.5": "1.1.6", - "jsr:@std/yaml@1": "1.3.0" + "jsr:@std/yaml@1": "1.3.0", + "npm:effect@4.0.1": "4.0.1" }, "jsr": { "@std/assert@1.0.19": { @@ -40,13 +41,19 @@ "integrity": "b250103854378b8b36cbff0fe95aec017d3d9ce86b489c67605180c602fb4922" } }, + "npm": { + "effect@4.0.1": { + "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==" + } + }, "workspace": { "dependencies": [ "jsr:@std/assert@1", "jsr:@std/cli@1", "jsr:@std/fs@1", "jsr:@std/path@1", - "jsr:@std/yaml@1" + "jsr:@std/yaml@1", + "npm:effect@4.0.1" ] } } From 9f7916dcae858dd530f5af1e5a318b336cf31ab1 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 8 Oct 2026 21:28:25 +0000 Subject: [PATCH 2/5] build(repo): let the test sandboxes fetch effect for the scripts' Deno tests pnpm test and pnpm gate:tasks wrap test:scripts in an outer sandbox that allowed only jsr.io, so Deno could not fetch npm:effect@4.0.1 on a cold cache: egress to registry.npmjs.org:443 refused, Failed caching npm package 'effect@4.0.1'. Both outer sandboxes now allow registry.npmjs.org, the one host test:scripts already declares --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 94f0de6..0a04a3d 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "format": "./bin/dprint fmt", "format:check": "./bin/dprint check", "gate:dist": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-100%} build", - "gate:tasks": "sandbox --allow-host jsr.io -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck typecheck:node test test:scripts", + "gate:tasks": "sandbox --allow-host jsr.io --allow-host registry.npmjs.org -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck typecheck:node test test:scripts", "lint": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} lint", "precommit": "pnpm exec lint-staged --no-revert --config .lintstagedrc.js", "prepare": "effect-tsgo patch --oxlint --no-typescript --skip-missing", - "test": "sandbox --allow-host jsr.io -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} test test:scripts && pnpm test:sandbox", + "test": "sandbox --allow-host jsr.io --allow-host registry.npmjs.org -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} test test:scripts && pnpm test:sandbox", "test:sandbox": "nix run .#sandbox-proofs && deno test --config=scripts/deno.json --allow-run=git,sandbox,sh --allow-env --allow-read --allow-write sandbox-proofs/", "typecheck": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue typecheck typecheck:node", "typecheck:node": "./bin/sandboxed -- tsc -p tsconfig.node.json --noEmit", From f5a83526f61ff962c5871bbbdc9efa3beced54e8 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 8 Oct 2026 21:51:05 +0000 Subject: [PATCH 3/5] build(repo): take effect from the workspace's node_modules in the Deno scripts Deno now resolves effect through nodeModulesDir manual from the root package's installed effect (catalog, 4.0.1, already in pnpm-lock.yaml), so test:scripts downloads nothing from the npm registry. The npm:effect import entry and its deno.lock lines are gone, and every sandbox is back to its earlier hosts, per conductor ruling 15 --- package.json | 9 +++++---- pnpm-lock.yaml | 3 +++ scripts/deno.json | 4 ++-- scripts/deno.lock | 11 ++--------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 0a04a3d..eb5c541 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@systemfsoftware/stryker-js": "catalog:", "@systemfsoftware/tsconfig": "catalog:", "@types/node": "catalog:", + "effect": "catalog:", "vitest": "catalog:", "lint-staged": "catalog:", "oxlint": "catalog:", @@ -26,7 +27,7 @@ "dev": "sandbox --publish 1337:1337 -- pnpm --filter @endgame/site dev", "journeys": "sandbox --listen 1337 --pass-env PLAYWRIGHT_BROWSERS_PATH -- ./bin/journeys", "check:ci": "s=0; pnpm format:check || s=1; pnpm check:sfs-sources || s=1; TURBO_CONCURRENCY=${TURBO_CONCURRENCY:-100%} pnpm gate:tasks || s=1; pnpm gate:dist || s=1; pnpm test:sandbox || s=1; exit $s", - "check:branch-rules": "sandbox --allow-host api.github.com --allow-host registry.npmjs.org --pass-env GITHUB_TOKEN --pass-env GITHUB_REPOSITORY -- ./scripts/check-branch-rules.ts", + "check:branch-rules": "sandbox --allow-host api.github.com --allow-host jsr.io --pass-env GITHUB_TOKEN --pass-env GITHUB_REPOSITORY -- ./scripts/check-branch-rules.ts", "check:sfs-sources": "sandbox --allow-host jsr.io -- ./scripts/check-sfs-sources.ts", "clean": "sandbox -- turbo clean", "deploy": "./bin/cloud pnpm --filter @endgame/site exec alchemy deploy --stage \"${ALCHEMY_STAGE:-prod}\" --no-input --yes", @@ -34,15 +35,15 @@ "format": "./bin/dprint fmt", "format:check": "./bin/dprint check", "gate:dist": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-100%} build", - "gate:tasks": "sandbox --allow-host jsr.io --allow-host registry.npmjs.org -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck typecheck:node test test:scripts", + "gate:tasks": "sandbox --allow-host jsr.io -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue lint typecheck typecheck:node test test:scripts", "lint": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} lint", "precommit": "pnpm exec lint-staged --no-revert --config .lintstagedrc.js", "prepare": "effect-tsgo patch --oxlint --no-typescript --skip-missing", - "test": "sandbox --allow-host jsr.io --allow-host registry.npmjs.org -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} test test:scripts && pnpm test:sandbox", + "test": "sandbox --allow-host jsr.io -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} test test:scripts && pnpm test:sandbox", "test:sandbox": "nix run .#sandbox-proofs && deno test --config=scripts/deno.json --allow-run=git,sandbox,sh --allow-env --allow-read --allow-write sandbox-proofs/", "typecheck": "sandbox -- turbo --concurrency=${TURBO_CONCURRENCY:-50%} --continue typecheck typecheck:node", "typecheck:node": "./bin/sandboxed -- tsc -p tsconfig.node.json --noEmit", - "test:scripts": "./bin/sandboxed --allow-host jsr.io --allow-host registry.npmjs.org -- deno test --config=scripts/deno.json --allow-read --allow-write --allow-import=jsr.io scripts/" + "test:scripts": "./bin/sandboxed --allow-host jsr.io -- deno test --config=scripts/deno.json --allow-read --allow-write --allow-import=jsr.io scripts/" }, "type": "module" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35f5d93..72463ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -322,6 +322,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.19.1 + effect: + specifier: 'catalog:' + version: 4.0.1 lint-staged: specifier: 'catalog:' version: 17.6.0 diff --git a/scripts/deno.json b/scripts/deno.json index 754a4f3..d41a352 100644 --- a/scripts/deno.json +++ b/scripts/deno.json @@ -3,12 +3,12 @@ "strict": true, "noImplicitOverride": true }, + "nodeModulesDir": "manual", "imports": { "@std/assert": "jsr:@std/assert@1", "@std/cli/parse-args": "jsr:@std/cli@1/parse-args", "@std/fs/expand-glob": "jsr:@std/fs@1/expand-glob", "@std/path": "jsr:@std/path@1", - "@std/yaml": "jsr:@std/yaml@1", - "effect": "npm:effect@4.0.1" + "@std/yaml": "jsr:@std/yaml@1" } } diff --git a/scripts/deno.lock b/scripts/deno.lock index d0ec799..91104ec 100644 --- a/scripts/deno.lock +++ b/scripts/deno.lock @@ -8,8 +8,7 @@ "jsr:@std/internal@^1.0.14": "1.0.14", "jsr:@std/path@1": "1.1.6", "jsr:@std/path@^1.1.5": "1.1.6", - "jsr:@std/yaml@1": "1.3.0", - "npm:effect@4.0.1": "4.0.1" + "jsr:@std/yaml@1": "1.3.0" }, "jsr": { "@std/assert@1.0.19": { @@ -41,19 +40,13 @@ "integrity": "b250103854378b8b36cbff0fe95aec017d3d9ce86b489c67605180c602fb4922" } }, - "npm": { - "effect@4.0.1": { - "integrity": "sha512-b1VlQG9g8fwxE5QnIZuPoOP/0MsqHJSRKxUijjoM80E5q95FXrX5yWtY5XI8G+GJsBQAVSDbLS458z9a++uVvw==" - } - }, "workspace": { "dependencies": [ "jsr:@std/assert@1", "jsr:@std/cli@1", "jsr:@std/fs@1", "jsr:@std/path@1", - "jsr:@std/yaml@1", - "npm:effect@4.0.1" + "jsr:@std/yaml@1" ] } } From adfa3404e1568b050adbe39886fb8b8a1318cee0 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 8 Oct 2026 21:58:20 +0000 Subject: [PATCH 4/5] ci(repo): install the workspace before the rules check reads effect check-branch-rules.ts now takes effect from node_modules, and the rules job skipped the install, so Deno failed with 'Could not resolve effect' on run 37849660110. The dev-shell step's default install is offline from the Nix pnpm store --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0c24d3..01b64ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,8 +92,6 @@ jobs: with: persist-credentials: false - uses: ./.github/actions/dev-shell - with: - install: "false" - name: Main's active rules require every pull-request check env: GITHUB_TOKEN: ${{ github.token }} From 3e36328f4678e06b32896376cf111278f8916ea9 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 8 Oct 2026 22:20:02 +0000 Subject: [PATCH 5/5] fix(repo): accept a null integration_id in the branch rules decode A ruleset created from gates.json through the REST API pins no app, so a required status check may come back with integration_id null. The decode refused that and would have kept rules red with the ruleset installed. The property now generates integer, null and absent ids, per conductor ruling 16 --- scripts/check-branch-rules.test.ts | 7 ++++++- scripts/check-branch-rules.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/check-branch-rules.test.ts b/scripts/check-branch-rules.test.ts index 902e227..6034699 100644 --- a/scripts/check-branch-rules.test.ts +++ b/scripts/check-branch-rules.test.ts @@ -40,6 +40,8 @@ const PULL_REQUEST = { }, } +const INTEGRATION_IDS = [{ integration_id: 15368 }, { integration_id: null }, {}] + const statusChecks = (contexts: readonly string[]) => ({ type: 'required_status_checks', ruleset_source_type: 'Repository', @@ -47,7 +49,10 @@ const statusChecks = (contexts: readonly string[]) => ({ ruleset_id: 1, parameters: { strict_required_status_checks_policy: false, - required_status_checks: contexts.map((context) => ({ context, integration_id: 15368 })), + required_status_checks: contexts.map((context, index) => ({ + context, + ...INTEGRATION_IDS[index % INTEGRATION_IDS.length], + })), }, }) diff --git a/scripts/check-branch-rules.ts b/scripts/check-branch-rules.ts index d095751..5837135 100755 --- a/scripts/check-branch-rules.ts +++ b/scripts/check-branch-rules.ts @@ -17,7 +17,7 @@ export const REQUIRED_CHECKS = [ const StatusChecksRule = S.Struct({ type: S.Literal('required_status_checks'), parameters: S.Struct({ - required_status_checks: S.Array(S.Struct({ context: S.String, integration_id: S.optionalKey(S.Int) })), + required_status_checks: S.Array(S.Struct({ context: S.String, integration_id: S.optionalKey(S.NullOr(S.Int)) })), }), }) type StatusChecksRule = S.Schema.Type