@workerstack/repo 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +0 -3
- package/agents/global-settings/antigravity.json +3 -0
- package/agents/global-settings/claude.json +6 -0
- package/agents/global-settings/codex.toml +2 -0
- package/agents/global-settings/cursor.json +3 -0
- package/agents/global-settings/grok.toml +36 -0
- package/agents/global-settings/manifest.json +36 -0
- package/agents/grok/hooks/orchestrate-parent.json +60 -0
- package/agents/grok/hooks/rtk.json +16 -0
- package/agents/grok/hooks/rulesync.json +3 -0
- package/agents/grok/roles/scout.toml +5 -0
- package/agents/hooks.jsonc +67 -0
- package/agents/mcp-policy.jsonc +16 -0
- package/agents/rules/overview.md +21 -0
- package/agents/settings/claude.json +5 -0
- package/agents/skills/ask/SKILL.md +13 -0
- package/agents/skills/auto/SKILL.md +13 -0
- package/agents/skills/away/SKILL.md +19 -0
- package/agents/skills/blast-radius/SKILL.md +54 -0
- package/agents/skills/bro/SKILL.md +9 -0
- package/agents/skills/browser/SKILL.md +16 -0
- package/agents/skills/clasp/SKILL.md +11 -0
- package/agents/skills/commit/SKILL.md +35 -0
- package/agents/skills/commit-and-push/SKILL.md +9 -0
- package/agents/skills/commit-push/SKILL.md +59 -0
- package/agents/skills/commit-push-deploy/SKILL.md +74 -0
- package/agents/skills/commit-push-release/SKILL.md +75 -0
- package/agents/skills/correct/SKILL.md +39 -0
- package/agents/skills/do/SKILL.md +21 -0
- package/agents/skills/explain/SKILL.md +13 -0
- package/agents/skills/google-workspace/SKILL.md +12 -0
- package/agents/skills/handoff/SKILL.md +81 -0
- package/agents/skills/implement-plan/SKILL.md +29 -0
- package/agents/skills/implement-roadmap/SKILL.md +27 -0
- package/agents/skills/jev/SKILL.md +17 -0
- package/agents/skills/listen/SKILL.md +15 -0
- package/agents/skills/pickup/SKILL.md +58 -0
- package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
- package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
- package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
- package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
- package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
- package/agents/skills/principle-experience-first/SKILL.md +22 -0
- package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
- package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
- package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
- package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
- package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
- package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
- package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
- package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
- package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
- package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
- package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
- package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
- package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
- package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
- package/agents/skills/review-plan/SKILL.md +28 -0
- package/agents/skills/review-roadmap/SKILL.md +28 -0
- package/agents/skills/show-me/SKILL.md +21 -0
- package/agents/skills/speak/SKILL.md +15 -0
- package/agents/skills/su/SKILL.md +11 -0
- package/agents/skills/teach/SKILL.md +25 -0
- package/agents/skills/tmp/SKILL.md +54 -0
- package/agents/skills/typescript-best-practices/SKILL.md +32 -0
- package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
- package/agents/skills/unslop/SKILL.md +62 -0
- package/agents/skills/write-plan/SKILL.md +33 -0
- package/agents/skills/write-roadmap/SKILL.md +66 -0
- package/agents/subagents/implementer-deep.md +57 -0
- package/agents/subagents/implementer-fast.md +57 -0
- package/agents/subagents/orchestrator.md +43 -0
- package/agents/subagents/researcher.md +47 -0
- package/agents/subagents/scout.md +44 -0
- package/bin/repo.ts +4 -0
- package/cli/actionlint-default.test.ts +38 -0
- package/cli/actionlint-download.test.ts +162 -0
- package/cli/actionlint-real.test.ts +33 -0
- package/cli/actionlint.constants.ts +43 -0
- package/cli/actionlint.test.ts +108 -0
- package/cli/actionlint.ts +178 -0
- package/cli/bin.test.ts +54 -0
- package/cli/bin.ts +40 -0
- package/cli/check-changelog.test.ts +40 -0
- package/cli/check-changelog.ts +12 -0
- package/cli/check-comments.test.ts +157 -0
- package/cli/check-comments.ts +123 -0
- package/cli/check-env-modules.test.ts +85 -0
- package/cli/check-env-modules.ts +56 -0
- package/cli/check-magic.test.ts +139 -0
- package/cli/check-magic.ts +120 -0
- package/cli/check-manifests.constants.ts +4 -0
- package/cli/check-manifests.test.ts +210 -0
- package/cli/check-manifests.ts +71 -0
- package/cli/check-nightly.test.ts +103 -0
- package/cli/check-nightly.ts +37 -0
- package/cli/check-ratchets-missing.test.ts +46 -0
- package/cli/check-ratchets-mutation.test.ts +49 -0
- package/cli/check-ratchets.constants.ts +20 -0
- package/cli/check-ratchets.test.ts +190 -0
- package/cli/check-ratchets.ts +193 -0
- package/cli/check-rejections.test.ts +220 -0
- package/cli/check-rejections.ts +153 -0
- package/cli/check-reports.ts +37 -0
- package/cli/check-tsconfig.test.ts +507 -0
- package/cli/check-tsconfig.ts +205 -0
- package/cli/commands.test.ts +114 -0
- package/cli/commands.ts +218 -0
- package/cli/commit-changelog.ts +27 -0
- package/cli/commit-msg-changelog.test.ts +93 -0
- package/cli/commit-msg-range.test.ts +126 -0
- package/cli/commit-msg-red-proof.test.ts +147 -0
- package/cli/commit-msg-repo.test.ts +153 -0
- package/cli/commit-msg.test.ts +101 -0
- package/cli/commit-msg.ts +194 -0
- package/cli/commit-range.constants.ts +17 -0
- package/cli/commit-range.ts +57 -0
- package/cli/config-knip.test.ts +70 -0
- package/cli/config-nightly.test.ts +84 -0
- package/cli/config-tdd.test.ts +46 -0
- package/cli/config.test.ts +576 -0
- package/cli/config.ts +261 -0
- package/cli/conventional.ts +9 -0
- package/cli/coverage-config.ts +62 -0
- package/cli/coverage.constants.ts +6 -0
- package/cli/coverage.test.ts +199 -0
- package/cli/coverage.ts +127 -0
- package/cli/env.constants.ts +10 -0
- package/cli/env.test.ts +56 -0
- package/cli/env.ts +72 -0
- package/cli/fixtures/base-config.ts +13 -0
- package/cli/fixtures/gate-world.ts +48 -0
- package/cli/fixtures/knip-run.ts +38 -0
- package/cli/fixtures/mutate-fakes.ts +137 -0
- package/cli/fixtures/planted-repo.ts +50 -0
- package/cli/fixtures/publish-world.ts +71 -0
- package/cli/fixtures/red-proof-repo.ts +72 -0
- package/cli/fixtures/release-context.ts +15 -0
- package/cli/fixtures/release-world.ts +111 -0
- package/cli/fixtures/stub-bin.ts +40 -0
- package/cli/guard-diagnostics.test.ts +157 -0
- package/cli/guard-diagnostics.ts +195 -0
- package/cli/init.test.ts +487 -0
- package/cli/init.ts +275 -0
- package/cli/knip-check-ignores.test.ts +106 -0
- package/cli/knip-check-real.test.ts +65 -0
- package/cli/knip-check.constants.ts +46 -0
- package/cli/knip-check.test.ts +114 -0
- package/cli/knip-check.ts +169 -0
- package/cli/knip-schema.ts +67 -0
- package/cli/known-failures.constants.ts +22 -0
- package/cli/known-failures.test.ts +138 -0
- package/cli/known-failures.ts +131 -0
- package/cli/lint/as-needs-reason.ts +41 -0
- package/cli/lint/boundaries.ts +73 -0
- package/cli/lint/class-file.ts +39 -0
- package/cli/lint/config-file.ts +32 -0
- package/cli/lint/entry-file.ts +54 -0
- package/cli/lint/env-in-config.ts +66 -0
- package/cli/lint/filename-matches-export.ts +63 -0
- package/cli/lint/index-file.ts +22 -0
- package/cli/lint/index.ts +47 -0
- package/cli/lint/is-reason.ts +6 -0
- package/cli/lint/manifest.ts +46 -0
- package/cli/lint/names.ts +59 -0
- package/cli/lint/no-commented-out-code.ts +105 -0
- package/cli/lint/no-mock-of-subject.ts +64 -0
- package/cli/lint/no-module-side-effects.ts +94 -0
- package/cli/lint/no-module-state.ts +58 -0
- package/cli/lint/no-side-effect-import.ts +17 -0
- package/cli/lint/no-v8-ignore.ts +18 -0
- package/cli/lint/script-rules.ts +9 -0
- package/cli/lint/skip-on-platform.constants.ts +8 -0
- package/cli/lint/skip-on-platform.ts +118 -0
- package/cli/lint/todo-needs-id.ts +63 -0
- package/cli/lint/types-file.ts +28 -0
- package/cli/lint-schema.ts +32 -0
- package/cli/magic/fixtures/magic-repo.ts +64 -0
- package/cli/magic/magic-hit.ts +2 -0
- package/cli/magic/magic-nodes.ts +44 -0
- package/cli/magic/magic-numbers.test.ts +99 -0
- package/cli/magic/magic-numbers.ts +117 -0
- package/cli/magic/magic-strings.test.ts +38 -0
- package/cli/magic/magic-strings.ts +81 -0
- package/cli/main.test.ts +853 -0
- package/cli/main.ts +292 -0
- package/cli/mutate-args.ts +79 -0
- package/cli/mutate-check.test.ts +147 -0
- package/cli/mutate-deps.ts +42 -0
- package/cli/mutate-edges.test.ts +150 -0
- package/cli/mutate-files.test.ts +100 -0
- package/cli/mutate-files.ts +31 -0
- package/cli/mutate-floors.test.ts +91 -0
- package/cli/mutate-floors.ts +104 -0
- package/cli/mutate-real-deps.ts +57 -0
- package/cli/mutate-real.test.ts +96 -0
- package/cli/mutate-report.ts +96 -0
- package/cli/mutate-run.ts +128 -0
- package/cli/mutate-shards.ts +64 -0
- package/cli/mutate.constants.ts +67 -0
- package/cli/mutate.test.ts +182 -0
- package/cli/mutate.ts +64 -0
- package/cli/nested-vitest-flags.ts +8 -0
- package/cli/nightly-config.constants.ts +14 -0
- package/cli/nightly-config.ts +62 -0
- package/cli/prompt.test.ts +202 -0
- package/cli/prompt.ts +1 -0
- package/cli/red-proof-edges.test.ts +78 -0
- package/cli/red-proof-failures.test.ts +82 -0
- package/cli/red-proof-tree.test.ts +157 -0
- package/cli/red-proof-verdict.test.ts +44 -0
- package/cli/red-proof.constants.ts +14 -0
- package/cli/red-proof.ts +202 -0
- package/cli/release/changelog-parse.ts +83 -0
- package/cli/release/changelog.test.ts +141 -0
- package/cli/release/changelog.ts +106 -0
- package/cli/release/gate-refusals.test.ts +85 -0
- package/cli/release/gate.test.ts +63 -0
- package/cli/release/gate.ts +70 -0
- package/cli/release/git.ts +66 -0
- package/cli/release/github.test.ts +128 -0
- package/cli/release/github.ts +46 -0
- package/cli/release/list-dir.test.ts +11 -0
- package/cli/release/list-dir.ts +6 -0
- package/cli/release/main.test.ts +48 -0
- package/cli/release/main.ts +51 -0
- package/cli/release/manifest.test.ts +24 -0
- package/cli/release/manifest.ts +34 -0
- package/cli/release/pack.test.ts +107 -0
- package/cli/release/pack.ts +76 -0
- package/cli/release/plan.ts +127 -0
- package/cli/release/publish-run.test.ts +98 -0
- package/cli/release/publish.test.ts +127 -0
- package/cli/release/publish.ts +125 -0
- package/cli/release/refuse.ts +5 -0
- package/cli/release/release-messages.constants.ts +96 -0
- package/cli/release/release-refusals.test.ts +149 -0
- package/cli/release/release.constants.ts +128 -0
- package/cli/release/release.test.ts +199 -0
- package/cli/release/release.ts +145 -0
- package/cli/release/version.test.ts +118 -0
- package/cli/release/version.ts +69 -0
- package/cli/release/wait.test.ts +101 -0
- package/cli/release/wait.ts +95 -0
- package/cli/script-diagnostics.test.ts +76 -0
- package/cli/script-diagnostics.ts +62 -0
- package/cli/scripts/board.ts +519 -0
- package/cli/scripts/browser.ts +994 -0
- package/cli/scripts/change-set.ts +105 -0
- package/cli/scripts/check-globals.ts +199 -0
- package/cli/scripts/ci-skips.constants.ts +17 -0
- package/cli/scripts/ci-skips.ts +118 -0
- package/cli/scripts/ci-wait-run.ts +28 -0
- package/cli/scripts/ci-wait.constants.ts +44 -0
- package/cli/scripts/ci-wait.ts +175 -0
- package/cli/scripts/ci.constants.ts +15 -0
- package/cli/scripts/ci.ts +208 -0
- package/cli/scripts/clean.ts +363 -0
- package/cli/scripts/comments-step.ts +15 -0
- package/cli/scripts/doctor.ts +828 -0
- package/cli/scripts/emit-agent-shell-tools.ts +91 -0
- package/cli/scripts/emit-mcp-policy.ts +402 -0
- package/cli/scripts/folder-map.ts +353 -0
- package/cli/scripts/gate-fat-tools.ts +1095 -0
- package/cli/scripts/gitignore-guard.ts +338 -0
- package/cli/scripts/google-workspace.ts +779 -0
- package/cli/scripts/hook-command.ts +72 -0
- package/cli/scripts/hook-input.ts +178 -0
- package/cli/scripts/hook-io.constants.ts +5 -0
- package/cli/scripts/hook-io.ts +46 -0
- package/cli/scripts/hooks-union.ts +469 -0
- package/cli/scripts/interactive.ts +30 -0
- package/cli/scripts/jev.ts +420 -0
- package/cli/scripts/job.ts +934 -0
- package/cli/scripts/jsonc.ts +9 -0
- package/cli/scripts/lessons.ts +564 -0
- package/cli/scripts/lint-baseline.ts +233 -0
- package/cli/scripts/lint-main.ts +24 -0
- package/cli/scripts/lint-skips.ts +139 -0
- package/cli/scripts/lint.constants.ts +6 -0
- package/cli/scripts/lint.ts +142 -0
- package/cli/scripts/listen.ts +962 -0
- package/cli/scripts/managed-text.ts +47 -0
- package/cli/scripts/orchestrate-parent.ts +1155 -0
- package/cli/scripts/parse-need.ts +81 -0
- package/cli/scripts/persist-handoff.ts +228 -0
- package/cli/scripts/persist-implementation.ts +276 -0
- package/cli/scripts/persist-plan.ts +888 -0
- package/cli/scripts/persist-roadmap.ts +478 -0
- package/cli/scripts/persist-tmp.ts +177 -0
- package/cli/scripts/persist.ts +54 -0
- package/cli/scripts/phase-files.ts +121 -0
- package/cli/scripts/prompt.ts +38 -0
- package/cli/scripts/purge-global-mcps.ts +313 -0
- package/cli/scripts/rejections-step.ts +37 -0
- package/cli/scripts/rtk-grok.ts +102 -0
- package/cli/scripts/scrub-spawn-prompt.ts +103 -0
- package/cli/scripts/scrub-text-lib.ts +58 -0
- package/cli/scripts/session-map.ts +69 -0
- package/cli/scripts/setup.ts +862 -0
- package/cli/scripts/skill-conflicts.ts +82 -0
- package/cli/scripts/speak.ts +370 -0
- package/cli/scripts/strategy.ts +168 -0
- package/cli/scripts/sync-global-settings.ts +316 -0
- package/cli/scripts/turn-steps.ts +249 -0
- package/cli/scripts/turn.ts +157 -0
- package/cli/scripts/typecheck-step.ts +43 -0
- package/cli/scripts/vscode-tasks.ts +175 -0
- package/cli/scripts/workspace-root.ts +160 -0
- package/cli/scripts/worktree.ts +806 -0
- package/cli/spawn.test.ts +34 -0
- package/cli/spawn.ts +27 -0
- package/cli/staged-change.constants.ts +11 -0
- package/cli/staged-change.ts +79 -0
- package/cli/stryker-preload.test.ts +50 -0
- package/cli/stryker-preload.ts +9 -0
- package/cli/sync/agents/frontmatter.test.ts +197 -0
- package/cli/sync/agents/frontmatter.ts +140 -0
- package/cli/sync/agents/hooks.test.ts +517 -0
- package/cli/sync/agents/hooks.ts +401 -0
- package/cli/sync/agents/rules.test.ts +229 -0
- package/cli/sync/agents/rules.ts +84 -0
- package/cli/sync/agents/sources.test.ts +375 -0
- package/cli/sync/agents/sources.ts +244 -0
- package/cli/sync/agents/subagents.test.ts +149 -0
- package/cli/sync/agents/subagents.ts +158 -0
- package/cli/sync/agents-text.test.ts +31 -0
- package/cli/sync/agents-text.ts +36 -0
- package/cli/sync/fixtures/copied.README.md +35 -0
- package/cli/sync/fixtures/copied.tgz +0 -0
- package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
- package/cli/sync/fixtures/maps-ws.tgz +0 -0
- package/cli/sync/git-files.test.ts +340 -0
- package/cli/sync/git-files.ts +197 -0
- package/cli/sync/git-hooks.test.ts +94 -0
- package/cli/sync/golden.test.ts +15 -0
- package/cli/sync/hooks-mode.test.ts +99 -0
- package/cli/sync/hosts.ts +28 -0
- package/cli/sync/knip-config.constants.ts +4 -0
- package/cli/sync/knip-config.test.ts +147 -0
- package/cli/sync/knip-config.ts +108 -0
- package/cli/sync/manifest.test.ts +78 -0
- package/cli/sync/manifest.ts +60 -0
- package/cli/sync/maps.test.ts +304 -0
- package/cli/sync/maps.ts +297 -0
- package/cli/sync/migrate.test.ts +2000 -0
- package/cli/sync/migrate.ts +1104 -0
- package/cli/sync/nightly-jobs.test.ts +63 -0
- package/cli/sync/nightly.constants.ts +48 -0
- package/cli/sync/nightly.test.ts +100 -0
- package/cli/sync/nightly.ts +75 -0
- package/cli/sync/outputs.test.ts +351 -0
- package/cli/sync/outputs.ts +168 -0
- package/cli/sync/oxlint-config.test.ts +178 -0
- package/cli/sync/oxlint-config.ts +168 -0
- package/cli/sync/release-jobs.test.ts +29 -0
- package/cli/sync/release-reports.test.ts +31 -0
- package/cli/sync/release-workflow.constants.ts +61 -0
- package/cli/sync/release-workflow.test.ts +192 -0
- package/cli/sync/release-workflow.ts +86 -0
- package/cli/sync/release.constants.ts +93 -0
- package/cli/sync/release.test.ts +187 -0
- package/cli/sync/release.ts +112 -0
- package/cli/sync/repo-dir.test.ts +371 -0
- package/cli/sync/repo-dir.ts +205 -0
- package/cli/sync/standards.constants.ts +8 -0
- package/cli/sync/standards.test.ts +206 -0
- package/cli/sync/standards.ts +179 -0
- package/cli/sync/stock-history.json +2796 -0
- package/cli/sync/stock-history.test.ts +66 -0
- package/cli/sync/stock-history.ts +84 -0
- package/cli/sync/stock.test.ts +47 -0
- package/cli/sync/stock.ts +24 -0
- package/cli/sync/trees.test.ts +621 -0
- package/cli/sync/trees.ts +280 -0
- package/cli/sync/validate.test.ts +299 -0
- package/cli/sync/validate.ts +402 -0
- package/cli/sync/workspace.test.ts +180 -0
- package/cli/sync/workspace.ts +138 -0
- package/cli/sync/write.test.ts +341 -0
- package/cli/sync/write.ts +231 -0
- package/cli/sync.test.ts +1495 -0
- package/cli/sync.ts +360 -0
- package/cli/tdd-scope.test.ts +36 -0
- package/cli/tdd-scope.ts +41 -0
- package/cli/test-files.ts +16 -0
- package/cli/ts-program.ts +69 -0
- package/cli/tsconfig.constants.ts +8 -0
- package/cli/workerstack-config.types.ts +21 -0
- package/dist/cli/env.d.ts +10 -0
- package/dist/cli/env.js +64 -0
- package/dist/presets/temp-root-env.d.ts +10 -0
- package/dist/presets/temp-root-env.js +20 -0
- package/dist/presets/temp-root.constants.d.ts +6 -0
- package/dist/presets/temp-root.constants.js +6 -0
- package/dist/presets/temp-root.d.ts +2 -0
- package/dist/presets/temp-root.js +60 -0
- package/dist/presets/vitest.constants.d.ts +16 -0
- package/dist/presets/vitest.constants.js +16 -0
- package/dist/presets/vitest.d.ts +11 -0
- package/dist/presets/vitest.js +67 -0
- package/dist/presets/workers-plugin.d.ts +10 -0
- package/dist/presets/workers-plugin.js +54 -0
- package/package.json +141 -4
- package/references/INDEX.md +100 -0
- package/references/browser.md +66 -0
- package/references/cascade.md +15 -0
- package/references/clasp.md +21 -0
- package/references/cloudflare.md +36 -0
- package/references/google-workspace.md +79 -0
- package/references/hooks.md +103 -0
- package/references/implementation-format.md +119 -0
- package/references/jev.md +56 -0
- package/references/lessons.md +73 -0
- package/references/listen.md +80 -0
- package/references/map.md +107 -0
- package/references/output.md +47 -0
- package/references/plan-format.md +264 -0
- package/references/review-loop.md +48 -0
- package/references/roadmap-format.md +104 -0
- package/references/rtk.md +27 -0
- package/references/scope.md +76 -0
- package/references/setup.md +42 -0
- package/references/speak.md +59 -0
- package/tsconfig/base.json +28 -0
- package/tsconfig/bun.d.ts +1 -0
- package/tsconfig/bun.json +8 -0
- package/tsconfig/reset.d.ts +1 -0
- package/tsconfig/strict.json +12 -0
- package/tsconfig/web.json +7 -0
- package/tsconfig/worker.d.ts +1 -0
- package/tsconfig/worker.json +8 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { changelogProblems, type Kind } from "./commit-changelog.ts";
|
|
4
|
+
import { HEADER, isBreaking } from "./conventional.ts";
|
|
5
|
+
import { commitRange } from "./commit-range.ts";
|
|
6
|
+
import { NAME_A_SOURCE, RANGE_ARGS, RANGE_FLAG } from "./commit-range.constants.ts";
|
|
7
|
+
import { DEFAULT_MAX_CHANGED_LINES, findRoot, loadConfig } from "./config.ts";
|
|
8
|
+
import { type NameStatus, parseNameStatus, redProof as realRedProof, type RedProofResult } from "./red-proof.ts";
|
|
9
|
+
import { stagedChange } from "./staged-change.ts";
|
|
10
|
+
import { runGit } from "./sync/standards.ts";
|
|
11
|
+
import { readWorkspace, type Workspace } from "./sync/workspace.ts";
|
|
12
|
+
import { isTestFile } from "./test-files.ts";
|
|
13
|
+
import { type Owner, ownerLookup, proofApplies } from "./tdd-scope.ts";
|
|
14
|
+
|
|
15
|
+
/** The Conventional Commits types a commit may use. */
|
|
16
|
+
export const TYPES: string[] = ["feat", "fix", "perf", "refactor", "test", "docs", "build", "ci", "chore", "revert"];
|
|
17
|
+
|
|
18
|
+
/** Scopes every monorepo accepts besides its workspace package names. */
|
|
19
|
+
export const REPO_SCOPES: string[] = ["repo", "deps", "ci"];
|
|
20
|
+
|
|
21
|
+
const MAX_HEADER = 100;
|
|
22
|
+
const NEEDS_TEST = new Set(["feat", "fix"]);
|
|
23
|
+
/** The line above the diff `git commit --verbose` appends; everything from it down is not the message. */
|
|
24
|
+
const SCISSORS = "# ------------------------ >8 ------------------------";
|
|
25
|
+
|
|
26
|
+
/** What a message is checked against: the staged paths and changed lines, the limit, and the workspace's scopes. */
|
|
27
|
+
export type CommitFacts = {
|
|
28
|
+
stagedFiles: string[];
|
|
29
|
+
changedLines: number;
|
|
30
|
+
maxChangedLines: number;
|
|
31
|
+
scopes?: string[] | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** What the repo's config and workspace set: the size limit, and the scopes of a monorepo. */
|
|
35
|
+
export type CommitRules = {
|
|
36
|
+
maxChangedLines: number;
|
|
37
|
+
scopes?: string[] | undefined;
|
|
38
|
+
/** Whether a feat or fix is held to red-proof even in private packages. */
|
|
39
|
+
tdd: boolean;
|
|
40
|
+
/** The package owning a repo-relative file. */
|
|
41
|
+
owners: (file: string) => Owner;
|
|
42
|
+
};
|
|
43
|
+
/** Where `repo commit-msg` runs and prints; `redProof` is the red-proof runner, replaceable in tests. */
|
|
44
|
+
export type CommitMsgIo = {
|
|
45
|
+
cwd: string;
|
|
46
|
+
stderr: (text: string) => void;
|
|
47
|
+
redProof?: (root: string, change: NameStatus[]) => RedProofResult;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The scope problem, if any: kebab-case always, and one of `scopes` or the repo scopes when `scopes` is given. */
|
|
51
|
+
function scopeProblems(scope: string | undefined, scopes: string[] | undefined): string[] {
|
|
52
|
+
if (scope === undefined) return [];
|
|
53
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(scope)) return [`the scope "${scope}" must be lowercase kebab-case`];
|
|
54
|
+
if (scopes === undefined || scopes.includes(scope) || REPO_SCOPES.includes(scope)) return [];
|
|
55
|
+
const known = `one of ${REPO_SCOPES.join(", ")} (${scopes.join(", ")})`;
|
|
56
|
+
return [`unknown scope "${scope}"; use a workspace package name or ${known}`];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The size problem, if any: over the limit, the message needs a body line saying why. */
|
|
60
|
+
function sizeProblems(body: readonly string[], { changedLines, maxChangedLines }: CommitFacts): string[] {
|
|
61
|
+
if (changedLines <= maxChangedLines || body.some((line) => line.trim() !== "")) return [];
|
|
62
|
+
return [
|
|
63
|
+
`the commit changes ${changedLines} lines, over the limit of ${maxChangedLines}; add a body line saying why, or split it`,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The lines of the message itself: no comment, and nothing from the verbose diff's scissors line down. */
|
|
68
|
+
function messageLines(message: string): string[] {
|
|
69
|
+
const all = message.split("\n");
|
|
70
|
+
const scissors = all.indexOf(SCISSORS);
|
|
71
|
+
return (scissors === -1 ? all : all.slice(0, scissors)).filter((line) => !line.startsWith("#"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What checking a message found: every problem, and the header's type and breaking mark when the header parsed. */
|
|
75
|
+
type Inspected = { problems: string[]; kind?: Kind };
|
|
76
|
+
|
|
77
|
+
/** Checks one message, keeping the header's type so the caller need not parse it again. */
|
|
78
|
+
function inspect(message: string, facts: CommitFacts): Inspected {
|
|
79
|
+
const lines = messageLines(message);
|
|
80
|
+
const at = lines.findIndex((line) => line.trim() !== "");
|
|
81
|
+
const header = lines[at];
|
|
82
|
+
if (header === undefined) return { problems: ["the commit message is empty"] };
|
|
83
|
+
if (header.startsWith("Merge ")) return { problems: [] };
|
|
84
|
+
|
|
85
|
+
const match = HEADER.exec(header);
|
|
86
|
+
if (!match)
|
|
87
|
+
return {
|
|
88
|
+
problems: ['the header must be "type(scope)!: subject", e.g. "fix(paths): compare hook paths canonically"'],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// the HEADER regex always captures type and subject; scope is optional
|
|
92
|
+
const { type, scope, subject } = match.groups as { type: string; scope: string | undefined; subject: string };
|
|
93
|
+
const problems: string[] = [];
|
|
94
|
+
if (!TYPES.includes(type)) problems.push(`unknown type "${type}"; use one of ${TYPES.join(", ")}`);
|
|
95
|
+
problems.push(...scopeProblems(scope, facts.scopes));
|
|
96
|
+
if (/^[A-Z]/.test(subject)) problems.push("the subject must start lowercase");
|
|
97
|
+
if (subject.endsWith(".")) problems.push("the subject must not end with a period");
|
|
98
|
+
if (header.length > MAX_HEADER) problems.push(`the header is ${header.length} characters; keep it to ${MAX_HEADER}`);
|
|
99
|
+
if (NEEDS_TEST.has(type) && !facts.stagedFiles.some(isTestFile)) {
|
|
100
|
+
problems.push(`a ${type} commit must include a test change (a *.test.* or *.spec.* file, or a file under e2e/)`);
|
|
101
|
+
}
|
|
102
|
+
const body = lines.slice(at + 1);
|
|
103
|
+
return { problems: [...problems, ...sizeProblems(body, facts)], kind: { type, breaking: isBreaking(header, body) } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks one commit message against the Conventional Commits rules and the size limit, and returns every problem it
|
|
108
|
+
* finds, or an empty list when the message passes. Without `scopes` any kebab-case scope is accepted.
|
|
109
|
+
*/
|
|
110
|
+
export function checkCommitMessage(message: string, facts: CommitFacts): string[] {
|
|
111
|
+
return inspect(message, facts).problems;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The scopes of a monorepo: the last segment of each workspace package name, or its folder; none for one package. */
|
|
115
|
+
function scopesOf(workspace: Workspace): string[] | undefined {
|
|
116
|
+
if (workspace.packages.length === 0) return undefined;
|
|
117
|
+
const names = workspace.packages.map(({ dir, manifest }) => (manifest.name ?? dir).split("/").at(-1) as string);
|
|
118
|
+
return [...new Set(names)].sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The limit and scopes for the repo at or above `cwd`: the defaults without a config, or the problems loading it. */
|
|
122
|
+
async function commitRules(cwd: string): Promise<CommitRules | string[]> {
|
|
123
|
+
const root = findRoot(cwd);
|
|
124
|
+
const owners = ownerLookup(cwd);
|
|
125
|
+
if (root === null) return { maxChangedLines: DEFAULT_MAX_CHANGED_LINES, tdd: false, owners };
|
|
126
|
+
const loaded = await loadConfig(root);
|
|
127
|
+
if (!loaded.ok) return loaded.errors;
|
|
128
|
+
const workspace = readWorkspace(root);
|
|
129
|
+
if (!workspace.ok) return workspace.errors;
|
|
130
|
+
const { tdd, commit } = loaded.config;
|
|
131
|
+
return { maxChangedLines: commit.maxChangedLines, scopes: scopesOf(workspace.workspace), tdd, owners };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The red-proof result for the staged change, or null when it does not apply to it. */
|
|
135
|
+
function proveRed(rules: CommitRules, io: CommitMsgIo): RedProofResult | null {
|
|
136
|
+
const diff = runGit(io.cwd, ["diff", "--cached", "--name-status", "--no-renames", "-z"]);
|
|
137
|
+
const change = parseNameStatus(diff.stdout);
|
|
138
|
+
return proofApplies(change, rules.tdd, rules.owners) ? (io.redProof ?? realRedProof)(io.cwd, change) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Every problem with `message` against the staged change, and the red-proof result when it ran. */
|
|
142
|
+
function judge(
|
|
143
|
+
message: string,
|
|
144
|
+
rules: CommitRules,
|
|
145
|
+
io: CommitMsgIo,
|
|
146
|
+
): { problems: string[]; proof: RedProofResult | null } {
|
|
147
|
+
const staged = stagedChange(io.cwd);
|
|
148
|
+
const facts = { ...rules, stagedFiles: staged.files, changedLines: staged.changedLines };
|
|
149
|
+
const { problems, kind } = inspect(message, facts);
|
|
150
|
+
const proof = kind !== undefined && problems.length === 0 && NEEDS_TEST.has(kind.type) ? proveRed(rules, io) : null;
|
|
151
|
+
if (kind !== undefined) problems.push(...changelogProblems(kind, staged.files, rules.owners));
|
|
152
|
+
if (proof?.ok === false) problems.push(proof.problem);
|
|
153
|
+
return { problems, proof };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The commit-msg check of the message file at `messageFile` (relative to `io.cwd`, the repo top level git runs hooks
|
|
158
|
+
* in) against the staged change and `rules`: prints each problem and returns 0 to let the commit through, 1 to refuse.
|
|
159
|
+
* A passing feat or fix is then held to red-proof where it applies, which prints its line or refuses.
|
|
160
|
+
*/
|
|
161
|
+
export function runCommitMsgHook(messageFile: string, rules: CommitRules, io: CommitMsgIo): number {
|
|
162
|
+
const message = fs.readFileSync(path.resolve(io.cwd, messageFile), "utf8");
|
|
163
|
+
const { problems, proof } = judge(message, rules, io);
|
|
164
|
+
if (problems.length > 0) {
|
|
165
|
+
io.stderr(`commit-msg: the message was refused\n${problems.map((problem) => ` - ${problem}\n`).join("")}`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
if (proof?.ok) io.stderr(`${proof.line}\n`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `repo commit-msg <file>`, the commit-msg git hook, or `--range <base>..<head>` for the commits of a range: `runCommitMsgHook` with the config's size limit and the
|
|
174
|
+
* workspace's scopes, or exit 1 with the reason when the file is not named or the config is broken.
|
|
175
|
+
*/
|
|
176
|
+
export async function main(
|
|
177
|
+
argv: string[],
|
|
178
|
+
io: CommitMsgIo = { cwd: process.cwd(), stderr: (text) => process.stderr.write(text) },
|
|
179
|
+
): Promise<number> {
|
|
180
|
+
const ranged = argv[0] === RANGE_FLAG && argv.length === RANGE_ARGS;
|
|
181
|
+
const [file] = argv;
|
|
182
|
+
if (file === undefined || (file === RANGE_FLAG ? !ranged : argv.length > 1)) {
|
|
183
|
+
io.stderr(NAME_A_SOURCE);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
const rules = await commitRules(io.cwd);
|
|
187
|
+
if (Array.isArray(rules)) {
|
|
188
|
+
for (const error of rules) io.stderr(`repo commit-msg: ${error}\n`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
return ranged
|
|
192
|
+
? commitRange(argv[1] /* the range follows the flag */ as string, { rules, check: checkCommitMessage }, io)
|
|
193
|
+
: runCommitMsgHook(file, rules, io);
|
|
194
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** What `repo commit-msg --range` reads and prints. */
|
|
2
|
+
|
|
3
|
+
/** The `--range` flag. */
|
|
4
|
+
export const RANGE_FLAG = "--range";
|
|
5
|
+
/** A base that names no commit, as a push event of a new branch gives. */
|
|
6
|
+
export const ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
7
|
+
/** The length of an abbreviated sha. */
|
|
8
|
+
export const SHORT_SHA = 7;
|
|
9
|
+
/** A full object name. */
|
|
10
|
+
export const FULL_SHA: RegExp = /^[0-9a-f]{40}$/;
|
|
11
|
+
/** How to call `repo commit-msg`, for the refusal when nothing is named. */
|
|
12
|
+
export const NAME_A_SOURCE =
|
|
13
|
+
"repo commit-msg: name the commit message file as git's commit-msg hook does or pass --range <base>..<head>\n";
|
|
14
|
+
/** The arguments of `--range <base>..<head>`: the flag and the range. */
|
|
15
|
+
export const RANGE_ARGS = 2;
|
|
16
|
+
/** The sides of a range split on `..`. */
|
|
17
|
+
export const RANGE_SIDES = 2;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CommitFacts, CommitMsgIo, CommitRules } from "./commit-msg.ts";
|
|
2
|
+
import { FULL_SHA, RANGE_SIDES, SHORT_SHA, ZERO_SHA } from "./commit-range.constants.ts";
|
|
3
|
+
import { commitChange } from "./staged-change.ts";
|
|
4
|
+
import { runGit } from "./sync/standards.ts";
|
|
5
|
+
|
|
6
|
+
/** The checker of one message, as `checkCommitMessage`. */
|
|
7
|
+
type Check = (message: string, facts: CommitFacts) => string[];
|
|
8
|
+
|
|
9
|
+
/** Whether git succeeds at `cwd` with `args`. */
|
|
10
|
+
const works = (cwd: string, ...args: string[]): boolean => runGit(cwd, args).status === 0;
|
|
11
|
+
/** The non-empty lines git prints at `cwd` with `args`; none when it fails. */
|
|
12
|
+
const linesOf = (cwd: string, ...args: string[]): string[] => runGit(cwd, args).stdout.split("\n").filter(Boolean);
|
|
13
|
+
|
|
14
|
+
/** The commits to check: those in `base..head` oldest first, or `head` alone, never a merge; null for an unknown range. */
|
|
15
|
+
function commitsOf({ base, head }: { base: string; head: string }, io: CommitMsgIo): string[] | null {
|
|
16
|
+
const tip = head === "" ? "HEAD" : head;
|
|
17
|
+
if (!works(io.cwd, "rev-parse", "--verify", `${tip}^{commit}`)) return null;
|
|
18
|
+
const alone = () => linesOf(io.cwd, "rev-list", "--no-merges", "-n1", tip);
|
|
19
|
+
if (base === "" || base === ZERO_SHA) return alone();
|
|
20
|
+
const known = works(io.cwd, "rev-parse", "--verify", `${base}^{commit}`);
|
|
21
|
+
if (!known && !FULL_SHA.test(base)) return null;
|
|
22
|
+
if (known && works(io.cwd, "merge-base", "--is-ancestor", base, tip)) {
|
|
23
|
+
return linesOf(io.cwd, "rev-list", "--reverse", "--no-merges", `${base}..${tip}`);
|
|
24
|
+
}
|
|
25
|
+
io.stderr(
|
|
26
|
+
`commit-msg: ${base.slice(0, SHORT_SHA)} is not an ancestor of HEAD (a force push?); checking HEAD alone\n`,
|
|
27
|
+
);
|
|
28
|
+
return alone();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `repo commit-msg --range <base>..<head>`: checks each non-merge commit with `how.check` and `how.rules`, as the hook checks a
|
|
33
|
+
* staged change, and prints `commit-msg: <sha7> <subject>: <problem>` per problem. Returns 1 when any, or the range is
|
|
34
|
+
* unknown. A base that is zero, empty or not an ancestor of the head checks the head alone.
|
|
35
|
+
*/
|
|
36
|
+
export function commitRange(range: string, how: { rules: CommitRules; check: Check }, io: CommitMsgIo): number {
|
|
37
|
+
const { rules, check } = how;
|
|
38
|
+
const sides = range.split("..");
|
|
39
|
+
// two sides are read only when the range has exactly two
|
|
40
|
+
const [base, head] = sides /* two sides, or unused */ as [string, string];
|
|
41
|
+
const shas = sides.length === RANGE_SIDES ? commitsOf({ base, head }, io) : null;
|
|
42
|
+
if (shas === null) {
|
|
43
|
+
io.stderr(`repo commit-msg: no commit range "${range}"; use <base>..<head>\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
let failed = false;
|
|
47
|
+
for (const sha of shas) {
|
|
48
|
+
const { files, changedLines } = commitChange(io.cwd, sha);
|
|
49
|
+
const message = runGit(io.cwd, ["log", "-1", "--format=%B", sha]).stdout;
|
|
50
|
+
const subject = runGit(io.cwd, ["log", "-1", "--format=%s", sha]).stdout.trim();
|
|
51
|
+
for (const problem of check(message, { ...rules, stagedFiles: files, changedLines })) {
|
|
52
|
+
io.stderr(`commit-msg: ${sha.slice(0, SHORT_SHA)} ${subject}: ${problem}\n`);
|
|
53
|
+
failed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return failed ? 1 : 0;
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, expect, it } from "vitest";
|
|
5
|
+
import { CONFIG_FILE, loadConfig } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
const temps: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** A temp repo root holding `body` as the default export of workerstack.config.ts. */
|
|
14
|
+
function repo(body: string): string {
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-config-knip-"));
|
|
16
|
+
temps.push(root);
|
|
17
|
+
fs.writeFileSync(path.join(root, CONFIG_FILE), `export default ${body}\n`);
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const refusal = (message: string) => ({ ok: false, errors: [`workerstack.config.ts: ${message}`] });
|
|
22
|
+
|
|
23
|
+
it("resolves knip.ignore to none when the config omits knip", async () => {
|
|
24
|
+
expect(await loadConfig(repo("{}"))).toMatchObject({ ok: true, config: { knip: { ignore: [] } } });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("resolves a file, a dependency and a binary ignore", async () => {
|
|
28
|
+
const body = `{ knip: { ignore: [
|
|
29
|
+
{ file: "src/old.ts", reason: "loaded by name" },
|
|
30
|
+
{ dependency: "oxlint", reason: "spawned by path" },
|
|
31
|
+
{ binary: "tsc", reason: "run by a hook" },
|
|
32
|
+
] } }`;
|
|
33
|
+
expect(await loadConfig(repo(body))).toMatchObject({
|
|
34
|
+
ok: true,
|
|
35
|
+
config: {
|
|
36
|
+
knip: {
|
|
37
|
+
ignore: [
|
|
38
|
+
{ file: "src/old.ts", reason: "loaded by name" },
|
|
39
|
+
{ dependency: "oxlint", reason: "spawned by path" },
|
|
40
|
+
{ binary: "tsc", reason: "run by a hook" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each(['{ file: "a.ts" }', '{ file: "a.ts", reason: "" }'])(
|
|
48
|
+
"refuses the ignore %s without a reason",
|
|
49
|
+
async (entry) => {
|
|
50
|
+
expect(await loadConfig(repo(`{ knip: { ignore: [${entry}] } }`))).toEqual(
|
|
51
|
+
refusal("knip.ignore[0].reason: say why knip cannot see the use"),
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
it.each([
|
|
57
|
+
'{ file: "a.ts", dependency: "x", reason: "why" }',
|
|
58
|
+
'{ file: "a.ts", binary: "x", dependency: "y", reason: "why" }',
|
|
59
|
+
'{ reason: "why" }',
|
|
60
|
+
])("refuses the ignore %s naming not exactly one of its keys", async (entry) => {
|
|
61
|
+
expect(await loadConfig(repo(`{ knip: { ignore: [{ file: "ok.ts", reason: "why" }, ${entry}] } }`))).toEqual(
|
|
62
|
+
refusal("knip.ignore[1]: name exactly one of file, dependency or binary"),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each(['["docs"]', "[]"])("refuses knip without the standards pack (packs %s)", async (list) => {
|
|
67
|
+
expect(await loadConfig(repo(`{ packs: ${list}, knip: {} }`))).toEqual(
|
|
68
|
+
refusal('knip: set only with the standards pack; add "standards" to packs or delete knip'),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { CONFIG_FILE, loadConfig } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
const temps: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** A temp repo root holding `body` as the default export of workerstack.config.ts. */
|
|
14
|
+
function repo(body: string): string {
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-config-nightly-"));
|
|
16
|
+
temps.push(root);
|
|
17
|
+
fs.writeFileSync(path.join(root, CONFIG_FILE), `export default ${body}\n`);
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const withRelease = (nightly: string) => repo(`{ packs: ["release"], nightly: ${nightly} }`);
|
|
22
|
+
const refusal = (message: string) => ({ ok: false, errors: [`workerstack.config.ts: ${message}`] });
|
|
23
|
+
const SHARDS_ERROR = "nightly.shards: must be a whole number from 1 to 32";
|
|
24
|
+
const SECRET_ERROR = (at: number) =>
|
|
25
|
+
`nightly.secrets[${at}]: must be a secret name like CLOUDFLARE_API_TOKEN, not starting with GITHUB_`;
|
|
26
|
+
|
|
27
|
+
describe("nightly", () => {
|
|
28
|
+
it("defaults to one shard and no secrets", async () => {
|
|
29
|
+
expect(await loadConfig(repo("{}"))).toMatchObject({ ok: true, config: { nightly: { shards: 1, secrets: [] } } });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("keeps the shards and secrets a release repo sets", async () => {
|
|
33
|
+
const loaded = await loadConfig(withRelease('{ shards: 8, secrets: ["API_TOKEN", "_X1"] }'));
|
|
34
|
+
expect(loaded).toMatchObject({ ok: true, config: { nightly: { shards: 8, secrets: ["API_TOKEN", "_X1"] } } });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("fills the shards when only the secrets are set", async () => {
|
|
38
|
+
const loaded = await loadConfig(withRelease('{ secrets: ["A"] }'));
|
|
39
|
+
expect(loaded).toMatchObject({ ok: true, config: { nightly: { shards: 1, secrets: ["A"] } } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each(["0", "33", "1.5", '"8"'])("refuses shards %s", async (shards) => {
|
|
43
|
+
expect(await loadConfig(withRelease(`{ shards: ${shards} }`))).toEqual(refusal(SHARDS_ERROR));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it.each(["1", "32"])("accepts shards %s", async (shards) => {
|
|
47
|
+
expect(await loadConfig(withRelease(`{ shards: ${shards} }`))).toMatchObject({ ok: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each(['"api_token"', '"GITHUB_TOKEN"', '"1A"', '""', "7"])("refuses the secret %s", async (secret) => {
|
|
51
|
+
expect(await loadConfig(withRelease(`{ secrets: [${secret}] }`))).toEqual(refusal(SECRET_ERROR(0)));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("names the index of the bad secret", async () => {
|
|
55
|
+
expect(await loadConfig(withRelease('{ secrets: ["A", "b"] }'))).toEqual(refusal(SECRET_ERROR(1)));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("nightly secrets", () => {
|
|
60
|
+
it("refuses a duplicate secret with its count", async () => {
|
|
61
|
+
expect(await loadConfig(withRelease('{ secrets: ["A", "B", "A"] }'))).toEqual(
|
|
62
|
+
refusal('nightly.secrets: "A" is listed twice'),
|
|
63
|
+
);
|
|
64
|
+
expect(await loadConfig(withRelease('{ secrets: ["A", "A", "A"] }'))).toEqual(
|
|
65
|
+
refusal('nightly.secrets: "A" is listed 3 times'),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("refuses secrets that are not a list and keys it does not know", async () => {
|
|
70
|
+
expect(await loadConfig(withRelease('{ secrets: "A" }'))).toEqual(
|
|
71
|
+
refusal("nightly.secrets: must be a list of secret names"),
|
|
72
|
+
);
|
|
73
|
+
expect(await loadConfig(withRelease("{ cron: 1 }"))).toEqual(refusal('nightly: unknown key "cron"'));
|
|
74
|
+
expect(await loadConfig(withRelease("3"))).toEqual(
|
|
75
|
+
refusal("nightly: must be an object like { shards: 8, secrets: [...] }"),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("refuses nightly without the release pack", async () => {
|
|
80
|
+
const message = 'nightly: set only with the release pack; add "release" to packs or delete nightly';
|
|
81
|
+
expect(await loadConfig(repo("{ nightly: { shards: 8 } }"))).toEqual(refusal(message));
|
|
82
|
+
expect(await loadConfig(repo('{ packs: ["docs"], nightly: {} }'))).toEqual(refusal(message));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, expect, it } from "vitest";
|
|
5
|
+
import { CONFIG_FILE, loadConfig } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
const temps: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** A temp repo root holding `body` as the default export of workerstack.config.ts. */
|
|
14
|
+
function repo(body: string): string {
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-config-tdd-"));
|
|
16
|
+
temps.push(root);
|
|
17
|
+
fs.writeFileSync(path.join(root, CONFIG_FILE), `export default ${body}\n`);
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const refusal = (message: string) => ({ ok: false, errors: [`workerstack.config.ts: ${message}`] });
|
|
22
|
+
|
|
23
|
+
it("resolves tdd: true", async () => {
|
|
24
|
+
expect(await loadConfig(repo("{ tdd: true }"))).toMatchObject({ ok: true, config: { tdd: true } });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("resolves tdd to false when the config omits it", async () => {
|
|
28
|
+
expect(await loadConfig(repo("{}"))).toMatchObject({ ok: true, config: { tdd: false } });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each(['"yes"', "1", "null"])("refuses tdd: %s and names tdd", async (value) => {
|
|
32
|
+
expect(await loadConfig(repo(`{ tdd: ${value} }`))).toEqual(refusal("tdd: must be true or false"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each(['["docs"]', "[]"])("refuses tdd without the standards pack (packs %s)", async (list) => {
|
|
36
|
+
expect(await loadConfig(repo(`{ packs: ${list}, tdd: true }`))).toEqual(
|
|
37
|
+
refusal('tdd: set only with the standards pack; add "standards" to packs or delete tdd'),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("accepts tdd with the standards pack listed", async () => {
|
|
42
|
+
expect(await loadConfig(repo('{ packs: ["standards"], tdd: true }'))).toMatchObject({
|
|
43
|
+
ok: true,
|
|
44
|
+
config: { tdd: true },
|
|
45
|
+
});
|
|
46
|
+
});
|