From 8375f6d54d957dd26c15942d8d6c7db6e03a453d Mon Sep 17 00:00:00 2001 From: DevBot Date: Fri, 4 Sep 2026 00:52:31 +0800 Subject: [PATCH 1/3] feat(governance): offload generic checks to mature OSS tooling (#1156 B2.6) ADR-0144 implementation slice: - Replace the bespoke explicit-any checker (tools/check-type-safety.ts + test + task + AutoFlow gate) with deno lint's no-explicit-any rule (AST-based, stricter, green on all 887 linted files). - Adopt markdownlint-cli2@0.23.2 for Markdown structure (thin .markdownlint-cli2.jsonc, gitignore-scoped, rule set green repo-wide; fixes 5 trailing-space + 3 EOF-newline violations in www/content/blog). - Adopt gitleaks 8.30.1 (CI step, binary pinned by version + SHA-256) for secret scanning; delete the custom SECRET_CONTENT_PATTERNS regex scan from check-repo-hygiene.ts. .gitleaks.toml allowlists only deliberate Stripe placeholder fixtures. - Adopt actionlint 1.7.12 (CI step, binary pinned by version + SHA-256) for workflow linting; clean on all 8 workflows. - Adopt zizmor via zizmorcore/zizmor-action@v0.6.3 (pinned SHA registered in ACTION_VERSION_PINS, offline audits, advanced-security off) for workflow security; fix all 11 artipacked findings with persist-credentials: false, and suppress 4 findings with in-band reasons (release-lane credential persistence, pinned npm floor, GitHub-controlled template expansions). - .github/zizmor.yml disables only the self-repository style audit (./local-action form is already audited by check-action-pins). Net governance surface: -1 custom checker (-210 LOC incl. tests), -1 custom task, -1 custom AutoFlow gate, zero new workflows/jobs. --- .github/workflows/autoflow-ci.yml | 30 +++++ .github/workflows/autoflow-release.yml | 4 +- .github/workflows/codeql.yml | 2 + .github/workflows/fullstack-deploy-smoke.yml | 6 + .github/workflows/nightly-stress.yml | 2 + .github/workflows/published-consumers.yml | 4 + .github/workflows/supabase-project-smoke.yml | 8 ++ .github/zizmor.yml | 8 ++ .gitleaks.toml | 20 +++ .markdownlint-cli2.jsonc | 14 +++ deno.json | 5 +- .../supabase-cloudflare-starter/deno.lock | 1 + tools/autoflow/policy.ts | 14 ++- tools/check-action-pins.ts | 2 + tools/check-architecture-contract.ts | 1 - tools/check-repo-hygiene.ts | 23 +--- tools/check-type-safety.test.ts | 73 ----------- tools/check-type-safety.ts | 118 ------------------ ...0007-view-transitions-speculation-rules.md | 2 +- ...ild-data-consistency-plugin-data-bridge.md | 2 +- .../blog/0019-post-review-improvement-plan.md | 2 +- .../blog/adr-0009-full-repo-simplification.md | 10 +- 22 files changed, 124 insertions(+), 227 deletions(-) create mode 100644 .github/zizmor.yml create mode 100644 .gitleaks.toml create mode 100644 .markdownlint-cli2.jsonc delete mode 100644 tools/check-type-safety.test.ts delete mode 100644 tools/check-type-safety.ts diff --git a/.github/workflows/autoflow-ci.yml b/.github/workflows/autoflow-ci.yml index b675e40e3..b62dc3eb0 100644 --- a/.github/workflows/autoflow-ci.yml +++ b/.github/workflows/autoflow-ci.yml @@ -40,6 +40,33 @@ jobs: # never checkout's default synthetic merge ref. ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 + persist-credentials: false + # #1156 (B2.6): mature OSS governance gates (ADR-0144) — fail fast, + # before the heavy matrix. Binaries pinned by version + SHA-256; + # zizmor-action pinned in tools/check-action-pins.ts. + - name: actionlint (workflow lint) + run: | + curl -sSfL -o /tmp/actionlint.tar.gz \ + https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz + echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 /tmp/actionlint.tar.gz" | sha256sum -c - + tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint + /tmp/actionlint -color + # v0.6.3 + - uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 + with: + # Offline audits only: this gate must be deterministic and + # reproducible locally (`zizmor --offline .github/workflows + # .github/actions`); network-dependent audits stay out of CI. + online-audits: false + advanced-security: false + version: '1.30.0' + - name: gitleaks (secret scan) + run: | + curl -sSfL -o /tmp/gitleaks.tar.gz \ + https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz + echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb /tmp/gitleaks.tar.gz" | sha256sum -c - + tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks + /tmp/gitleaks git --redact --verbose . - uses: ./.github/actions/setup-deno-workspace - name: Install Playwright browsers # All three engines up front: the gate's fixture:request-time:gate @@ -66,6 +93,7 @@ jobs: with: # #1156 R11: same exact-SHA expression as every required job. ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace # v7.0.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 @@ -127,6 +155,7 @@ jobs: with: # #1156 R11: same exact-SHA expression as every required job. ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace # v7.0.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 @@ -156,6 +185,7 @@ jobs: with: # #1156 R11: the aggregation job checks out the exact SHA it attests. ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace - name: Write exact-SHA PR CI evidence record env: diff --git a/.github/workflows/autoflow-release.yml b/.github/workflows/autoflow-release.yml index 627bb45a2..fa8ec4be1 100644 --- a/.github/workflows/autoflow-release.yml +++ b/.github/workflows/autoflow-release.yml @@ -37,7 +37,7 @@ jobs: actions: read steps: # v7.0.1 - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # zizmor: ignore[artipacked] the release lane pushes the immutable release tag with this credential (tools/autoflow/release.ts); every other workflow sets persist-credentials: false with: ref: main fetch-depth: 0 @@ -57,7 +57,7 @@ jobs: # and switching the whole release lane to Node 24 would still leave # the floor to whatever npm that image bundles, so the floor is # pinned explicitly here and verified before publish runs. - run: | + run: | # zizmor: ignore[adhoc-packages] the npm floor is pinned and asserted immediately below; trusted publishing requires it npm install -g "npm@^11.5.1" actual="$(npm --version)" minimum="11.5.1" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8856253d9..f98a6edbc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,6 +36,8 @@ jobs: - name: Checkout repository # v7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Initialize CodeQL # v4.37.9 diff --git a/.github/workflows/fullstack-deploy-smoke.yml b/.github/workflows/fullstack-deploy-smoke.yml index 85c31b9c3..9fd560755 100644 --- a/.github/workflows/fullstack-deploy-smoke.yml +++ b/.github/workflows/fullstack-deploy-smoke.yml @@ -62,6 +62,8 @@ jobs: steps: # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace - name: Build the Workers bundle @@ -415,6 +417,10 @@ jobs: if: always() env: WORKER_URL: https://openelement-ref-starter.freemanzheng.workers.dev + # zizmor: ignore[template-injection] every expansion below is + # GitHub-controlled context (job.status, github.run_id, + # steps.*.outcome) — no attacker-controllable input reaches this + # report writer. run: | checks='[]' if [ -f .smoke/results.jsonl ]; then diff --git a/.github/workflows/nightly-stress.yml b/.github/workflows/nightly-stress.yml index e8c2550d1..67ffbee6e 100644 --- a/.github/workflows/nightly-stress.yml +++ b/.github/workflows/nightly-stress.yml @@ -15,6 +15,8 @@ jobs: steps: # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace - name: Run representative 30-minute workload env: diff --git a/.github/workflows/published-consumers.yml b/.github/workflows/published-consumers.yml index 96afb95f0..ec9b7f7ab 100644 --- a/.github/workflows/published-consumers.yml +++ b/.github/workflows/published-consumers.yml @@ -22,6 +22,8 @@ jobs: steps: # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace - name: Generate and exercise the published starter env: @@ -45,6 +47,8 @@ jobs: steps: # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace - name: Install Chromium for browser-backed consumer smoke run: ./node_modules/.bin/playwright install --with-deps chromium diff --git a/.github/workflows/supabase-project-smoke.yml b/.github/workflows/supabase-project-smoke.yml index e2f381322..c9677c6af 100644 --- a/.github/workflows/supabase-project-smoke.yml +++ b/.github/workflows/supabase-project-smoke.yml @@ -44,6 +44,8 @@ jobs: steps: # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false # v1, pinned 2026-08-17 - uses: supabase/setup-cli@ab058987d8d6c725971f6cf9d0b5c98467e30bd1 @@ -75,6 +77,8 @@ jobs: steps: # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - uses: ./.github/actions/setup-deno-workspace - name: Require dedicated migration credentials @@ -477,6 +481,10 @@ jobs: - name: Write the redacted smoke report if: always() + # zizmor: ignore[template-injection] every expansion below is + # GitHub-controlled context (job.status, github.run_id, + # steps.*.outcome, inputs.migration_mode) — no attacker-controllable + # input reaches this report writer. run: | matrix='[]' if [ -f .smoke/results.jsonl ]; then diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..5115cc20d --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,8 @@ +# zizmor configuration (#1156 B2.6). Findings are fixed, not suppressed; the +# only disabled audit is a style preference already covered by an owned gate. +rules: + self-repository: + # The repository standardizes on the `./.github/...` local-action form; + # tools/check-action-pins.ts already audits every `uses:` clause, so the + # `$/` prefix would add a second convention without new evidence. + disable: true diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 000000000..0a665d2e2 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,20 @@ +# Gitleaks configuration for OpenElement (#1156 B2.6). +# Extends the default rule set; the allowlist below covers deliberate +# placeholder credentials used in tests and documentation. Real credential +# material must never appear here — fix the leak, do not allowlist it. + +[extend] +useDefault = true + +[allowlist] +description = "Deliberate non-secret placeholders" +regexTarget = "line" +regexes = [ + # Test fixtures: intentionally invalid Stripe-shaped placeholders used to + # assert checkoutConfiguration mode validation (never real credentials). + '''sk_live_wrong''', + '''rk_(?:test|live)_restricted''', + # Runbook example (git history): a shell variable reference, not a + # credential value. + '''-u\s+["']?\$STRIPE_SECRET_KEY''', +] diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 000000000..8a39ac939 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.23.2/schema/markdownlint-cli2-config-schema.json", + "gitignore": true, + "config": { + "default": false, + "MD001": true, + "MD009": { "br_spaces": 2 }, + "MD011": true, + "MD012": true, + "MD024": { "siblings_only": true }, + "MD042": true, + "MD047": true + } +} diff --git a/deno.json b/deno.json index bded23499..c429b6f3d 100644 --- a/deno.json +++ b/deno.json @@ -74,7 +74,7 @@ "fullstack:evidence-freshness": "deno run --allow-env --allow-net=api.github.com tools/check-evidence-freshness.ts", "fullstack:cloudflare-config-check": "deno test --allow-read tools/render-cloudflare-async-config.test.ts && deno task --cwd examples/supabase-cloudflare-starter build && deno task --cwd examples/supabase-cloudflare-starter nitro:build && deno run --allow-read --allow-write tools/render-cloudflare-async-config.ts examples/supabase-cloudflare-starter/wrangler.jsonc examples/supabase-cloudflare-starter/.wrangler-async.generated.json && deno run --allow-run=deno tools/run-wrangler-dry-run.ts examples/supabase-cloudflare-starter/.wrangler-async.generated.json && rm examples/supabase-cloudflare-starter/.wrangler-async.generated.json", "arch:check": "deno run --allow-read --allow-run tools/check-architecture-contract.ts", - "type-safety:check": "deno run --allow-read tools/check-type-safety.ts", + "lint:markdown": "deno run -A npm:markdownlint-cli2@0.23.2 \"**/*.md\"", "deno-api:check": "deno run --allow-read --allow-env tools/check-deno-api-free.ts", "text-integrity:check": "deno run --allow-read --allow-run=git tools/check-docs-truth.ts --check=text", "audit:citations:check": "deno run -A tools/check-audit-citations.ts", @@ -160,6 +160,9 @@ "tags": [ "recommended" ], + "include": [ + "no-explicit-any" + ], "exclude": [ "no-sloppy-imports" ] diff --git a/examples/supabase-cloudflare-starter/deno.lock b/examples/supabase-cloudflare-starter/deno.lock index ded6eeb2d..6d12fdaed 100644 --- a/examples/supabase-cloudflare-starter/deno.lock +++ b/examples/supabase-cloudflare-starter/deno.lock @@ -21,6 +21,7 @@ "npm:pdf-parse@^1.1.1": "1.1.4", "npm:pdfjs-dist@4.8.69": "4.8.69", "npm:preact@^10.28.0": "10.29.8_preact-render-to-string@6.7.0", + "npm:typescript@^5.9.0": "5.9.3", "npm:urlpattern-polyfill@10.1.0": "10.1.0", "npm:valibot@^1.1.0": "1.4.2_typescript@5.9.3", "npm:vite@8.0.16": "8.0.16_esbuild@0.25.12_jiti@2.7.0_yaml@2.9.0", diff --git a/tools/autoflow/policy.ts b/tools/autoflow/policy.ts index b8c07be5c..8ac731ba5 100644 --- a/tools/autoflow/policy.ts +++ b/tools/autoflow/policy.ts @@ -320,18 +320,20 @@ const GATES: readonly GateDefinition[] = [ /^tools\/check-signal-protocol-boundary\.ts$/, ], }, - { - name: 'type-safety:check', - command: ['deno', 'task', 'type-safety:check'], - tiers: ['ci', 'release'], - triggers: [/^packages\//, /^tools\//, /^www\//, /^deno\.json$/], - }, { name: 'deno-api:check', command: ['deno', 'task', 'deno-api:check'], tiers: ['ci', 'release'], triggers: [/^packages\/(element|ui|app)\/src\//], }, + { + // #1156 (B2.6): markdownlint-cli2 owns Markdown structure per ADR-0144 + // (thin .markdownlint-cli2.jsonc config; no bespoke checker). + name: 'lint:markdown', + command: ['deno', 'task', 'lint:markdown'], + tiers: ['push', 'ci', 'release'], + triggers: [/\.md$/, /^\.markdownlint-cli2\.jsonc$/, /^deno\.json$/], + }, { name: 'text-integrity:check', command: ['deno', 'task', 'text-integrity:check'], diff --git a/tools/check-action-pins.ts b/tools/check-action-pins.ts index abe6fa199..c872bcd6c 100644 --- a/tools/check-action-pins.ts +++ b/tools/check-action-pins.ts @@ -25,6 +25,8 @@ const ACTION_VERSION_PINS = new Map([ ['github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3', 'v4.37.6'], ['github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938', 'v4.37.9'], ['github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938', 'v4.37.9'], + // #1156 (B2.6): zizmor workflow-security gate (ADR-0144). + ['zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99', 'v0.6.3'], ]); // Repos that carry an approved pin above. A full-SHA use of one of these repos diff --git a/tools/check-architecture-contract.ts b/tools/check-architecture-contract.ts index c62711cf3..33e9aa5aa 100644 --- a/tools/check-architecture-contract.ts +++ b/tools/check-architecture-contract.ts @@ -255,7 +255,6 @@ export function isProductionSource(path: string): boolean { // Test files that exercise the architecture contract necessarily contain // escape tokens, so they are excluded from production scanning. if (path === 'tools/check-architecture-contract.test.ts') return false; - if (path === 'tools/check-type-safety.test.ts') return false; if (path.startsWith('packages/') && path.includes('/src/') && /\.(ts|tsx)$/.test(path)) { return true; } diff --git a/tools/check-repo-hygiene.ts b/tools/check-repo-hygiene.ts index d62c52316..2b0b11656 100644 --- a/tools/check-repo-hygiene.ts +++ b/tools/check-repo-hygiene.ts @@ -80,24 +80,17 @@ const allowedTrackedIgnoredPaths = [ /^vendor\/jsr\.io\/(@[^/]+\/)?[^/]+\/LICENSE$/, ]; -// Secret scanning: tracked credential files are always failures, and -// credential-shaped content in active source files fails the gate. These -// patterns intentionally stay narrow to keep false positives at zero. -// Placeholder templates (.env.example/.env.sample/.env.template) are -// allowed by name — the content scan below still applies to them, so a -// template carrying real credentials still fails. +// Tracked credential files are always failures. The content-level secret +// scan is owned by gitleaks (#1156 B2.6, .gitleaks.toml); this file-name +// tripwire stays because gitleaks does not flag a tracked-but-empty +// credential file. Placeholder templates (.env.example/.env.sample/ +// .env.template) are allowed by name. const allowedCredentialTemplates = /(?:^|\/)\.env(?:\.example|\.sample|\.template)$/; const forbiddenTrackedSecretFiles = [ /(?:^|\/)\.env(?:\.[^/]+)?$/, /(?:^|\/)[^/]+\.pem$/, /(?:^|\/)id_rsa(?:\.pub)?$/, ]; -const SECRET_CONTENT_PATTERNS = [ - /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/, - /\bAKIA[0-9A-Z]{16}\b/, - /\bgh[pousr]_[A-Za-z0-9]{36,}\b/, - /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/, -]; // Large tracked binaries: intentional design/e2e/fixture assets are listed; // anything else above 1 MiB should not enter the repository. @@ -165,12 +158,6 @@ for (const file of files.filter(isActiveScanFile)) { }); } } - for (const pattern of SECRET_CONTENT_PATTERNS) { - if (pattern.test(text)) { - failures.push({ path: file, message: 'credential-shaped content detected' }); - break; - } - } } for (const file of files) { diff --git a/tools/check-type-safety.test.ts b/tools/check-type-safety.test.ts deleted file mode 100644 index fd53e70fc..000000000 --- a/tools/check-type-safety.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { assert, assertEquals } from '@std/assert'; -import { isCodeLine, type Issue, scanSourcesForAnyIssues } from './check-type-safety.ts'; - -const SAMPLE = 'packages/core/src/example.ts'; - -Deno.test('type-safety: detects `as any` cast escape', () => { - const issues = scanSourcesForAnyIssues([{ path: SAMPLE, text: 'const x = foo() as any;' }]); - assertEquals(issues.length, 1); - assertEquals(issues[0].line, 1); - assertEquals(issues[0].file, SAMPLE); - assert(issues[0].text.includes('cast')); -}); - -Deno.test('type-safety: detects `: any` annotation escape', () => { - const issues = scanSourcesForAnyIssues([{ path: SAMPLE, text: 'function f(x: any): void {}' }]); - assertEquals(issues.length, 1); - assert(issues[0].text.includes('annotation')); -}); - -Deno.test('type-safety: detects `any[]` in a generic position', () => { - // `as any` and `: any` do not match here, isolating the any[] branch. - const issues = scanSourcesForAnyIssues([ - { path: SAMPLE, text: 'const m: Map = new Map();' }, - ]); - assertEquals(issues.length, 1); - assert(issues[0].text.includes('array')); -}); - -Deno.test('type-safety: allows `unknown` escapes', () => { - const issues = scanSourcesForAnyIssues([ - { - path: SAMPLE, - text: 'const v = x as unknown as Y;\nconst u: unknown = {};\nconst list: unknown[] = [];', - }, - ]); - assertEquals(issues, []); -}); - -Deno.test('type-safety: ignores any escapes inside comments', () => { - const issues = scanSourcesForAnyIssues([ - { path: SAMPLE, text: '// we must not use as any here\n/* const x = y as any; */' }, - ]); - assertEquals(issues, []); -}); - -Deno.test('type-safety: reports no issues for clean sources', () => { - const issues = scanSourcesForAnyIssues([ - { path: 'packages/core/src/a.ts', text: 'export const x = 1;' }, - { path: 'packages/core/src/b.ts', text: 'export function y(): number {\n return 2;\n}' }, - ]); - assertEquals(issues, []); -}); - -Deno.test('type-safety: reports the first matching escape per line only', () => { - // `as any` matches before `: any`, so only one issue is recorded. - const issues = scanSourcesForAnyIssues([{ path: SAMPLE, text: 'const x = y as any; // : any' }]); - assertEquals(issues.length, 1); - assert(issues[0].text.includes('cast')); -}); - -Deno.test('type-safety: isCodeLine skips comments but keeps code', () => { - assert(!isCodeLine('// foo as any')); - assert(!isCodeLine('* foo as any')); - assert(!isCodeLine('/* foo as any')); - assert(isCodeLine('const x = y as any;')); - assert(isCodeLine(' const x = y as any;')); -}); - -Deno.test('type-safety: returns typed Issue objects', () => { - const issues: Issue[] = scanSourcesForAnyIssues([{ path: SAMPLE, text: 'const x = y as any;' }]); - assertEquals(typeof issues[0].line, 'number'); - assertEquals(typeof issues[0].file, 'string'); -}); diff --git a/tools/check-type-safety.ts b/tools/check-type-safety.ts deleted file mode 100644 index 3512a2784..000000000 --- a/tools/check-type-safety.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Type-safety gate for the current source tree. - * - * Scans active TypeScript/TSX source and tests for explicit `any` type escapes. - * Allowed: `unknown`, `unknown[]`, structured interfaces, generic constraints. - * Forbidden: \x60as any\x60, \x60: any\x60, \x60any[]\x60 in active code. - */ - -import { walk } from '@std/fs/walk'; -import { normalizeSlashes } from './lib/path.ts'; -import { stripCommentsLine } from './lib/text.ts'; - -export interface Issue { - file: string; - line: number; - text: string; -} - -const ANY_PATTERNS = [ - { re: /\bas\s+any\b/u, name: 'unsafe cast' }, - { re: /:\s*any\b/u, name: 'unsafe annotation' }, - { re: /\bany\s*\[\s*\]/u, name: 'unsafe array element' }, -]; - -const ACTIVE_ROOTS = [ - 'packages', - 'tools', - 'www', -]; - -const EXCLUDED_FILES = new Set([ - 'tools/check-type-safety.ts', - 'tools/check-architecture-contract.ts', - // Test files that exercise the detector necessarily contain any-escape tokens, - // so they are excluded from scanning just like the gate tools themselves. - 'tools/check-type-safety.test.ts', - 'tools/check-architecture-contract.test.ts', -]); - -export function isCodeLine(line: string): boolean { - const trimmed = line.trim(); - if (trimmed.startsWith('//')) return false; - if (trimmed.startsWith('*') || trimmed.startsWith('/*')) return false; - return true; -} - -export interface SourceFile { - path: string; - text: string; -} - -/** - * Scan already-loaded source files for explicit `any` escapes. - * - * Pure: does not touch the filesystem and does not apply EXCLUDED_FILES, so it - * can be exercised directly in tests with synthetic inputs. - */ -export function scanSourcesForAnyIssues(files: SourceFile[]): Issue[] { - const issues: Issue[] = []; - for (const file of files) { - const lines = file.text.split(/\r?\n/); - let inBlock = false; - for (let i = 0; i < lines.length; i++) { - const { line, inBlock: next } = stripCommentsLine(lines[i], inBlock); - inBlock = next; - if (!isCodeLine(line)) continue; - for (const { re, name } of ANY_PATTERNS) { - if (re.test(line)) { - issues.push({ file: file.path, line: i + 1, text: name }); - break; - } - } - } - } - return issues; -} - -/** Walk the active roots and read every scannable, non-excluded source file. */ -export async function collectActiveSourceFiles(): Promise { - const files: SourceFile[] = []; - for (const root of ACTIVE_ROOTS) { - try { - for await ( - const { path } of walk(root, { - includeDirs: false, - skip: [/(^|\/)node_modules(\/|$)/, /(^|\/)dist(\/|$)/, /(^|\/)vendor(\/|$)/], - exts: ['ts', 'tsx'], - }) - ) { - const normalized = normalizeSlashes(path); - if (EXCLUDED_FILES.has(normalized)) continue; - files.push({ path: normalized, text: await Deno.readTextFile(normalized) }); - } - } catch { - // Root may not exist in all contexts. - } - } - return files; -} - -async function main(): Promise { - const files = await collectActiveSourceFiles(); - const issues = scanSourcesForAnyIssues(files); - - if (issues.length > 0) { - console.error(`Type-safety check failed: ${issues.length} explicit any escape(s) found.`); - for (const issue of issues) { - console.error(` ${issue.file}:${issue.line} (${issue.text})`); - } - Deno.exit(1); - } - - console.log(`Type-safety check passed (${files.length} active TS/TSX files, 0 explicit any).`); -} - -if (import.meta.main) { - await main(); -} diff --git a/www/content/blog/0007-view-transitions-speculation-rules.md b/www/content/blog/0007-view-transitions-speculation-rules.md index af0b8109e..15f7b88f9 100644 --- a/www/content/blog/0007-view-transitions-speculation-rules.md +++ b/www/content/blog/0007-view-transitions-speculation-rules.md @@ -93,4 +93,4 @@ ISR(Incremental Static Regeneration)是 Next.js 在没有 Islands 架构下 --- -_决策日期: 2026-05-09 | 版本: v0.9.2_ \ No newline at end of file +_决策日期: 2026-05-09 | 版本: v0.9.2_ diff --git a/www/content/blog/0018-dev-build-data-consistency-plugin-data-bridge.md b/www/content/blog/0018-dev-build-data-consistency-plugin-data-bridge.md index 025319a81..81cb4a4fc 100644 --- a/www/content/blog/0018-dev-build-data-consistency-plugin-data-bridge.md +++ b/www/content/blog/0018-dev-build-data-consistency-plugin-data-bridge.md @@ -797,4 +797,4 @@ export default class AboutPage extends LitElement { --- -_提出日期: 2026-05-11 | 状态: ACCEPTED | 目标版本: v0.12.0_ \ No newline at end of file +_提出日期: 2026-05-11 | 状态: ACCEPTED | 目标版本: v0.12.0_ diff --git a/www/content/blog/0019-post-review-improvement-plan.md b/www/content/blog/0019-post-review-improvement-plan.md index 9a6236fcb..11e60d78b 100644 --- a/www/content/blog/0019-post-review-improvement-plan.md +++ b/www/content/blog/0019-post-review-improvement-plan.md @@ -11,7 +11,7 @@ draft: false **PARTIALLY IMPLEMENTED** (v0.12.0–v0.14.0) — See implementation notes below. > 2026-05-13 更新:ADR 0022 (ESM-Native SSG pipeline) + ADR 0023 (Phase reordering) 在 v0.14.0 中落地。Phase 3 从 Vite closeBundle 中抽出为独立模块,URLPattern 替换手写路由解析。 -> +> > 2026-05-12 更新:ADR 0021 (API surface convergence) 在 v0.13.0 中落地,解决了 `ssr-handler.ts` 删除、导出收敛等问题。`@openelement/app` 测试从 0 增加到 16 个。 ## Context diff --git a/www/content/blog/adr-0009-full-repo-simplification.md b/www/content/blog/adr-0009-full-repo-simplification.md index 29b504f72..5dfaa235e 100644 --- a/www/content/blog/adr-0009-full-repo-simplification.md +++ b/www/content/blog/adr-0009-full-repo-simplification.md @@ -61,7 +61,7 @@ return renderEntry(descriptor); 同时该文件 re-export 了 `buildEntryDescriptor`、`renderEntry`、`EntryDescriptor`——三者已各自在原文件中独立导出。 -**方案**: +**方案**: - 将 `generateHonoEntryCode` + `HonoEntryOptions` 移到 `entry-renderer.ts` 底部 - 删除 `hono-entry.ts` - 所有导入点改为从 `entry-renderer.ts` 导入 @@ -223,7 +223,7 @@ export { **问题**: 这是历史遗留。`render-dsd.ts` 的核心职责是 DSD 渲染,不应承担 escape 工具的导出。 -**方案**: +**方案**: - 保留 `deno.json` 中 `./html-escape` 导出路径 - 在 `render-dsd.ts` 的 re-export 块上方添加 `@deprecated` 注释 - 下一个大版本移除 re-export @@ -369,7 +369,7 @@ export { wrapInDocument } from './ssr-handler.js'; 这些都是 CLI 工具的内部函数,不应通过公共 API 暴露。 -**方案**: +**方案**: - 将 CLI 专用函数标记为 `@internal`(在 deno.json 的 exports 中不导出) - 或将其移到 `@openelement/core/cli/*` 导出路径下 @@ -453,7 +453,7 @@ Layer 0: ADR 0008 (Phase C+B → A → D) └─→ Layer 3.1-3.4: 导出精简与拆分 (Layer 1 完成后) ``` -**关键约束**: +**关键约束**: - Layer 1.1–1.6 和 1.8 **全部互相独立**,可以并行执行 - Layer 1.7 和 1.9 **依赖 ADR 0008 Phase C** - Layer 3 的导出变更 **依赖 Layer 1 完成后**(避免中途改导出路径两次) @@ -488,4 +488,4 @@ Layer 0: ADR 0008 (Phase C+B → A → D) --- -_方案日期: 2026-05-10 | 基于 ADR 0008 + 全仓库审查_ \ No newline at end of file +_方案日期: 2026-05-10 | 基于 ADR 0008 + 全仓库审查_ From e155e645bdf23cf6aaf265da6e0c069984889f20 Mon Sep 17 00:00:00 2001 From: DevBot Date: Fri, 4 Sep 2026 00:59:24 +0800 Subject: [PATCH 2/3] fix(governance): satisfy actionlint shellcheck integration (#1156 B2.6) actionlint on the ubuntu runner runs shellcheck over every run block (absent locally on macOS); fix the six findings it reported: four unused loop counters renamed to _, an intentional SC2086 word-split and an intentional SC2016 single-quoted EICAR signature documented with in-band disable comments. --- .github/workflows/autoflow-ci.yml | 2 +- .github/workflows/autoflow-release.yml | 2 ++ .github/workflows/fullstack-deploy-smoke.yml | 4 +++- .github/workflows/supabase-project-smoke.yml | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/autoflow-ci.yml b/.github/workflows/autoflow-ci.yml index b62dc3eb0..a75146f2d 100644 --- a/.github/workflows/autoflow-ci.yml +++ b/.github/workflows/autoflow-ci.yml @@ -124,7 +124,7 @@ jobs: OPEN_ELEMENT_PORT=4891 OPEN_ELEMENT_HOST=127.0.0.1 node dist/server/serve.mjs & server_pid=$! trap 'kill $server_pid 2>/dev/null || true' EXIT - for i in $(seq 1 50); do + for _ in $(seq 1 50); do curl -sf -o /dev/null http://127.0.0.1:4891/ && break sleep 0.2 done diff --git a/.github/workflows/autoflow-release.yml b/.github/workflows/autoflow-release.yml index fa8ec4be1..2d9574fca 100644 --- a/.github/workflows/autoflow-release.yml +++ b/.github/workflows/autoflow-release.yml @@ -111,4 +111,6 @@ jobs: RELEASE_DRY_RUN: ${{ inputs.dry_run && '--dry-run' || '' }} PR_CI_EVIDENCE: .artifacts/pr-ci/pr-full-ci-evidence.json run: | + # shellcheck disable=SC2086 # RELEASE_DRY_RUN is intentionally + # word-split: it is either empty or the single flag --dry-run. deno task autoflow:publish-existing --to "$RELEASE_VERSION" --pr-ci "$PR_CI_EVIDENCE" $RELEASE_DRY_RUN diff --git a/.github/workflows/fullstack-deploy-smoke.yml b/.github/workflows/fullstack-deploy-smoke.yml index 9fd560755..cf21083ce 100644 --- a/.github/workflows/fullstack-deploy-smoke.yml +++ b/.github/workflows/fullstack-deploy-smoke.yml @@ -189,7 +189,7 @@ jobs: set -e mkdir -p .smoke record() { echo "{\"check\":\"$1\",\"result\":\"$2\"}" >> .smoke/results.jsonl; } - for i in 1 2 3 4 5 6; do + for _ in 1 2 3 4 5 6; do code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$WORKER_URL/" || true) [ "$code" = "200" ] && break sleep 10 @@ -338,6 +338,8 @@ jobs: eicar_name="scanner-eicar-$suffix.txt" printf 'OpenElement scanner qualification clean fixture.\n' > "$clean_file" # Standard EICAR test string, generated only in the ephemeral runner. + # shellcheck disable=SC2016 # the single-quoted EICAR signature must + # not expand its $ sequences. printf '%s' 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > "$eicar_file" for fixture in clean eicar; do name_var="${fixture}_name"; file_var="${fixture}_file" diff --git a/.github/workflows/supabase-project-smoke.yml b/.github/workflows/supabase-project-smoke.yml index c9677c6af..9db424089 100644 --- a/.github/workflows/supabase-project-smoke.yml +++ b/.github/workflows/supabase-project-smoke.yml @@ -181,7 +181,7 @@ jobs: set -euo pipefail deno task build OPEN_ELEMENT_PORT=4173 nohup deno task start > "$GITHUB_WORKSPACE/.smoke/server.log" 2>&1 & - for i in $(seq 1 30); do + for _ in $(seq 1 30); do if curl -s -o /dev/null http://127.0.0.1:4173/; then break; fi sleep 1 done @@ -383,7 +383,7 @@ jobs: # instead of racing the invalidation; the assertion itself — the # object must become inaccessible — is unchanged. after_delete="" - for i in $(seq 1 12); do + for _ in $(seq 1 12); do after_delete=$(curl -s -o /dev/null -w '%{http_code}' \ "$SUPABASE_URL/storage/v1/object/notes-attachments/$STORAGE_POLICY_KEY" \ -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $token_a") From f1db34586279d7ee7b0bf903645ae62eb31d8818 Mon Sep 17 00:00:00 2001 From: DevBot Date: Fri, 4 Sep 2026 01:05:21 +0800 Subject: [PATCH 3/3] fix(governance): add Dependabot cooldown for action updates (#1156 B2.6) zizmor-action audits dependabot.yml in addition to workflows (its default input collection); fix the dependabot-cooldown finding with a 7-day bake-in period for github-actions updates. --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 800498342..cba74787c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,4 +4,8 @@ updates: directory: / schedule: interval: weekly + cooldown: + # zizmor dependabot-cooldown (#1156 B2.6): let fresh action releases + # bake for a week before Dependabot proposes them. + default-days: 7 open-pull-requests-limit: 5