testguard-cli 0.7.0__tar.gz → 0.8.1__tar.gz
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.
- testguard_cli-0.8.1/.github/scripts/check-cost-budget.mjs +43 -0
- testguard_cli-0.8.1/.github/scripts/sync-release-version.mjs +195 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/workflows/auto-merge.yml +8 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/workflows/ci.yml +8 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/workflows/release.yml +130 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/workflows/scheduled-release.yml +38 -8
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/CHANGELOG.md +185 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/CONTRIBUTING.md +8 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/PKG-INFO +15 -12
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/README.md +14 -11
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/action.yml +1 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs-canonical/DATA-MODEL.md +3 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs-canonical/REQUIREMENTS.md +4 -4
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs-canonical/TEST-SPEC.md +2 -2
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/package-lock.json +2 -2
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/package.json +1 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/packaging/gitlab/testguard.gitlab-ci.yml +2 -2
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/packaging/homebrew/testguard.rb +7 -5
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/pyproject.toml +1 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/GATE-SEMANTICS.md +98 -3
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/README.md +5 -2
- testguard_cli-0.8.1/spec/conformance/examples/calibration-websec.json +98 -0
- testguard_cli-0.8.1/spec/conformance/examples/calibration.json +51 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/claims-methods.json +14 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/evidence-scan-method.json +5 -1
- testguard_cli-0.8.1/spec/conformance/invalid/calibration.backoff-cell-not-wilson.json +98 -0
- testguard_cli-0.8.1/spec/conformance/invalid/calibration.breakdown-does-not-sum-to-n.json +56 -0
- testguard_cli-0.8.1/spec/conformance/invalid/calibration.ci-not-wilson.json +44 -0
- testguard_cli-0.8.1/spec/conformance/invalid/calibration.compound-key-arity-mismatch.json +107 -0
- testguard_cli-0.8.1/spec/conformance/invalid/calibration.fallback-without-basis.json +97 -0
- testguard_cli-0.7.0/spec/conformance/examples/calibration.json → testguard_cli-0.8.1/spec/conformance/invalid/calibration.p-not-positives-over-n.json +2 -2
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/calibration.p-outside-ci.json +1 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/calibration.positives-exceed-n.json +1 -1
- testguard_cli-0.8.1/spec/conformance/invalid/claims.method-detail-on-a-specified-method.json +37 -0
- testguard_cli-0.8.1/spec/conformance/invalid/evidence.method-detail-on-an-injecting-run.json +192 -0
- testguard_cli-0.8.1/spec/conformance/schemas.test.mjs +132 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/lib/validate.mjs +41 -6
- testguard_cli-0.8.1/spec/lib/wilson.mjs +112 -0
- testguard_cli-0.8.1/spec/schemas/calibration.schema.json +124 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/claims.schema.json +8 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/evidence.schema.json +6 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/replay.mjs +1 -1
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/cost.mjs +60 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/replay/replay.mjs +47 -20
- testguard_cli-0.8.1/test/calibration.test.mjs +73 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/cost.test.mjs +62 -1
- testguard_cli-0.8.1/test/release-sync.test.mjs +318 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/replay.test.mjs +5 -39
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/testguard.claims.json +397 -10
- testguard_cli-0.8.1/testguard.cost-budget.json +37 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/testguard.ignore.json +7 -0
- testguard_cli-0.7.0/.github/scripts/sync-release-version.mjs +0 -50
- testguard_cli-0.7.0/spec/conformance/schemas.test.mjs +0 -63
- testguard_cli-0.7.0/spec/schemas/calibration.schema.json +0 -54
- testguard_cli-0.7.0/test/release-sync.test.mjs +0 -118
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.agents/skills/testguard/SKILL.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.codex/hooks.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.docguard.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.docguardignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.gitattributes +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/dependabot.yml +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/scripts/build-one-pager.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/scripts/check-python-fixture.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/scripts/install-smoke.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.github/workflows/supply-chain.yml +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.gitignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.npmignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.npmrc +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.pre-commit-config.yaml +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/.pre-commit-hooks.yaml +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/AGENTS.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/CLAUDE.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/CODE_OF_CONDUCT.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/DRIFT-LOG.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/LICENSE +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/PRIVACY.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/SECURITY.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/SUPPORT.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/bench/README.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/cli/testguard.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs/testguard-explained.html +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs/testguard-explained.pdf +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs-canonical/ARCHITECTURE.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs-canonical/ENVIRONMENT.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/docs-canonical/SECURITY.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/.gitignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/README.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/expected.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/package.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/src/export.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/src/redact.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/test/export-mocked.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/test/flaky.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/test/redact.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/testguard.claims.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer/vitest.config.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/.gitignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/README.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/expected.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/jest.config.js +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/package.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/src/export.js +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/src/redact.js +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/test/export-mocked.test.js +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/test/flaky.test.js +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/test/redact.test.js +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-jest/testguard.claims.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/.gitignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/README.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/e2e/flaky.spec.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/e2e/toggle.spec.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/expected.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/package-lock.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/package.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/playwright.config.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/src/toggle.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/test/toggle.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/test-results/.last-run.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/testguard.claims.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-playwright/vitest.config.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/.gitignore +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/README.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/demo/__init__.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/demo/export.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/demo/orphan.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/demo/pipeline.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/demo/redact.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/expected.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/testguard.claims.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/tests/test_export_mocked.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/tests/test_flaky.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/tests/test_patched.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/fixtures/known-answer-python/tests/test_redact.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/baseline.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/brief.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/claims.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/evidence-disturbed-run.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/evidence-negative-control.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/evidence-provisional.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/evidence-python.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/evidence.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/gate.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/ignore.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/replay.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/examples/status.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/baseline.zero-count.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.absolute-path.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.fault-injection-without-a-substitution.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.noop-fault.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.annotated-signal-without-reason.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.attribute-signal-not-a-defender.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.attribute-signal-without-attributes.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.byrunner-file-not-resolved.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.contention-without-runners.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.discovered-defender-mocks-subject.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.fault-injection-without-its-evidence.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.ignored-dirty-on-snapshot.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.negative-control-not-reached-but-survived.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.negative-control-reached-on-a-kill.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.probe-error-without-message.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.provisional-flag-on-confirmed-run.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.restore-skipped-in-place.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.single-run-without-provisional.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.target-not-imported-but-killed.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/gate.counts-disagree.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/gate.exit-zero-with-uncovered.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/gate.ignore-cover-without-pattern.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/ignore.short-reason.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/replay.blind-with-a-failing-run.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/replay.duplicate-patch-id.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/status.claim-without-file.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/status.clean-with-new-findings.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/status.unclaimed-hidden-behind-unproven.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/conformance/invalid/status.write-test-without-target.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/lib/fingerprint.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/baseline.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/brief.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/common.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/gate.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/ignore.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/replay.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/spec/schemas/status.schema.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/admit/admit.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/baseline/baseline.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/brief/brief.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/claims/annotations.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/claims/load.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/claims/removed.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/cli.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/admit.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/baseline.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/brief.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/claims.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/gate.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/init.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/mcp.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/probe.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/scaffold.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/commands/status.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/evidence/writer.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/gate/changed.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/git.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/init/init.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/init/templates/SKILL.md +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/mcp/server.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/mcp/tools.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/attribution.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/classify.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/contention.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/discover.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/independence.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/inject.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/mocks.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/probe.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/progress.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/pyimports.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/rank.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/index.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/jest.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/playwright.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/python/_testguard_pytest_plugin.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/python/_testguard_report.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/python/_testguard_unittest_main.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/python.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/shared.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/runners/vitest.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/probe/worktree.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/render.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/replay/label.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/scaffold/producers.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/scaffold/producers.python.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/scaffold/scaffold.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/status/status.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/util/glob.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/src/util/hash.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/admit.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/annotations.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/attribution.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/baseline.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/brief.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/claims-since.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/classify.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/contention.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/discover.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/gate.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/gitlab-template.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/glob.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/independence.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/init.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/inject.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/mcp.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/mocks.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/negative-control.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/probe-error.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/probe-preconditions.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/probe.fixture.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/probe.jest.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/probe.playwright.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/probe.python.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/progress.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/pyimports.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/rank-aliases.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/runner-argv.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/runner-command.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/runner-playwright.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/runner-python.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/runner-vitest.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/samples/playwright-report.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/samples/python-report-collect-error.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/samples/python-report.json +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/scaffold.python.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/scaffold.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/snapshot.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/status.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/worktree.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/test/writer.test.mjs +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/testguard_cli/__init__.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/testguard_cli/wrapper.py +0 -0
- {testguard_cli-0.7.0 → testguard_cli-0.8.1}/vitest.config.mjs +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fail CI when the self-probe gate outgrows its budget.
|
|
4
|
+
*
|
|
5
|
+
* The decision lives in `src/probe/cost.mjs` as a pure function, unit-tested
|
|
6
|
+
* and covered by a self-claim; this script is only wiring — read the evidence,
|
|
7
|
+
* read the budget, print, choose an exit code.
|
|
8
|
+
*
|
|
9
|
+
* usage: check-cost-budget.mjs <evidence.json> [previous-evidence.json]
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { costReport, checkCostBudget, renderCostBudget } from '../../src/probe/cost.mjs';
|
|
15
|
+
|
|
16
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
+
const [evidencePath, previousPath] = process.argv.slice(2);
|
|
18
|
+
if (!evidencePath) {
|
|
19
|
+
console.error('usage: check-cost-budget.mjs <evidence.json> [previous-evidence.json]');
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const read = (p) => JSON.parse(readFileSync(resolve(p), 'utf8'));
|
|
24
|
+
const budget = read(join(ROOT, 'testguard.cost-budget.json'));
|
|
25
|
+
|
|
26
|
+
// A previous run is informational only. Its absence must never fail the check:
|
|
27
|
+
// the first run on a branch, or a cold cache, has nothing to compare against.
|
|
28
|
+
let previousMs;
|
|
29
|
+
if (previousPath) {
|
|
30
|
+
try {
|
|
31
|
+
previousMs = costReport(read(previousPath).records).totalMs;
|
|
32
|
+
} catch {
|
|
33
|
+
previousMs = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const decision = checkCostBudget(costReport(read(evidencePath).records), { budgetSeconds: budget.seconds, previousMs });
|
|
38
|
+
const text = renderCostBudget(decision);
|
|
39
|
+
console.log(text);
|
|
40
|
+
if (!decision.ok) {
|
|
41
|
+
console.log(`::error::${text.split('\n')[0]}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* package.json is the single source of truth for the version. Every other
|
|
4
|
+
* surface that carries it is synced from there:
|
|
5
|
+
* - pyproject.toml `version = "x.y.z"`
|
|
6
|
+
* - action.yml default of the `version` input
|
|
7
|
+
* - packaging/homebrew/*.rb the tarball URL; the `sha256` is set by `--sha256`
|
|
8
|
+
* from the PUBLISHED tarball (release.yml, after npm)
|
|
9
|
+
* - packaging/gitlab/*.yml the include URL tag and the `version` input default
|
|
10
|
+
* - README.md the GitHub Action reference AND the GitLab include URL
|
|
11
|
+
*
|
|
12
|
+
* A file may carry more than one form, and each needs its own entry: README
|
|
13
|
+
* holds both the `raccioly/testguard@vX.Y.Z` action reference and the
|
|
14
|
+
* `testguard/vX.Y.Z/packaging` template URL. Only the first was listed, so
|
|
15
|
+
* v0.6.0 shipped with both README copies of the GitLab URL left at v0.5.0
|
|
16
|
+
* while `--check` reported "all version surfaces at 0.6.0" — a surface this
|
|
17
|
+
* script was never told to look at cannot drift in its eyes.
|
|
18
|
+
*
|
|
19
|
+
* The Homebrew `sha256` is the same lesson one step further: the URL was a
|
|
20
|
+
* surface, the hash beside it was a comment telling a human to run curl. It
|
|
21
|
+
* carried 385d69f9… unchanged across v0.6.0, v0.7.0 and v0.8.0 — a hash
|
|
22
|
+
* matching none of them. So the hash is now a surface too, with three modes:
|
|
23
|
+
*
|
|
24
|
+
* --check offline: every version surface at package.json's version,
|
|
25
|
+
* the formula's sha256 is 64 hex and not the known-stale
|
|
26
|
+
* placeholder. Used by release.yml's test job (before the
|
|
27
|
+
* tag, when the tarball cannot exist yet) and prepublishOnly.
|
|
28
|
+
* --check --online additionally downloads the tarball the formula names and
|
|
29
|
+
* requires its sha256 to match. Used by release.yml's
|
|
30
|
+
* detect-version (is the formula still owed?) and by the
|
|
31
|
+
* sha job as a post-condition before it opens the PR.
|
|
32
|
+
* --sha256 waits for the tarball to become downloadable (bounded by
|
|
33
|
+
* --wait), computes its sha256 and writes it into the
|
|
34
|
+
* formula — only when the value changes. Exit 1 on timeout
|
|
35
|
+
* or on a 200 that is not a gzip tarball: a hashed error
|
|
36
|
+
* page would poison the formula, and "never optimistic".
|
|
37
|
+
*
|
|
38
|
+
* The hash is always computed from the REGISTRY tarball — what Homebrew will
|
|
39
|
+
* download — never from a local `npm pack`: a provenance-signed publish can
|
|
40
|
+
* differ byte for byte from the pack output.
|
|
41
|
+
*
|
|
42
|
+
* --list-surfaces print every file this script may write, one per line. The
|
|
43
|
+
* release workflows allow-list exactly these (plus
|
|
44
|
+
* package.json, package-lock.json and CHANGELOG.md, which
|
|
45
|
+
* the bump writes directly), so a surface added above can
|
|
46
|
+
* never again be rejected as an "unexpected file in the
|
|
47
|
+
* release diff" — which is what stopped every weekly
|
|
48
|
+
* release after the GitLab template became a surface.
|
|
49
|
+
* --registry <url> registry base (default https://registry.npmjs.org); the
|
|
50
|
+
* tests point it at a local server so `npm test` is offline.
|
|
51
|
+
* --wait <s> how long --sha256 keeps polling (default 900).
|
|
52
|
+
* --interval <s> seconds between polls (default 15).
|
|
53
|
+
* --timeout <s> per-request timeout (default 60): a registry that accepts the
|
|
54
|
+
* connection and never answers is a failed attempt, not a hang.
|
|
55
|
+
*/
|
|
56
|
+
import { createHash } from 'node:crypto';
|
|
57
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
58
|
+
import { resolve } from 'node:path';
|
|
59
|
+
|
|
60
|
+
const root = resolve(process.argv[1], '..', '..', '..');
|
|
61
|
+
const argv = process.argv.slice(2);
|
|
62
|
+
const flag = (name) => argv.includes(name);
|
|
63
|
+
const option = (name, fallback) => {
|
|
64
|
+
const i = argv.indexOf(name);
|
|
65
|
+
return i === -1 ? fallback : argv[i + 1];
|
|
66
|
+
};
|
|
67
|
+
const check = flag('--check');
|
|
68
|
+
const listSurfaces = flag('--list-surfaces');
|
|
69
|
+
const online = flag('--online');
|
|
70
|
+
const setSha = flag('--sha256');
|
|
71
|
+
const registry = (option('--registry', 'https://registry.npmjs.org')).replace(/\/+$/, '');
|
|
72
|
+
const waitSeconds = Number(option('--wait', '900'));
|
|
73
|
+
const intervalSeconds = Number(option('--interval', '15'));
|
|
74
|
+
const timeoutSeconds = Number(option('--timeout', '60'));
|
|
75
|
+
if (!Number.isFinite(waitSeconds) || waitSeconds < 0) throw new Error(`--wait must be a non-negative number of seconds, got ${option('--wait')}`);
|
|
76
|
+
if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0) throw new Error(`--interval must be a non-negative number of seconds, got ${option('--interval')}`);
|
|
77
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) throw new Error(`--timeout must be a positive number of seconds, got ${option('--timeout')}`);
|
|
78
|
+
|
|
79
|
+
const version = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')).version;
|
|
80
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`package.json version is not a stable semver: ${version}`);
|
|
81
|
+
|
|
82
|
+
const FORMULA = 'packaging/homebrew/testguard.rb';
|
|
83
|
+
const formulaPath = resolve(root, FORMULA);
|
|
84
|
+
/** The hash the formula carried, unchanged, from v0.6.0 through v0.8.0. It matches no published tarball. */
|
|
85
|
+
const KNOWN_STALE_SHA256 = '385d69f9d3c153b934d9c1cb6a2c754eb9b221b0a8c0ba8b805858384d2d4678';
|
|
86
|
+
const SHA_LINE = /^(\s*sha256 ")([^"]*)(")/m;
|
|
87
|
+
const TARBALL = `testguard-cli-${version}.tgz`;
|
|
88
|
+
const tarballUrl = `${registry}/testguard-cli/-/${TARBALL}`;
|
|
89
|
+
|
|
90
|
+
const surfaces = [
|
|
91
|
+
['pyproject.toml', /^version = "[^"]+"/m, `version = "${version}"`],
|
|
92
|
+
['action.yml', /(\n version:\n description: [^\n]*\n required: false\n default: ')[^']*(')/, `$1${version}$2`],
|
|
93
|
+
[FORMULA, /testguard-cli-\d+\.\d+\.\d+\.tgz/g, TARBALL],
|
|
94
|
+
['README.md', /raccioly\/testguard@v\d+\.\d+\.\d+/g, `raccioly/testguard@v${version}`],
|
|
95
|
+
['README.md', /testguard\/v\d+\.\d+\.\d+\/packaging/g, `testguard/v${version}/packaging`],
|
|
96
|
+
['packaging/gitlab/testguard.gitlab-ci.yml', /testguard\/v\d+\.\d+\.\d+\/packaging/g, `testguard/v${version}/packaging`],
|
|
97
|
+
['packaging/gitlab/testguard.gitlab-ci.yml', /(\n version:\n description: [^\n]*\n default: ")\d+\.\d+\.\d+(")/, `$1${version}$2`],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
if (listSurfaces) {
|
|
101
|
+
// Derived from the same table the writes use, so the two cannot disagree.
|
|
102
|
+
for (const rel of [...new Set(surfaces.map(([rel]) => rel))]) console.log(rel);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The formula's current `sha256 "…"` value, or a throw when the line is missing. */
|
|
107
|
+
function formulaSha256() {
|
|
108
|
+
const m = SHA_LINE.exec(readFileSync(formulaPath, 'utf8'));
|
|
109
|
+
if (!m) throw new Error(`${FORMULA}: sha256 line not found`);
|
|
110
|
+
return m[2];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One GET of the tarball. Returns `{ status, sha256 }`; `sha256` is set only
|
|
115
|
+
* for a 200 whose body starts with the gzip magic (1f 8b) — a registry error
|
|
116
|
+
* page with a 200 status is treated as "not a tarball", never hashed.
|
|
117
|
+
*/
|
|
118
|
+
async function fetchTarball() {
|
|
119
|
+
const res = await fetch(tarballUrl, { redirect: 'follow', signal: AbortSignal.timeout(timeoutSeconds * 1000) });
|
|
120
|
+
if (res.status !== 200) return { status: res.status };
|
|
121
|
+
const body = Buffer.from(await res.arrayBuffer());
|
|
122
|
+
if (body.length < 2 || body[0] !== 0x1f || body[1] !== 0x8b) return { status: 200, notTarball: true, bytes: body.length };
|
|
123
|
+
return { status: 200, sha256: createHash('sha256').update(body).digest('hex'), bytes: body.length };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Polls until the tarball downloads as a gzip, or the wait runs out. Every
|
|
130
|
+
* non-200 and every network error counts as "not yet": the registry answers
|
|
131
|
+
* `PUT 202` to a publish and can take minutes to serve the file.
|
|
132
|
+
*/
|
|
133
|
+
async function waitForTarball() {
|
|
134
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
135
|
+
let attempt = 0;
|
|
136
|
+
for (;;) {
|
|
137
|
+
attempt++;
|
|
138
|
+
let result;
|
|
139
|
+
try {
|
|
140
|
+
result = await fetchTarball();
|
|
141
|
+
} catch (e) {
|
|
142
|
+
result = { status: 0, error: e.message };
|
|
143
|
+
}
|
|
144
|
+
if (result.sha256) return result;
|
|
145
|
+
const why = result.notTarball ? `200 but not a gzip tarball (${result.bytes} bytes)` : result.error ? `network error: ${result.error}` : `HTTP ${result.status}`;
|
|
146
|
+
if (Date.now() >= deadline) throw new Error(`${tarballUrl}: not downloadable after ${attempt} attempt(s) over ${waitSeconds}s — last: ${why}`);
|
|
147
|
+
console.error(`${TARBALL}: ${why}; retrying in ${intervalSeconds}s (attempt ${attempt})`);
|
|
148
|
+
await sleep(intervalSeconds);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (setSha) {
|
|
153
|
+
const { sha256, bytes } = await waitForTarball();
|
|
154
|
+
const before = readFileSync(formulaPath, 'utf8');
|
|
155
|
+
const current = formulaSha256();
|
|
156
|
+
if (current === sha256) {
|
|
157
|
+
console.log(`${FORMULA}: sha256 unchanged (${sha256}, ${bytes} bytes)`);
|
|
158
|
+
} else {
|
|
159
|
+
writeFileSync(formulaPath, before.replace(SHA_LINE, `$1${sha256}$3`));
|
|
160
|
+
if (formulaSha256() !== sha256) throw new Error(`${FORMULA}: sha256 line did not take the new value`);
|
|
161
|
+
console.log(`${FORMULA}: sha256 ${current} -> ${sha256} (${bytes} bytes from ${tarballUrl})`);
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let drift = 0;
|
|
167
|
+
for (const [rel, pattern, replacement] of surfaces) {
|
|
168
|
+
const path = resolve(root, rel);
|
|
169
|
+
const before = readFileSync(path, 'utf8');
|
|
170
|
+
if (!pattern.test(before)) throw new Error(`${rel}: version pattern not found`);
|
|
171
|
+
const after = before.replace(pattern, replacement);
|
|
172
|
+
if (after === before) continue;
|
|
173
|
+
drift++;
|
|
174
|
+
if (check) console.error(`${rel}: out of sync with package.json ${version}`);
|
|
175
|
+
else { writeFileSync(path, after); console.log(`${rel}: synced to ${version}`); }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (check) {
|
|
179
|
+
const sha = formulaSha256();
|
|
180
|
+
if (!/^[0-9a-f]{64}$/.test(sha)) { drift++; console.error(`${FORMULA}: sha256 is not 64 hex characters: "${sha}"`); }
|
|
181
|
+
else if (sha === KNOWN_STALE_SHA256) { drift++; console.error(`${FORMULA}: sha256 is the known-stale placeholder ${sha.slice(0, 8)}… (matches no published tarball); run --sha256 after publishing`); }
|
|
182
|
+
if (online) {
|
|
183
|
+
let result;
|
|
184
|
+
try {
|
|
185
|
+
result = await fetchTarball();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
result = { status: 0, error: e.message };
|
|
188
|
+
}
|
|
189
|
+
if (!result.sha256) { drift++; console.error(`${FORMULA}: tarball not downloadable — ${tarballUrl}: ${result.error ?? (result.notTarball ? '200 but not a gzip tarball' : `HTTP ${result.status}`)}`); }
|
|
190
|
+
else if (result.sha256 !== sha) { drift++; console.error(`${FORMULA}: sha256 ${sha} does not match the published tarball ${result.sha256} (${tarballUrl})`); }
|
|
191
|
+
else console.log(`${FORMULA}: sha256 matches ${tarballUrl}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (check && drift) process.exit(1);
|
|
195
|
+
if (check) console.log(`all version surfaces at ${version}`);
|
|
@@ -46,7 +46,14 @@ jobs:
|
|
|
46
46
|
|
|
47
47
|
const { data: files } = await github.rest.pulls.listFiles({ owner, repo, pull_number: num, per_page: 300 });
|
|
48
48
|
const paths = files.map((f) => f.filename);
|
|
49
|
-
|
|
49
|
+
// Deliberately spelled out, not derived: this is the gate that merges
|
|
50
|
+
// without human review, so what a bot may touch stays readable here
|
|
51
|
+
// and is not computed from code the PR itself could change. Keep it
|
|
52
|
+
// in step with `sync-release-version.mjs --list-surfaces` plus the
|
|
53
|
+
// three files the bump writes — test/release-sync.test.mjs fails if
|
|
54
|
+
// a surface is missing, which is how the GitLab template's absence
|
|
55
|
+
// (it would have held every release PR for review) was found.
|
|
56
|
+
const RELEASE_SURFACES = new Set(['package.json', 'package-lock.json', 'pyproject.toml', 'CHANGELOG.md', 'action.yml', 'packaging/homebrew/testguard.rb', 'README.md', 'packaging/gitlab/testguard.gitlab-ci.yml']);
|
|
50
57
|
|
|
51
58
|
if (author === 'dependabot[bot]') {
|
|
52
59
|
const m = /from (\d+)\.(\d+)\.\S+ to (\d+)\.(\d+)\./.exec(pr.title || '');
|
|
@@ -210,6 +210,14 @@ jobs:
|
|
|
210
210
|
if: matrix.node-version == 22
|
|
211
211
|
run: node cli/testguard.mjs probe . --budget 180000 --no-escalate --cost --out .testguard/ci-self-evidence.json
|
|
212
212
|
|
|
213
|
+
# The gate went 9.6 -> 23.6 min across one merged PR and nothing said a
|
|
214
|
+
# word, because --cost only ever printed into a log. `always()` so the
|
|
215
|
+
# cost is reported even when the probe itself failed: a run that got
|
|
216
|
+
# slower AND found a survivor should say both, not just the second.
|
|
217
|
+
- name: The gate must stay inside its cost budget
|
|
218
|
+
if: always() && matrix.node-version == 22
|
|
219
|
+
run: node .github/scripts/check-cost-budget.mjs .testguard/ci-self-evidence.json
|
|
220
|
+
|
|
213
221
|
# A stable, branch-named artifact so a developer can fetch the evidence
|
|
214
222
|
# CI produced without knowing a run id (README: read CI's evidence
|
|
215
223
|
# locally). One Node leg is enough; the verdicts are identical.
|
|
@@ -36,6 +36,7 @@ jobs:
|
|
|
36
36
|
tag_missing: ${{ steps.check.outputs.tag_missing }}
|
|
37
37
|
npm_missing: ${{ steps.check.outputs.npm_missing }}
|
|
38
38
|
pypi_missing: ${{ steps.check.outputs.pypi_missing }}
|
|
39
|
+
brew_missing: ${{ steps.check.outputs.brew_missing }}
|
|
39
40
|
steps:
|
|
40
41
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
41
42
|
with:
|
|
@@ -45,6 +46,13 @@ jobs:
|
|
|
45
46
|
# Each surface is gated on ITS OWN registry, not on the tag. A tag
|
|
46
47
|
# whose npm or PyPI publish failed (e.g. Trusted Publishing not yet
|
|
47
48
|
# configured) is retried by simply re-running this workflow.
|
|
49
|
+
#
|
|
50
|
+
# The Homebrew formula's sha256 is a surface too, gated on the registry
|
|
51
|
+
# tarball it names: `--check --online` downloads it and compares. It is
|
|
52
|
+
# owed (brew_missing=true) until npm serves the tarball AND the formula
|
|
53
|
+
# on main carries its hash — so a failed sha job is retried by the
|
|
54
|
+
# hourly sweep exactly like a failed publish. It does not set `changed`:
|
|
55
|
+
# only tag/npm/PyPI need the test job and the publish jobs.
|
|
48
56
|
run: |
|
|
49
57
|
CURRENT=$(node -p "require('./package.json').version")
|
|
50
58
|
TAG="v${CURRENT}"
|
|
@@ -53,12 +61,17 @@ jobs:
|
|
|
53
61
|
if npm view "testguard-cli@${CURRENT}" version >/dev/null 2>&1; then NPMM=false; else NPMM=true; fi
|
|
54
62
|
CODE=$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/testguard-cli/${CURRENT}/json")
|
|
55
63
|
if [ "$CODE" = "200" ]; then PYPIM=false; else PYPIM=true; fi
|
|
64
|
+
if node .github/scripts/sync-release-version.mjs --check --online; then BREWM=false; else BREWM=true; fi
|
|
56
65
|
echo "tag_missing=$TAGM" >> "$GITHUB_OUTPUT"
|
|
57
66
|
echo "npm_missing=$NPMM" >> "$GITHUB_OUTPUT"
|
|
58
67
|
echo "pypi_missing=$PYPIM" >> "$GITHUB_OUTPUT"
|
|
68
|
+
echo "brew_missing=$BREWM" >> "$GITHUB_OUTPUT"
|
|
59
69
|
if [ "$TAGM" = true ] || [ "$NPMM" = true ] || [ "$PYPIM" = true ]; then
|
|
60
70
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
61
|
-
echo "✅ $TAG — tag missing: $TAGM · npm missing: $NPMM · PyPI missing: $PYPIM"
|
|
71
|
+
echo "✅ $TAG — tag missing: $TAGM · npm missing: $NPMM · PyPI missing: $PYPIM · Homebrew sha256 owed: $BREWM"
|
|
72
|
+
elif [ "$BREWM" = true ]; then
|
|
73
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
74
|
+
echo "✅ $TAG is published everywhere; only the Homebrew sha256 is owed"
|
|
62
75
|
else
|
|
63
76
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
64
77
|
echo "⏭️ $TAG is fully released everywhere — nothing to do"
|
|
@@ -82,6 +95,12 @@ jobs:
|
|
|
82
95
|
- name: Self-probe must be fully defended before anything ships
|
|
83
96
|
run: node cli/testguard.mjs probe . --budget 180000 --out "$RUNNER_TEMP/self-evidence.json" --quiet --cost
|
|
84
97
|
- name: Version surfaces are in sync
|
|
98
|
+
# Offline on purpose. Before the tag the tarball for this version
|
|
99
|
+
# cannot exist, so `--online` would say nothing in the normal path and
|
|
100
|
+
# would DEADLOCK the recovery path: npm published, the sha job failed,
|
|
101
|
+
# the formula still stale — an online check here fails and the job
|
|
102
|
+
# that fixes the formula never runs. The online comparison lives in
|
|
103
|
+
# detect-version (is it owed?) and in homebrew-sha (did it take?).
|
|
85
104
|
run: node .github/scripts/sync-release-version.mjs --check
|
|
86
105
|
|
|
87
106
|
create-release:
|
|
@@ -177,3 +196,113 @@ jobs:
|
|
|
177
196
|
- name: Assert OIDC token is available
|
|
178
197
|
run: '[ -n "$ACTIONS_ID_TOKEN_REQUEST_URL" ] || { echo "::error::no OIDC token endpoint"; exit 1; }'
|
|
179
198
|
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
199
|
+
|
|
200
|
+
homebrew-sha:
|
|
201
|
+
# The formula's sha256 is set from the PUBLISHED tarball — what Homebrew
|
|
202
|
+
# will download, which a provenance-signed publish can make differ byte
|
|
203
|
+
# for byte from `npm pack` — after npm serves it (a publish is `PUT 202`;
|
|
204
|
+
# the file can take minutes to appear, so the script polls, bounded).
|
|
205
|
+
#
|
|
206
|
+
# It cannot commit to main: branch protection requires the three CI legs
|
|
207
|
+
# on every commit and github-actions[bot] is no admin. So the one-line
|
|
208
|
+
# change lands as a bot PR with native auto-merge armed, and this job
|
|
209
|
+
# dispatches ci.yml on the branch itself — a PR opened with GITHUB_TOKEN
|
|
210
|
+
# gets no pull_request run, but workflow_dispatch is the documented
|
|
211
|
+
# exception. That also means the merge triggers nothing here; the hourly
|
|
212
|
+
# sweep observes the merged formula and finds brew_missing=false.
|
|
213
|
+
#
|
|
214
|
+
# Idempotent: an existing branch is reused, an existing open PR is left
|
|
215
|
+
# alone (CI re-dispatched only if its head has no check runs), a closed
|
|
216
|
+
# PR is respected as a human decision, and a formula that already carries
|
|
217
|
+
# the published hash lands nothing.
|
|
218
|
+
needs: [detect-version, test, create-release, publish-npm]
|
|
219
|
+
if: >-
|
|
220
|
+
!cancelled()
|
|
221
|
+
&& needs.detect-version.outputs.brew_missing == 'true'
|
|
222
|
+
&& needs.test.result != 'failure'
|
|
223
|
+
&& needs.create-release.result != 'failure'
|
|
224
|
+
&& (needs.publish-npm.result == 'success' || (needs.publish-npm.result == 'skipped' && needs.detect-version.outputs.npm_missing == 'false'))
|
|
225
|
+
runs-on: ubuntu-latest
|
|
226
|
+
permissions:
|
|
227
|
+
contents: write # pushes the chore/homebrew-sha-v* branch
|
|
228
|
+
pull-requests: write # opens the PR and arms auto-merge
|
|
229
|
+
actions: write # dispatches ci.yml on that branch
|
|
230
|
+
steps:
|
|
231
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
232
|
+
with:
|
|
233
|
+
ref: main
|
|
234
|
+
fetch-depth: 0
|
|
235
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
236
|
+
with:
|
|
237
|
+
node-version: 24
|
|
238
|
+
- name: Set sha256 from the published tarball; land it as an auto-merging PR
|
|
239
|
+
env:
|
|
240
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
241
|
+
run: |
|
|
242
|
+
set -euo pipefail
|
|
243
|
+
VERSION="${{ needs.detect-version.outputs.version }}"
|
|
244
|
+
ON_MAIN=$(node -p "require('./package.json').version")
|
|
245
|
+
if [ "$ON_MAIN" != "$VERSION" ]; then
|
|
246
|
+
echo "⏭️ main is already at v${ON_MAIN}, not v${VERSION}; that release sets its own sha256"; exit 0
|
|
247
|
+
fi
|
|
248
|
+
BRANCH="chore/homebrew-sha-v${VERSION}"
|
|
249
|
+
git config user.name "github-actions[bot]"
|
|
250
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
251
|
+
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
|
|
252
|
+
git fetch origin "$BRANCH"
|
|
253
|
+
git checkout -B "$BRANCH" "origin/$BRANCH"
|
|
254
|
+
else
|
|
255
|
+
git checkout -b "$BRANCH"
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
node .github/scripts/sync-release-version.mjs --sha256 --wait 900
|
|
259
|
+
# Post-condition: the file now on disk matches the tarball, and no
|
|
260
|
+
# other surface drifted. Nothing is pushed unless this passes.
|
|
261
|
+
node .github/scripts/sync-release-version.mjs --check --online
|
|
262
|
+
|
|
263
|
+
if git diff --quiet -- packaging/homebrew/testguard.rb; then
|
|
264
|
+
echo "✅ $BRANCH already carries the published sha256"
|
|
265
|
+
else
|
|
266
|
+
git add packaging/homebrew/testguard.rb
|
|
267
|
+
git commit -m "chore(homebrew): sha256 for v${VERSION}, from the published tarball"
|
|
268
|
+
git push origin "$BRANCH"
|
|
269
|
+
fi
|
|
270
|
+
if [ -z "$(git log --oneline origin/main..HEAD)" ]; then
|
|
271
|
+
echo "✅ main already matches the published tarball; nothing to land"; exit 0
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
CLOSED=$(gh pr list --head "$BRANCH" --base main --state closed --json url,mergedAt --jq '[.[] | select(.mergedAt == null)][0].url')
|
|
275
|
+
if [ -n "$CLOSED" ]; then
|
|
276
|
+
echo "::warning::$CLOSED was closed without merging; leaving that decision alone. Delete branch $BRANCH to let the sweep retry."; exit 0
|
|
277
|
+
fi
|
|
278
|
+
PR=$(gh pr list --head "$BRANCH" --base main --state open --json url --jq '.[0].url')
|
|
279
|
+
if [ -z "$PR" ]; then
|
|
280
|
+
{
|
|
281
|
+
printf 'Automated: `sha256` of `packaging/homebrew/testguard.rb` set from the published tarball\n\n'
|
|
282
|
+
printf ' https://registry.npmjs.org/testguard-cli/-/testguard-cli-%s.tgz\n\n' "$VERSION"
|
|
283
|
+
printf 'Computed by `.github/scripts/sync-release-version.mjs --sha256` in release.yml after `npm publish`, from the registry — never from a local `npm pack`. `--check --online` confirmed the value before this PR was opened.\n\n'
|
|
284
|
+
printf 'Auto-merge is armed; CI was dispatched on this branch by the release workflow. If the checks never appear: `gh workflow run ci.yml --ref %s`.\n' "$BRANCH"
|
|
285
|
+
} > /tmp/pr-body.md
|
|
286
|
+
if PR=$(gh pr create --base main --head "$BRANCH" --title "chore(homebrew): sha256 for v${VERSION}" --body-file /tmp/pr-body.md 2>/tmp/pr-err); then
|
|
287
|
+
gh pr merge --auto --squash "$PR"
|
|
288
|
+
echo "Opened $PR"
|
|
289
|
+
else
|
|
290
|
+
cat /tmp/pr-err
|
|
291
|
+
if grep -q 'not permitted to create or approve pull requests' /tmp/pr-err; then
|
|
292
|
+
echo "::error::GITHUB_TOKEN may not open a pull request: \"GitHub Actions is not permitted to create or approve pull requests\". The branch is pushed and correct; only the PR is missing. Fix it once, in Settings -> Actions -> General -> Workflow permissions, by ticking \"Allow GitHub Actions to create and approve pull requests\" (equivalently: gh api -X PUT repos/$GITHUB_REPOSITORY/actions/permissions/workflow -F default_workflow_permissions=read -F can_approve_pull_request_reviews=true), then re-run this workflow. Until then, open it by hand: gh pr create --base main --head $BRANCH The formula on main still carries the previous release's hash until then, and detect-version will keep reporting it owed on every hourly sweep."
|
|
293
|
+
else
|
|
294
|
+
echo "::error::gh pr create failed for $BRANCH — see the output above."
|
|
295
|
+
fi
|
|
296
|
+
exit 1
|
|
297
|
+
fi
|
|
298
|
+
else
|
|
299
|
+
echo "✅ PR already open: $PR"
|
|
300
|
+
fi
|
|
301
|
+
HEAD_SHA=$(git rev-parse HEAD)
|
|
302
|
+
RUNS=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/check-runs" --jq '.total_count')
|
|
303
|
+
if [ "${RUNS:-0}" = "0" ]; then
|
|
304
|
+
gh workflow run ci.yml --ref "$BRANCH"
|
|
305
|
+
echo "Dispatched ci.yml on $BRANCH ($HEAD_SHA)"
|
|
306
|
+
else
|
|
307
|
+
echo "✅ $HEAD_SHA already has $RUNS check run(s)"
|
|
308
|
+
fi
|
|
@@ -88,10 +88,23 @@ jobs:
|
|
|
88
88
|
|
|
89
89
|
- name: Verify the tree is still green
|
|
90
90
|
if: steps.check.outputs.changed == 'true'
|
|
91
|
+
# --include-dirty is REQUIRED here, not a convenience. The step above
|
|
92
|
+
# bumped the version and synced every surface, and those writes are
|
|
93
|
+
# still uncommitted: the commit happens later, in the PR step. One of
|
|
94
|
+
# those surfaces, packaging/gitlab/testguard.gitlab-ci.yml, is the
|
|
95
|
+
# fault target of TG-GITLAB-PROBE-EXIT-PRESERVED, so the default
|
|
96
|
+
# worktree mode refuses to run at all — "1 defender/target file has
|
|
97
|
+
# uncommitted changes … worktree mode probes HEAD, so those changes
|
|
98
|
+
# would be silently ignored" (exit 2). That refusal is correct; the
|
|
99
|
+
# answer is to probe the tree we are about to release, which is what
|
|
100
|
+
# --include-dirty does (it snapshots the working tree into a scratch
|
|
101
|
+
# commit). --ignore-dirty would probe HEAD and quietly verify the
|
|
102
|
+
# PREVIOUS version instead, which is the optimistic reading this
|
|
103
|
+
# project does not take.
|
|
91
104
|
run: |
|
|
92
105
|
npm ci
|
|
93
106
|
npm test
|
|
94
|
-
node cli/testguard.mjs probe . --budget 180000 --quiet --out "$RUNNER_TEMP/self.json"
|
|
107
|
+
node cli/testguard.mjs probe . --budget 180000 --quiet --include-dirty --out "$RUNNER_TEMP/self.json"
|
|
95
108
|
|
|
96
109
|
- name: Open release PR with auto-merge armed
|
|
97
110
|
if: steps.check.outputs.changed == 'true'
|
|
@@ -99,12 +112,17 @@ jobs:
|
|
|
99
112
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
100
113
|
run: |
|
|
101
114
|
BRANCH="release/v${NEW_VERSION}"
|
|
102
|
-
# Only release surfaces may change in a release PR.
|
|
115
|
+
# Only release surfaces may change in a release PR. The list is
|
|
116
|
+
# DERIVED from the sync script, never retyped here: when the GitLab
|
|
117
|
+
# template became a surface it was left out of the hand-written list,
|
|
118
|
+
# so the first release to reach this guard would have been rejected
|
|
119
|
+
# for writing a file the release is supposed to write. The three
|
|
120
|
+
# extras are the ones the bump step writes directly.
|
|
121
|
+
ALLOWED=$(node .github/scripts/sync-release-version.mjs --list-surfaces; printf 'package.json\npackage-lock.json\nCHANGELOG.md\n')
|
|
122
|
+
echo "Release surfaces allowed in this diff:"; printf '%s\n' "$ALLOWED" | sed 's/^/ /'
|
|
103
123
|
CHANGED=$(git diff --name-only HEAD --)
|
|
104
124
|
for f in $CHANGED; do
|
|
105
|
-
|
|
106
|
-
*) echo "::error::unexpected file in release diff: $f"; exit 1;;
|
|
107
|
-
esac
|
|
125
|
+
printf '%s\n' "$ALLOWED" | grep -qxF "$f" || { echo "::error::unexpected file in release diff: $f"; exit 1; }
|
|
108
126
|
done
|
|
109
127
|
if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
|
|
110
128
|
echo "::error::branch $BRANCH already exists; refusing to overwrite"; exit 1
|
|
@@ -116,6 +134,18 @@ jobs:
|
|
|
116
134
|
git commit -m "release: v${NEW_VERSION} — automated weekly batch"
|
|
117
135
|
git push origin "$BRANCH"
|
|
118
136
|
printf 'Automated weekly release. Version bump + CHANGELOG entry + synced version surfaces. Merging tags and publishes to npm and PyPI via release.yml.\n\nCI does not start automatically on PRs opened by GITHUB_TOKEN. Run:\n\n gh workflow run ci.yml --ref %s\n\nAuto-merge is armed; it completes once the required checks pass.\n' "$BRANCH" > /tmp/pr-body.md
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
# A bare `gh pr create` failure here reads as a GraphQL error with no
|
|
138
|
+
# hint of the cause, which is how v0.8.1 lost its release PR after
|
|
139
|
+
# everything else in the release had succeeded.
|
|
140
|
+
if PR=$(gh pr create --base main --head "$BRANCH" --title "release: v${NEW_VERSION} — automated weekly batch" --body-file /tmp/pr-body.md 2>/tmp/pr-err); then
|
|
141
|
+
gh pr merge --auto --squash "$PR"
|
|
142
|
+
echo "Release PR: $PR"
|
|
143
|
+
else
|
|
144
|
+
cat /tmp/pr-err
|
|
145
|
+
if grep -q 'not permitted to create or approve pull requests' /tmp/pr-err; then
|
|
146
|
+
echo "::error::GITHUB_TOKEN may not open a pull request: \"GitHub Actions is not permitted to create or approve pull requests\". The branch is pushed and correct; only the PR is missing. Fix it once, in Settings -> Actions -> General -> Workflow permissions, by ticking \"Allow GitHub Actions to create and approve pull requests\" (equivalently: gh api -X PUT repos/$GITHUB_REPOSITORY/actions/permissions/workflow -F default_workflow_permissions=read -F can_approve_pull_request_reviews=true), then re-run this workflow. Until then, open it by hand: gh pr create --base main --head $BRANCH"
|
|
147
|
+
else
|
|
148
|
+
echo "::error::gh pr create failed for $BRANCH — see the output above."
|
|
149
|
+
fi
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|