fix: harden CI evidence and removal contracts
This commit is contained in:
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Install Playwright browsers
|
||||
if: ${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Install Playwright browsers
|
||||
if: ${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Build candidate once and verify local evidence
|
||||
run: corepack pnpm ci:gate -- FE-GATE-015
|
||||
- name: Archive and validate the exact candidate file set
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
@@ -278,7 +278,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
@@ -336,7 +336,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||
- name: Upload production gate evidence
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-018
|
||||
- name: Upload field gate evidence
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run documentation gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-017
|
||||
- name: Upload documentation gate evidence
|
||||
|
||||
+448
-105
@@ -648,527 +648,870 @@
|
||||
{
|
||||
"id": "artifact-artifacts-quality-install-txt",
|
||||
"path": "artifacts/quality/install.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-lint-txt",
|
||||
"path": "artifacts/quality/lint.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-check-types-txt",
|
||||
"path": "artifacts/quality/check-types.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-004-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-004.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-runtime-schema-xml",
|
||||
"path": "artifacts/tests/runtime-schema.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-runtime-schema"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-005-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-005.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-unit-xml",
|
||||
"path": "artifacts/tests/unit.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-unit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-coverage-xml",
|
||||
"path": "artifacts/tests/coverage.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-coverage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-coverage-coverage-summary-json",
|
||||
"path": "artifacts/tests/coverage/coverage-summary.json",
|
||||
"schemaId": "json-coverage-summary-v8"
|
||||
"schemaId": "json-coverage-summary-v8",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-coverage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-risk-coverage-json",
|
||||
"path": "artifacts/quality/risk-coverage.json",
|
||||
"schemaId": "json-risk-coverage-v3"
|
||||
"schemaId": "json-risk-coverage-v3",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-coverage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-risk-coverage-fixture-json",
|
||||
"path": "artifacts/quality/risk-coverage-fixture.json",
|
||||
"schemaId": "json-risk-coverage-v3"
|
||||
"schemaId": "json-risk-coverage-v3",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-coverage-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-006-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-006.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-component-xml",
|
||||
"path": "artifacts/tests/component.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-component"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-007-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-007.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-integration-xml",
|
||||
"path": "artifacts/tests/integration.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-integration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-http-scenario-executions-json",
|
||||
"path": "artifacts/tests/http-scenario-executions.json",
|
||||
"schemaId": "json-http-scenario-receipt"
|
||||
"schemaId": "json-http-scenario-receipt",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-http-scenario-evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-http-scenario-evidence-json",
|
||||
"path": "artifacts/quality/http-scenario-evidence.json",
|
||||
"schemaId": "json-test-evidence-report"
|
||||
"schemaId": "json-test-evidence-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-http-scenario-evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-http-scenario-evidence-fixture-json",
|
||||
"path": "artifacts/quality/http-scenario-evidence-fixture.json",
|
||||
"schemaId": "json-test-evidence-report"
|
||||
"schemaId": "json-test-evidence-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-http-scenario-evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-reference-feature-xml",
|
||||
"path": "artifacts/tests/reference-feature.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-reference-feature"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-optional-recipes-xml",
|
||||
"path": "artifacts/tests/optional-recipes.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-recipes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-008-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-008.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-e2e-report-index-html",
|
||||
"path": "artifacts/tests/e2e/report/index.html",
|
||||
"schemaId": "html"
|
||||
"schemaId": "html",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-e2e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-e2e-results-xml",
|
||||
"path": "artifacts/tests/e2e/results.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-e2e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-browser-capabilities-report-index-html",
|
||||
"path": "artifacts/tests/browser-capabilities/report/index.html",
|
||||
"schemaId": "html"
|
||||
"schemaId": "html",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-browser-capabilities"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-browser-capabilities-results-xml",
|
||||
"path": "artifacts/tests/browser-capabilities/results.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-browser-capabilities"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-storybook-report-index-html",
|
||||
"path": "artifacts/tests/storybook/report/index.html",
|
||||
"schemaId": "html"
|
||||
"schemaId": "html",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-storybook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-storybook-results-xml",
|
||||
"path": "artifacts/tests/storybook/results.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-storybook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-visual-report-index-html",
|
||||
"path": "artifacts/tests/visual/report/index.html",
|
||||
"schemaId": "html"
|
||||
"schemaId": "html",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-visual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-visual-results-xml",
|
||||
"path": "artifacts/tests/visual/results.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-visual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-test-evidence-json",
|
||||
"path": "artifacts/quality/test-evidence.json",
|
||||
"schemaId": "json-test-evidence-report"
|
||||
"schemaId": "json-test-evidence-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-test-evidence-browser"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-test-evidence-fixture-json",
|
||||
"path": "artifacts/quality/test-evidence-fixture.json",
|
||||
"schemaId": "json-test-evidence-report"
|
||||
"schemaId": "json-test-evidence-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-test-evidence-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-009.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-json",
|
||||
"path": "artifacts/tests/a11y.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-a11y"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
|
||||
"path": "artifacts/tests/a11y-manual/APP_HOME.md",
|
||||
"schemaId": "markdown"
|
||||
"schemaId": "markdown",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
|
||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_UI.md",
|
||||
"schemaId": "markdown"
|
||||
"schemaId": "markdown",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
|
||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
|
||||
"schemaId": "markdown"
|
||||
"schemaId": "markdown",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
|
||||
"path": "artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
|
||||
"schemaId": "markdown"
|
||||
"schemaId": "markdown",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
|
||||
"path": "artifacts/tests/a11y-manual/REFERENCE_RESOURCE_LIST.md",
|
||||
"schemaId": "markdown"
|
||||
"schemaId": "markdown",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
|
||||
"path": "artifacts/tests/a11y-manual/NOT_FOUND.md",
|
||||
"schemaId": "markdown"
|
||||
"schemaId": "markdown",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-a11y-manual-report-json",
|
||||
"path": "artifacts/tests/a11y-manual/report.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"review-a11y-manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-010.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-dependency-report-json",
|
||||
"path": "artifacts/quality/dependency-report.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-architecture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-design-system-json",
|
||||
"path": "artifacts/quality/design-system.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-design-system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-design-system-fixture-json",
|
||||
"path": "artifacts/quality/design-system-fixture.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-design-system-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-i18n-json",
|
||||
"path": "artifacts/quality/i18n.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-i18n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-i18n-fixture-json",
|
||||
"path": "artifacts/quality/i18n-fixture.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-i18n-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-diagnostics-json",
|
||||
"path": "artifacts/quality/diagnostics.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-diagnostics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-diagnostics-fixture-json",
|
||||
"path": "artifacts/quality/diagnostics-fixture.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-diagnostics-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-realtime-boundaries-json",
|
||||
"path": "artifacts/quality/realtime-boundaries.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-realtime-boundaries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-optional-recipes-json",
|
||||
"path": "artifacts/quality/optional-recipes.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-optional-recipes-source",
|
||||
"check-optional-recipes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-optional-recipe-fixtures-json",
|
||||
"path": "artifacts/quality/optional-recipe-fixtures.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-optional-recipe-fixtures"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-registries-json",
|
||||
"path": "artifacts/quality/registries.json",
|
||||
"schemaId": "json-registry-snapshot"
|
||||
"schemaId": "json-registry-snapshot",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-registries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-registry-compatibility-fixtures-json",
|
||||
"path": "artifacts/quality/registry-compatibility-fixtures.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-registries-compatibility-fixtures"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-registry-baseline-fixture-json",
|
||||
"path": "artifacts/quality/registry-baseline-fixture.json",
|
||||
"schemaId": "json-registry-governance-run"
|
||||
"schemaId": "json-registry-governance-run",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-registries-baseline-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-registry-fixture-json",
|
||||
"path": "artifacts/quality/registry-fixture.json",
|
||||
"schemaId": "json-registry-governance-run"
|
||||
"schemaId": "json-registry-governance-run",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-registries-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-route-registry-fixture-json",
|
||||
"path": "artifacts/quality/route-registry-fixture.json",
|
||||
"schemaId": "json-registry-governance-run"
|
||||
"schemaId": "json-registry-governance-run",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-routes-fixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-011-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-011.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-build-manifest-json",
|
||||
"path": "artifacts/release/build-manifest.json",
|
||||
"schemaId": "json-build-manifest"
|
||||
"schemaId": "json-build-manifest",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-runtime-config-schema-json",
|
||||
"path": "artifacts/release/runtime-config.schema.json",
|
||||
"schemaId": "json-schema-document"
|
||||
"schemaId": "json-schema-document",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-storybook-static-index-html",
|
||||
"path": "artifacts/storybook/static/index.html",
|
||||
"schemaId": "html"
|
||||
"schemaId": "html",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-storybook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-012-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-012.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-performance-bundle-json",
|
||||
"path": "artifacts/performance/bundle.json",
|
||||
"schemaId": "json-bundle-performance"
|
||||
"schemaId": "json-bundle-performance",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-bundle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-013-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-013.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-scan-sarif",
|
||||
"path": "artifacts/security/scan.sarif",
|
||||
"schemaId": "sarif-secret-scan"
|
||||
"schemaId": "sarif-secret-scan",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-dependency-inventory-json",
|
||||
"path": "artifacts/release/dependency-inventory.json",
|
||||
"schemaId": "json-dependency-inventory"
|
||||
"schemaId": "json-dependency-inventory",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-sbom-cdx-json",
|
||||
"path": "artifacts/release/sbom.cdx.json",
|
||||
"schemaId": "json-sbom"
|
||||
"schemaId": "json-sbom",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-provenance-json",
|
||||
"path": "artifacts/release/provenance.json",
|
||||
"schemaId": "json-provenance"
|
||||
"schemaId": "json-provenance",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-reproducible-build-json",
|
||||
"path": "artifacts/release/reproducible-build.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"verify-reproducible-build"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-dependency-diff-json",
|
||||
"path": "artifacts/security/dependency-diff.json",
|
||||
"schemaId": "json-dependency-diff"
|
||||
"schemaId": "json-dependency-diff",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-license-report-json",
|
||||
"path": "artifacts/security/license-report.json",
|
||||
"schemaId": "json-license-report"
|
||||
"schemaId": "json-license-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-vulnerability-report-json",
|
||||
"path": "artifacts/security/vulnerability-report.json",
|
||||
"schemaId": "json-vulnerability-report"
|
||||
"schemaId": "json-vulnerability-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-supply-chain-verification-json",
|
||||
"path": "artifacts/security/supply-chain-verification.json",
|
||||
"schemaId": "json-supply-chain-verification"
|
||||
"schemaId": "json-supply-chain-verification",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-supply-chain-coherence-json",
|
||||
"path": "artifacts/security/supply-chain-coherence.json",
|
||||
"schemaId": "json-supply-chain-coherence"
|
||||
"schemaId": "json-supply-chain-coherence",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-supply-chain-fixtures-json",
|
||||
"path": "artifacts/security/supply-chain-fixtures.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-supply-chain-fixtures"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-security-supply-chain-provider-fixtures-json",
|
||||
"path": "artifacts/security/supply-chain-provider-fixtures.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-supply-chain-provider-fixtures"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-014-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-014.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-compatibility-json",
|
||||
"path": "artifacts/release/compatibility.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"verify-compatibility"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-015-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-015.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-release-candidate-json",
|
||||
"path": "artifacts/release/release-candidate.json",
|
||||
"schemaId": "json-release-candidate"
|
||||
"schemaId": "json-release-candidate",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-verification-json",
|
||||
"path": "artifacts/release/verification.json",
|
||||
"schemaId": "json-release-verification"
|
||||
"schemaId": "json-release-verification",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"build-release-candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-016-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-016.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-runbooks-FE-RB-005-record-json",
|
||||
"path": "artifacts/runbooks/FE-RB-005/record.json",
|
||||
"schemaId": "json-runbook-record"
|
||||
"schemaId": "json-runbook-record",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"drill-runbook"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-017-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-017.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-documentation-review-json",
|
||||
"path": "artifacts/quality/documentation-review.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"verify-documentation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-018-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-018.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-performance-field-web-vitals-json",
|
||||
"path": "artifacts/performance/field-web-vitals.json",
|
||||
"schemaId": "json-field-web-vitals"
|
||||
"schemaId": "json-field-web-vitals",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"collect-web-vitals-evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-019-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-019.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-release-hosting-headers-json",
|
||||
"path": "artifacts/release/hosting-headers.json",
|
||||
"schemaId": "json-generic-json-object"
|
||||
"schemaId": "json-generic-json-object",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"verify-hosting-headers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-020-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-020.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-sample-removal-xml",
|
||||
"path": "artifacts/tests/sample-removal.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-sample-removal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-optional-recipe-removal-xml",
|
||||
"path": "artifacts/tests/optional-recipe-removal.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-optional-recipe-removal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-browser-file-storage-runtime-removal-xml",
|
||||
"path": "artifacts/tests/browser-file-storage-runtime-removal.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-browser-file-storage-removal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-tests-realtime-runtime-removal-xml",
|
||||
"path": "artifacts/tests/realtime-runtime-removal.xml",
|
||||
"schemaId": "junit"
|
||||
"schemaId": "junit",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-realtime-removal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-021-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-021.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-runbooks-FE-RB-001-record-json",
|
||||
"path": "artifacts/runbooks/FE-RB-001/record.json",
|
||||
"schemaId": "json-runbook-record"
|
||||
"schemaId": "json-runbook-record",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"drill-runbook-2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-022-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-022.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-runbooks-FE-RB-002-record-json",
|
||||
"path": "artifacts/runbooks/FE-RB-002/record.json",
|
||||
"schemaId": "json-runbook-record"
|
||||
"schemaId": "json-runbook-record",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"drill-runbook-3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-023-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-023.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-runbooks-FE-RB-003-record-json",
|
||||
"path": "artifacts/runbooks/FE-RB-003/record.json",
|
||||
"schemaId": "json-runbook-record"
|
||||
"schemaId": "json-runbook-record",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"drill-runbook-4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-024-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-024.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-runbooks-FE-RB-004-record-json",
|
||||
"path": "artifacts/runbooks/FE-RB-004/record.json",
|
||||
"schemaId": "json-runbook-record"
|
||||
"schemaId": "json-runbook-record",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"drill-runbook-5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-025-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-025.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-gates-FE-GATE-026-txt",
|
||||
"path": "artifacts/quality/gates/FE-GATE-026.txt",
|
||||
"schemaId": "text"
|
||||
"schemaId": "text",
|
||||
"production": "runner-generated"
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-performance-lab-json",
|
||||
"path": "artifacts/performance/lab.json",
|
||||
"schemaId": "json-lab-performance"
|
||||
"schemaId": "json-lab-performance",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"test-performance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-artifacts-quality-ci-contract-json",
|
||||
"path": "artifacts/quality/ci-contract.json",
|
||||
"schemaId": "json-ci-contract-report"
|
||||
"schemaId": "json-ci-contract-report",
|
||||
"production": "command-generated",
|
||||
"producerCommandIds": [
|
||||
"check-ci"
|
||||
]
|
||||
}
|
||||
],
|
||||
"gates": [
|
||||
|
||||
@@ -56,8 +56,8 @@ Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체
|
||||
|
||||
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이
|
||||
workflow의 provider job은 Linux runner에서 실행 권한이 있는
|
||||
`/usr/bin/bwrap`를 필수로 요구하며, 사용할 Node 실행 파일도 sandbox 안의
|
||||
`/usr/local/bin/node`로 고정한다. Provider command는 bubblewrap 안에서
|
||||
`/usr/bin/bwrap`를 필수로 요구하며, trusted `process.execPath`를 sandbox 안의
|
||||
`/tmp/node`에 read-only bind한다. Provider command는 bubblewrap 안에서
|
||||
`/bin/sh -eu -c`로 비대화식 실행되고 30분 안에 종료되어야 한다. Sandbox는
|
||||
workspace를 read-only로 bind하고 `.git`을 가리며, 별도의 `untrusted`
|
||||
raw-evidence 하위 디렉터리만 writable로 노출한다. 따라서 command는 전달된
|
||||
|
||||
@@ -95,8 +95,8 @@ mandatory before any generated job becomes a required check.
|
||||
|
||||
External provider supervision is fail-closed and requires a Linux runner with
|
||||
an executable `/usr/bin/bwrap`. Bubblewrap mounts the repository workspace
|
||||
read-only, hides `.git`, pins the trusted Node executable at
|
||||
`/usr/local/bin/node`, and exposes only the sibling `untrusted` raw-evidence
|
||||
read-only, hides `.git`, and read-only binds the trusted `process.execPath`
|
||||
inside the sandbox at `/tmp/node`. It exposes only the sibling `untrusted` raw-evidence
|
||||
directory as writable. Provider commands run non-interactively through
|
||||
`/bin/sh -eu -c`, receive a minimized environment plus only their own
|
||||
provider-prefixed credentials, and have a 30-minute limit. They must consume
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
import { validatePackageScriptGraph } from "./lib/package-script-graph.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
const contract = await loadCiGateContract(process.cwd());
|
||||
const removalFixtureMode = process.argv.includes("--reduced-removal-fixture");
|
||||
const contract = await loadCiGateContract(process.cwd(), {
|
||||
mode: removalFixtureMode ? "removal-fixture" : "canonical",
|
||||
});
|
||||
const index = indexCiGateContract(contract);
|
||||
const [packageDocument, nodeVersion] = await Promise.all([
|
||||
readFile("package.json", "utf8").then((value) => JSON.parse(value) as { scripts?: Record<string, string> }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
import { z } from "zod";
|
||||
@@ -246,9 +247,18 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
|
||||
.strict(),
|
||||
]);
|
||||
|
||||
const artifactSchema = z
|
||||
.object({ id, path: repositoryPath, schemaId: id })
|
||||
.strict();
|
||||
const artifactBaseShape = { id, path: repositoryPath, schemaId: id } as const;
|
||||
const artifactSchema = z.discriminatedUnion("production", [
|
||||
z.object({ ...artifactBaseShape, production: z.literal("source-controlled") }).strict(),
|
||||
z
|
||||
.object({
|
||||
...artifactBaseShape,
|
||||
production: z.literal("command-generated"),
|
||||
producerCommandIds: z.array(id).min(1).max(32),
|
||||
})
|
||||
.strict(),
|
||||
z.object({ ...artifactBaseShape, production: z.literal("runner-generated") }).strict(),
|
||||
]);
|
||||
|
||||
const gateSchema = z
|
||||
.object({
|
||||
@@ -405,6 +415,35 @@ export type CiGateContractIndex = Readonly<{
|
||||
jobs: ReadonlyMap<string, CiWorkflowJob>;
|
||||
retentionClasses: ReadonlyMap<string, CiGateContract["retention"]["classes"][number]>;
|
||||
}>;
|
||||
export type LoadCiGateContractOptions = Readonly<{
|
||||
mode?: "canonical" | "removal-fixture";
|
||||
}>;
|
||||
|
||||
const CANONICAL_GATE_SHAPE_SHA256 =
|
||||
"a4a963d0b9deffb7a0a3d755bbbcb979d72610eb74751c3a2e5eca55251e12d4";
|
||||
|
||||
function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
||||
const normalized = gates.map(
|
||||
({
|
||||
id,
|
||||
name,
|
||||
commandIds,
|
||||
logArtifactId,
|
||||
evidenceArtifactIds,
|
||||
retentionClassId,
|
||||
requiresEnvironment,
|
||||
}) => ({
|
||||
id,
|
||||
name,
|
||||
commandIds,
|
||||
logArtifactId,
|
||||
evidenceArtifactIds,
|
||||
retentionClassId,
|
||||
requiresEnvironment: requiresEnvironment ?? [],
|
||||
}),
|
||||
);
|
||||
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
||||
}
|
||||
|
||||
export function parseCiGateContract(value: unknown): CiGateContract {
|
||||
const result = ciGateContractSchema.safeParse(value);
|
||||
@@ -417,12 +456,22 @@ export function parseCiGateContract(value: unknown): CiGateContract {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function loadCiGateContract(root = process.cwd()): Promise<CiGateContract> {
|
||||
export async function loadCiGateContract(
|
||||
root = process.cwd(),
|
||||
options: LoadCiGateContractOptions = {},
|
||||
): Promise<CiGateContract> {
|
||||
const [rawContract, rawPackage] = await Promise.all([
|
||||
readFile(path.join(root, "config/ci/gates.json"), "utf8"),
|
||||
readFile(path.join(root, "package.json"), "utf8"),
|
||||
]);
|
||||
const contract = parseCiGateContract(JSON.parse(rawContract));
|
||||
const mode = options.mode ?? "canonical";
|
||||
if (
|
||||
mode === "canonical" &&
|
||||
canonicalGateShapeSha256(contract.gates) !== CANONICAL_GATE_SHAPE_SHA256
|
||||
) {
|
||||
throw new TypeError("CI gate contract canonical gate semantic shape drift");
|
||||
}
|
||||
const packageDocument = z
|
||||
.object({ scripts: z.record(z.string(), z.string()).default({}) })
|
||||
.passthrough()
|
||||
@@ -434,10 +483,23 @@ export async function loadCiGateContract(root = process.cwd()): Promise<CiGateCo
|
||||
if (missing.length > 0) {
|
||||
throw new TypeError(`CI gate contract missing package scripts: ${missing.join(", ")}`);
|
||||
}
|
||||
const expectedCheckCi = "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts && corepack pnpm check:ci-workflow";
|
||||
const expectedCheckCi = mode === "canonical"
|
||||
? "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts && corepack pnpm check:ci-workflow"
|
||||
: "corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts --reduced-removal-fixture && corepack pnpm check:ci-workflow";
|
||||
if (packageDocument.scripts["check:ci"] !== expectedCheckCi) {
|
||||
throw new TypeError("check:ci must use the exact canonical non-recursive orchestration");
|
||||
}
|
||||
const canonicalCheckCiDependencies = {
|
||||
"check:artifact-schemas": "node scripts/generate-artifact-schemas.ts --check",
|
||||
"check:ci-workflow": mode === "canonical"
|
||||
? "node scripts/generate-ci-workflow.ts --check"
|
||||
: "node scripts/generate-ci-workflow.ts --check --reduced-removal-fixture",
|
||||
} as const;
|
||||
for (const [script, expected] of Object.entries(canonicalCheckCiDependencies)) {
|
||||
if (packageDocument.scripts[script] !== expected) {
|
||||
throw new TypeError(`canonical check:ci dependency drift: ${script}`);
|
||||
}
|
||||
}
|
||||
const graphFailures = validatePackageScriptGraph(packageDocument.scripts, "check:ci");
|
||||
if (graphFailures.length > 0) {
|
||||
throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`);
|
||||
@@ -510,6 +572,16 @@ function validateContractSemantics(
|
||||
if (!schemaIds.has(artifact.schemaId)) {
|
||||
issue(`unknown artifact schema ${artifact.schemaId} for ${artifact.id}`);
|
||||
}
|
||||
if (artifact.production === "command-generated") {
|
||||
if (new Set(artifact.producerCommandIds).size !== artifact.producerCommandIds.length) {
|
||||
issue(`duplicate producer command reference for artifact: ${artifact.id}`);
|
||||
}
|
||||
for (const producerCommandId of artifact.producerCommandIds) {
|
||||
if (!commandIds.has(producerCommandId)) {
|
||||
issue(`unknown producer command ${producerCommandId} for ${artifact.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const gate of contract.gates) {
|
||||
if (new Set(gate.commandIds).size !== gate.commandIds.length) {
|
||||
@@ -524,6 +596,21 @@ function validateContractSemantics(
|
||||
for (const artifactId of [gate.logArtifactId, ...gate.evidenceArtifactIds]) {
|
||||
if (!artifactIds.has(artifactId)) issue(`unknown artifact ${artifactId} for ${gate.id}`);
|
||||
}
|
||||
const logArtifact = contract.artifacts.find(({ id }) => id === gate.logArtifactId);
|
||||
if (logArtifact && logArtifact.production !== "runner-generated") {
|
||||
issue(`gate log must be runner-generated: ${gate.id}`);
|
||||
}
|
||||
for (const artifactId of gate.evidenceArtifactIds) {
|
||||
const artifact = contract.artifacts.find(({ id }) => id === artifactId);
|
||||
if (
|
||||
artifact?.production === "command-generated" &&
|
||||
!artifact.producerCommandIds.some((producerCommandId) =>
|
||||
gate.commandIds.includes(producerCommandId)
|
||||
)
|
||||
) {
|
||||
issue(`gate lacks a bound producer command for ${artifact.id}: ${gate.id}`);
|
||||
}
|
||||
}
|
||||
if (!retentionIds.has(gate.retentionClassId)) {
|
||||
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export type GenerateCiWorkflowOptions = Readonly<{
|
||||
root: string;
|
||||
contract?: CiGateContract;
|
||||
check: boolean;
|
||||
contractMode?: "canonical" | "removal-fixture";
|
||||
}>;
|
||||
|
||||
export type GenerateCiWorkflowResult = Readonly<{
|
||||
@@ -176,7 +177,7 @@ function renderStep(
|
||||
" - name: Frozen install",
|
||||
" run: |",
|
||||
" corepack enable",
|
||||
" corepack pnpm install --frozen-lockfile",
|
||||
" corepack pnpm install --frozen-lockfile --ignore-scripts",
|
||||
];
|
||||
case "browser-install":
|
||||
return [
|
||||
@@ -335,7 +336,9 @@ export function createCiWorkflowGenerator(
|
||||
const createNonce = dependencies.createNonce ?? randomUUID;
|
||||
return async function generate(options: GenerateCiWorkflowOptions): Promise<GenerateCiWorkflowResult> {
|
||||
const root = path.resolve(options.root);
|
||||
const contract = options.contract ?? (await loadCiGateContract(root));
|
||||
const contract = options.contract ?? (await loadCiGateContract(root, {
|
||||
mode: options.contractMode ?? "canonical",
|
||||
}));
|
||||
const target = path.resolve(root, contract.providerAdapter);
|
||||
if (path.relative(root, target).startsWith("..") || path.relative(root, target) === "") {
|
||||
throw new TypeError(`workflow target escapes repository root: ${contract.providerAdapter}`);
|
||||
@@ -449,8 +452,11 @@ function hasErrorCode(error: unknown, code: string): boolean {
|
||||
const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
if (isCli) {
|
||||
const check = process.argv.includes("--check");
|
||||
const contractMode = process.argv.includes("--reduced-removal-fixture")
|
||||
? "removal-fixture" as const
|
||||
: "canonical" as const;
|
||||
try {
|
||||
const result = await generateCiWorkflow({ root: process.cwd(), check });
|
||||
const result = await generateCiWorkflow({ root: process.cwd(), check, contractMode });
|
||||
if (!result.matches) {
|
||||
process.stderr.write(
|
||||
`CI workflow drift: ${result.target} differs at byte ${result.firstDifferenceByte ?? 0}, line ${result.firstDifferenceLine ?? 1}\n`,
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
cp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
loadCiGateContract,
|
||||
parseCiGateContract,
|
||||
} from "../contracts/ci-gates.ts";
|
||||
import { generateCiWorkflow } from "../generate-ci-workflow.ts";
|
||||
|
||||
export const REMOVAL_FIXTURE_COPY_TARGETS = Object.freeze([
|
||||
"src", "tests", "recipes", "scripts", "schemas", "config", "public",
|
||||
".gitea", ".storybook", "index.html", "package.json", "tsconfig.base.json",
|
||||
"tsconfig.json", "tsconfig.app.json", "tsconfig.node.json", "tsconfig.test.json",
|
||||
"tsconfig.recipes.json", "tsconfig.web-worker.json", "tsconfig.service-worker.json",
|
||||
"vite.service-worker.config.ts", "vite.config.ts", "vitest.config.ts",
|
||||
"playwright.config.ts", "playwright.capabilities.config.ts", "playwright.dev.config.ts",
|
||||
"playwright.storybook.config.ts", "playwright.visual.config.ts", "eslint.config.ts",
|
||||
".dependency-cruiser.json", ".nvmrc",
|
||||
] as const);
|
||||
|
||||
export function requireRemovalFixtureEnvironment(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) throw new Error(`${name} is required for removal verification`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function prepareRemovalFixture(
|
||||
root: string,
|
||||
copyTargets: readonly string[] = REMOVAL_FIXTURE_COPY_TARGETS,
|
||||
): Promise<void> {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(root, { recursive: true });
|
||||
for (const target of copyTargets) {
|
||||
await cp(target, path.join(root, target), { recursive: true });
|
||||
}
|
||||
await symlink(path.resolve("node_modules"), path.join(root, "node_modules"), "dir");
|
||||
}
|
||||
|
||||
export function runRemovalFixturePnpm(
|
||||
root: string,
|
||||
pnpmCli: string,
|
||||
script: string,
|
||||
extra: readonly string[] = [],
|
||||
): boolean {
|
||||
return spawnSync(process.execPath, [pnpmCli, script, ...extra], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, CI_CONTRACT_MODE: "removal-fixture" },
|
||||
}).status === 0;
|
||||
}
|
||||
|
||||
export async function filesBelow(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
return (await Promise.all(entries.map((entry) => {
|
||||
const target = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? filesBelow(target) : [target];
|
||||
}))).flat();
|
||||
}
|
||||
|
||||
function isWithin(target: string, root: string): boolean {
|
||||
const relative = path.relative(root, target);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
export async function runtimeImportGraph(
|
||||
root: string,
|
||||
runtimeSourceRoots: readonly string[],
|
||||
): Promise<Readonly<{ dependentTests: readonly string[]; importingFiles: readonly string[] }>> {
|
||||
const files = (await filesBelow(root))
|
||||
.filter((file) => /\.(?:[cm]?ts|tsx)$/u.test(file))
|
||||
.map((file) => path.resolve(file));
|
||||
const sourceSet = new Set(files);
|
||||
const runtimeRoots = runtimeSourceRoots.map((entry) => path.resolve(root, entry));
|
||||
const imports = new Map<string, readonly string[]>();
|
||||
for (const file of files) {
|
||||
const source = await readFile(file, "utf8");
|
||||
const specifiers = [...source.matchAll(/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu)]
|
||||
.map((match) => match[1])
|
||||
.filter((specifier): specifier is string => typeof specifier === "string" && specifier.startsWith("."));
|
||||
imports.set(file, specifiers.map((specifier) => {
|
||||
const base = path.resolve(path.dirname(file), specifier);
|
||||
return [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, `${base}.cts`, path.join(base, "index.ts"), path.join(base, "index.tsx")]
|
||||
.find((candidate) => sourceSet.has(candidate)) ?? base;
|
||||
}));
|
||||
}
|
||||
const memo = new Map<string, boolean>();
|
||||
const reachesRuntime = (file: string, visiting = new Set<string>()): boolean => {
|
||||
if (runtimeRoots.some((runtimeRoot) => isWithin(file, runtimeRoot))) return true;
|
||||
const known = memo.get(file);
|
||||
if (known !== undefined) return known;
|
||||
if (visiting.has(file)) return false;
|
||||
visiting.add(file);
|
||||
const reaches = (imports.get(file) ?? []).some((dependency) =>
|
||||
runtimeRoots.some((runtimeRoot) => isWithin(dependency, runtimeRoot)) ||
|
||||
(sourceSet.has(dependency) && reachesRuntime(dependency, visiting))
|
||||
);
|
||||
visiting.delete(file);
|
||||
memo.set(file, reaches);
|
||||
return reaches;
|
||||
};
|
||||
const testsRoot = path.resolve(root, "tests");
|
||||
return Object.freeze({
|
||||
dependentTests: Object.freeze(files.filter((file) => isWithin(file, testsRoot) && reachesRuntime(file))),
|
||||
importingFiles: Object.freeze(files.filter((file) =>
|
||||
!runtimeRoots.some((runtimeRoot) => isWithin(file, runtimeRoot)) &&
|
||||
(imports.get(file) ?? []).some((dependency) =>
|
||||
runtimeRoots.some((runtimeRoot) => isWithin(dependency, runtimeRoot))
|
||||
)
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeRuntimeDependentTests(
|
||||
root: string,
|
||||
runtimeSourceRoots: readonly string[],
|
||||
): Promise<number> {
|
||||
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
|
||||
await Promise.all(graph.dependentTests.map((file) => rm(file, { force: true })));
|
||||
return graph.dependentTests.length;
|
||||
}
|
||||
|
||||
export async function assertNoRuntimeImports(
|
||||
root: string,
|
||||
runtimeSourceRoots: readonly string[],
|
||||
capability: string,
|
||||
): Promise<void> {
|
||||
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
|
||||
if (graph.importingFiles.length > 0) {
|
||||
throw new Error(`Removed ${capability} runtime is still imported by: ${graph.importingFiles.map((file) => path.relative(root, file)).join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function pruneScriptOrchestration(
|
||||
scripts: Record<string, string>,
|
||||
orchestrationScript: string,
|
||||
removedScripts: ReadonlySet<string>,
|
||||
): void {
|
||||
const command = scripts[orchestrationScript];
|
||||
if (!command) return;
|
||||
scripts[orchestrationScript] = command.split(" && ").filter((segment) =>
|
||||
![...removedScripts].some((removed) =>
|
||||
new RegExp(`(?:^|\\s)(?:corepack\\s+)?pnpm\\s+${removed.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?:\\s|$)`, "u").test(segment)
|
||||
)
|
||||
).join(" && ");
|
||||
}
|
||||
|
||||
export async function regenerateRemovalFixtureWorkflow(root: string): Promise<void> {
|
||||
const contract = await loadCiGateContract(root, { mode: "removal-fixture" });
|
||||
await generateCiWorkflow({ root, contract, check: false });
|
||||
}
|
||||
|
||||
export async function pruneRemovalFixtureCiContract(options: Readonly<{
|
||||
root: string;
|
||||
removedScripts: ReadonlySet<string>;
|
||||
removedEvidencePathFragments: readonly string[];
|
||||
}>): Promise<void> {
|
||||
const packagePath = path.join(options.root, "package.json");
|
||||
const gatesPath = path.join(options.root, "config/ci/gates.json");
|
||||
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
for (const script of options.removedScripts) delete packageDocument.scripts[script];
|
||||
packageDocument.scripts["check:ci-workflow"] =
|
||||
"node scripts/generate-ci-workflow.ts --check --reduced-removal-fixture";
|
||||
packageDocument.scripts["check:ci"] =
|
||||
"corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts --reduced-removal-fixture && corepack pnpm check:ci-workflow";
|
||||
|
||||
const contract = structuredClone(
|
||||
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
|
||||
);
|
||||
const removedCommandIds = new Set(
|
||||
contract.commands
|
||||
.filter(({ script }) => options.removedScripts.has(script))
|
||||
.map(({ id }) => id),
|
||||
);
|
||||
const missing = [...options.removedScripts].filter(
|
||||
(script) => !contract.commands.some((command) => command.script === script),
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`removal fixture CI command set is incomplete: ${missing.join(", ")}`);
|
||||
}
|
||||
const removedArtifactIds = new Set(
|
||||
contract.artifacts
|
||||
.filter(({ path: artifactPath }) =>
|
||||
options.removedEvidencePathFragments.some((fragment) => artifactPath.includes(fragment))
|
||||
)
|
||||
.map(({ id }) => id),
|
||||
);
|
||||
for (const fragment of options.removedEvidencePathFragments) {
|
||||
if (!contract.artifacts.some(({ path: artifactPath }) => artifactPath.includes(fragment))) {
|
||||
throw new Error(`removal fixture CI evidence is missing: ${fragment}`);
|
||||
}
|
||||
}
|
||||
contract.commands = contract.commands.filter(({ id }) => !removedCommandIds.has(id));
|
||||
contract.artifacts = contract.artifacts
|
||||
.filter(({ id }) => !removedArtifactIds.has(id))
|
||||
.map((artifact) => artifact.production === "command-generated"
|
||||
? {
|
||||
...artifact,
|
||||
producerCommandIds: artifact.producerCommandIds.filter(
|
||||
(commandId) => !removedCommandIds.has(commandId),
|
||||
),
|
||||
}
|
||||
: artifact)
|
||||
.filter((artifact) =>
|
||||
artifact.production !== "command-generated" || artifact.producerCommandIds.length > 0
|
||||
);
|
||||
const retainedArtifactIds = new Set(contract.artifacts.map(({ id }) => id));
|
||||
for (const gate of contract.gates) {
|
||||
gate.commandIds = gate.commandIds.filter((commandId) => !removedCommandIds.has(commandId));
|
||||
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter((artifactId) =>
|
||||
retainedArtifactIds.has(artifactId)
|
||||
);
|
||||
}
|
||||
const referencedSchemaIds = new Set(contract.artifacts.map(({ schemaId }) => schemaId));
|
||||
contract.artifactSchemas = contract.artifactSchemas.filter(({ id }) =>
|
||||
referencedSchemaIds.has(id)
|
||||
);
|
||||
const validated = parseCiGateContract(contract);
|
||||
await Promise.all([
|
||||
writeFile(packagePath, `${JSON.stringify(packageDocument, null, 2)}\n`),
|
||||
writeFile(gatesPath, `${JSON.stringify(validated, null, 2)}\n`),
|
||||
]);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
ciCheckoutIdentityFailures,
|
||||
@@ -37,6 +39,28 @@ let passed = true;
|
||||
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60 * 1_000;
|
||||
const MAX_STEP_OUTPUT_BYTES = 16 * 1024 * 1_024;
|
||||
const LOG_DIAGNOSTIC_RESERVE_BYTES = 4_096;
|
||||
const freshlyProducedArtifactIds = new Set<string>();
|
||||
const commandGeneratedEvidence = gate.evidenceArtifactIds
|
||||
.map((artifactId) => contractIndex.artifacts.get(artifactId))
|
||||
.filter((artifact) => artifact?.production === "command-generated");
|
||||
|
||||
async function observeArtifactGeneration(relativePath: string): Promise<string> {
|
||||
try {
|
||||
const metadata = await lstat(path.join(process.cwd(), relativePath), {
|
||||
bigint: true,
|
||||
});
|
||||
return [
|
||||
metadata.dev,
|
||||
metadata.ino,
|
||||
metadata.size,
|
||||
metadata.mtimeNs,
|
||||
metadata.ctimeNs,
|
||||
].join(":");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "missing";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const appendOutput = (...values: readonly string[]): boolean => {
|
||||
for (const value of values.filter(Boolean)) {
|
||||
const addedBytes = Buffer.byteLength(value, "utf8") + 1;
|
||||
@@ -97,6 +121,17 @@ if (passed) {
|
||||
for (const commandId of gate.commandIds) {
|
||||
const step = contractIndex.commands.get(commandId);
|
||||
if (!step) throw new TypeError(`CI gate command disappeared after validation: ${commandId}`);
|
||||
const producedArtifacts = commandGeneratedEvidence.filter((artifact) =>
|
||||
artifact.producerCommandIds.includes(commandId)
|
||||
);
|
||||
const generationBefore = new Map(
|
||||
await Promise.all(
|
||||
producedArtifacts.map(async (artifact) => [
|
||||
artifact.id,
|
||||
await observeArtifactGeneration(artifact.path),
|
||||
] as const),
|
||||
),
|
||||
);
|
||||
const commandLine = `$ corepack pnpm ${step.script} ${(step.args ?? []).join(" ")}`.trim();
|
||||
if (!appendOutput(commandLine) || logSchema.maxBytes - outputBytes <= LOG_DIAGNOSTIC_RESERVE_BYTES) {
|
||||
appendOutput("gate aggregate output budget exhausted before command execution");
|
||||
@@ -159,6 +194,21 @@ if (passed) {
|
||||
passed = false;
|
||||
break;
|
||||
}
|
||||
for (const artifact of producedArtifacts) {
|
||||
const generationAfter = await observeArtifactGeneration(artifact.path);
|
||||
if (generationAfter !== generationBefore.get(artifact.id)) {
|
||||
freshlyProducedArtifactIds.add(artifact.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (passed) {
|
||||
for (const artifact of commandGeneratedEvidence) {
|
||||
if (!freshlyProducedArtifactIds.has(artifact.id)) {
|
||||
appendOutput(`command-generated evidence was not freshly produced: ${artifact.path}`);
|
||||
passed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
cp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { parseCiGateContract } from "./contracts/ci-gates.ts";
|
||||
import { generateCiWorkflow } from "./generate-ci-workflow.ts";
|
||||
import {
|
||||
assertNoRuntimeImports,
|
||||
prepareRemovalFixture,
|
||||
pruneRemovalFixtureCiContract,
|
||||
regenerateRemovalFixtureWorkflow,
|
||||
removeRuntimeDependentTests,
|
||||
requireRemovalFixtureEnvironment,
|
||||
runRemovalFixturePnpm,
|
||||
} from "./lib/removal-fixture.ts";
|
||||
|
||||
const fixtureRoot = path.resolve(
|
||||
".tmp/browser-file-storage-runtime-removal",
|
||||
);
|
||||
const pnpmCli = requireEnvironment("npm_execpath");
|
||||
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
|
||||
const runtimePaths = [
|
||||
"src/application/ports/browser-file-storage",
|
||||
"src/application/ports/browser-transfer",
|
||||
@@ -42,216 +45,14 @@ const removedEvidencePathFragments = [
|
||||
"browser-capabilities",
|
||||
"browser-file-storage-runtime-removal",
|
||||
] as const;
|
||||
const copyTargets = [
|
||||
"src",
|
||||
"tests",
|
||||
"recipes",
|
||||
"scripts",
|
||||
"config",
|
||||
"schemas",
|
||||
"public",
|
||||
".gitea",
|
||||
".storybook",
|
||||
"index.html",
|
||||
"package.json",
|
||||
"tsconfig.base.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.node.json",
|
||||
"tsconfig.test.json",
|
||||
"tsconfig.recipes.json",
|
||||
"tsconfig.web-worker.json",
|
||||
"tsconfig.service-worker.json",
|
||||
"vite.service-worker.config.ts",
|
||||
"vite.config.ts",
|
||||
"vitest.config.ts",
|
||||
"playwright.config.ts",
|
||||
"playwright.capabilities.config.ts",
|
||||
"playwright.dev.config.ts",
|
||||
"playwright.storybook.config.ts",
|
||||
"playwright.visual.config.ts",
|
||||
"eslint.config.ts",
|
||||
".dependency-cruiser.json",
|
||||
".nvmrc",
|
||||
] as const;
|
||||
|
||||
function requireEnvironment(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required for runtime removal verification`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function runPnpm(script: string): boolean {
|
||||
return (
|
||||
spawnSync(process.execPath, [pnpmCli, script], {
|
||||
cwd: fixtureRoot,
|
||||
stdio: "inherit",
|
||||
}).status === 0
|
||||
);
|
||||
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
|
||||
}
|
||||
|
||||
async function sourceFiles(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
return (
|
||||
await Promise.all(
|
||||
entries.map(async (entry): Promise<string[]> => {
|
||||
const target = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return await sourceFiles(target);
|
||||
}
|
||||
return /\.(?:[cm]?ts|tsx)$/u.test(entry.name)
|
||||
? [path.resolve(target)]
|
||||
: [];
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
|
||||
function staticImportSpecifiers(source: string): string[] {
|
||||
return [
|
||||
...source.matchAll(
|
||||
/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu,
|
||||
),
|
||||
]
|
||||
.map((match) => match[1])
|
||||
.filter((specifier): specifier is string =>
|
||||
typeof specifier === "string",
|
||||
);
|
||||
}
|
||||
|
||||
function isWithin(target: string, root: string): boolean {
|
||||
const relative = path.relative(root, target);
|
||||
return (
|
||||
relative === "" ||
|
||||
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
||||
);
|
||||
}
|
||||
|
||||
function resolvedImport(
|
||||
importer: string,
|
||||
specifier: string,
|
||||
sourceSet: ReadonlySet<string>,
|
||||
): string | null {
|
||||
if (!specifier.startsWith(".")) return null;
|
||||
const base = path.resolve(path.dirname(importer), specifier);
|
||||
const candidates = [
|
||||
base,
|
||||
`${base}.ts`,
|
||||
`${base}.tsx`,
|
||||
`${base}.mts`,
|
||||
`${base}.cts`,
|
||||
path.join(base, "index.ts"),
|
||||
path.join(base, "index.tsx"),
|
||||
];
|
||||
return candidates.find((candidate) => sourceSet.has(candidate)) ?? base;
|
||||
}
|
||||
|
||||
async function runtimeImportGraph(root: string): Promise<Readonly<{
|
||||
dependentTests: readonly string[];
|
||||
importingFiles: readonly string[];
|
||||
}>> {
|
||||
const files = await sourceFiles(root);
|
||||
const sourceSet = new Set(files);
|
||||
const runtimeRoots = runtimeSourceRoots.map((entry) =>
|
||||
path.resolve(root, entry),
|
||||
);
|
||||
const imports = new Map<string, readonly string[]>();
|
||||
for (const file of files) {
|
||||
const source = await readFile(file, "utf8");
|
||||
imports.set(
|
||||
file,
|
||||
staticImportSpecifiers(source)
|
||||
.map((specifier) =>
|
||||
resolvedImport(file, specifier, sourceSet),
|
||||
)
|
||||
.filter((target): target is string => target !== null),
|
||||
);
|
||||
}
|
||||
|
||||
const memo = new Map<string, boolean>();
|
||||
const reachesRuntime = (
|
||||
file: string,
|
||||
visiting = new Set<string>(),
|
||||
): boolean => {
|
||||
if (runtimeRoots.some((root) => isWithin(file, root))) return true;
|
||||
const known = memo.get(file);
|
||||
if (known !== undefined) return known;
|
||||
if (visiting.has(file)) return false;
|
||||
visiting.add(file);
|
||||
const reaches = (imports.get(file) ?? []).some(
|
||||
(dependency) =>
|
||||
runtimeRoots.some((runtimeRoot) =>
|
||||
isWithin(dependency, runtimeRoot),
|
||||
) ||
|
||||
(sourceSet.has(dependency) &&
|
||||
reachesRuntime(dependency, visiting)),
|
||||
);
|
||||
visiting.delete(file);
|
||||
memo.set(file, reaches);
|
||||
return reaches;
|
||||
};
|
||||
|
||||
const testsRoot = path.resolve(root, "tests");
|
||||
const dependentTests = files.filter(
|
||||
(file) => isWithin(file, testsRoot) && reachesRuntime(file),
|
||||
);
|
||||
const importingFiles = files.filter(
|
||||
(file) =>
|
||||
!runtimeRoots.some((runtimeRoot) =>
|
||||
isWithin(file, runtimeRoot),
|
||||
) &&
|
||||
(imports.get(file) ?? []).some((dependency) =>
|
||||
runtimeRoots.some((runtimeRoot) =>
|
||||
isWithin(dependency, runtimeRoot),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Object.freeze({
|
||||
dependentTests: Object.freeze(dependentTests),
|
||||
importingFiles: Object.freeze(importingFiles),
|
||||
});
|
||||
}
|
||||
|
||||
async function assertNoRuntimeImports(root: string): Promise<void> {
|
||||
const graph = await runtimeImportGraph(root);
|
||||
if (graph.importingFiles.length > 0) {
|
||||
throw new Error(
|
||||
`Removed browser file/storage runtime is still imported by: ${graph.importingFiles
|
||||
.map((file) => path.relative(root, file))
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRuntimeDependentTests(
|
||||
root: string,
|
||||
): Promise<number> {
|
||||
const graph = await runtimeImportGraph(root);
|
||||
await Promise.all(
|
||||
graph.dependentTests.map(async (file) => {
|
||||
if (isWithin(file, path.resolve(root, "tests"))) {
|
||||
await rm(file, { force: true });
|
||||
}
|
||||
}),
|
||||
);
|
||||
return graph.dependentTests.length;
|
||||
}
|
||||
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
await mkdir(fixtureRoot, { recursive: true });
|
||||
for (const target of copyTargets) {
|
||||
await cp(target, path.join(fixtureRoot, target), { recursive: true });
|
||||
}
|
||||
await symlink(
|
||||
path.resolve("node_modules"),
|
||||
path.join(fixtureRoot, "node_modules"),
|
||||
"dir",
|
||||
);
|
||||
await prepareRemovalFixture(fixtureRoot);
|
||||
|
||||
const removedRuntimeTests =
|
||||
await removeRuntimeDependentTests(fixtureRoot);
|
||||
await removeRuntimeDependentTests(fixtureRoot, runtimeSourceRoots);
|
||||
for (const runtimePath of runtimePaths) {
|
||||
await rm(path.join(fixtureRoot, runtimePath), {
|
||||
recursive: true,
|
||||
@@ -290,17 +91,6 @@ if (removedRuntimeEntries !== 3) {
|
||||
}
|
||||
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
||||
|
||||
const packagePath = path.join(fixtureRoot, "package.json");
|
||||
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
for (const script of removedScripts) {
|
||||
delete packageDocument.scripts[script];
|
||||
}
|
||||
await writeFile(
|
||||
packagePath,
|
||||
`${JSON.stringify(packageDocument, null, 2)}\n`,
|
||||
);
|
||||
await rm(path.join(fixtureRoot, "playwright.capabilities.config.ts"), {
|
||||
force: true,
|
||||
});
|
||||
@@ -330,67 +120,17 @@ await rm(
|
||||
{ force: true },
|
||||
);
|
||||
|
||||
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
|
||||
const gatesDocument = structuredClone(
|
||||
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
|
||||
);
|
||||
const removedCommandIds = new Set(
|
||||
gatesDocument.commands
|
||||
.filter(({ script }) => removedScripts.has(script))
|
||||
.map(({ id }) => id),
|
||||
);
|
||||
if (removedCommandIds.size !== removedScripts.size) {
|
||||
throw new Error("Browser file/storage CI command removal set is incomplete");
|
||||
}
|
||||
const removedArtifactIds = new Set(
|
||||
gatesDocument.artifacts
|
||||
.filter(({ path: artifactPath }) =>
|
||||
removedEvidencePathFragments.some((fragment) =>
|
||||
artifactPath.includes(fragment),
|
||||
),
|
||||
)
|
||||
.map(({ id }) => id),
|
||||
);
|
||||
for (const fragment of removedEvidencePathFragments) {
|
||||
if (
|
||||
!gatesDocument.artifacts.some(({ path: artifactPath }) =>
|
||||
artifactPath.includes(fragment),
|
||||
)
|
||||
) {
|
||||
throw new Error(`Browser file/storage CI evidence is missing: ${fragment}`);
|
||||
}
|
||||
}
|
||||
gatesDocument.commands = gatesDocument.commands.filter(
|
||||
({ id }) => !removedCommandIds.has(id),
|
||||
);
|
||||
gatesDocument.artifacts = gatesDocument.artifacts.filter(
|
||||
({ id }) => !removedArtifactIds.has(id),
|
||||
);
|
||||
for (const gate of gatesDocument.gates) {
|
||||
gate.commandIds = gate.commandIds.filter(
|
||||
(commandId) => !removedCommandIds.has(commandId),
|
||||
);
|
||||
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
|
||||
(artifactId) => !removedArtifactIds.has(artifactId),
|
||||
);
|
||||
}
|
||||
const referencedSchemaIds = new Set(
|
||||
gatesDocument.artifacts.map(({ schemaId }) => schemaId),
|
||||
);
|
||||
gatesDocument.artifactSchemas = gatesDocument.artifactSchemas.filter(
|
||||
({ id }) => referencedSchemaIds.has(id),
|
||||
);
|
||||
const validatedGates = parseCiGateContract(gatesDocument);
|
||||
await writeFile(
|
||||
gatesPath,
|
||||
`${JSON.stringify(validatedGates, null, 2)}\n`,
|
||||
);
|
||||
await generateCiWorkflow({
|
||||
await pruneRemovalFixtureCiContract({
|
||||
root: fixtureRoot,
|
||||
contract: validatedGates,
|
||||
check: false,
|
||||
removedScripts,
|
||||
removedEvidencePathFragments,
|
||||
});
|
||||
await assertNoRuntimeImports(fixtureRoot);
|
||||
await regenerateRemovalFixtureWorkflow(fixtureRoot);
|
||||
await assertNoRuntimeImports(
|
||||
fixtureRoot,
|
||||
runtimeSourceRoots,
|
||||
"browser file/storage",
|
||||
);
|
||||
|
||||
const checks: Array<readonly [string, boolean]> = [
|
||||
["typecheck", runPnpm("check:types")],
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
cp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
filesBelow,
|
||||
prepareRemovalFixture,
|
||||
pruneRemovalFixtureCiContract,
|
||||
pruneScriptOrchestration,
|
||||
regenerateRemovalFixtureWorkflow,
|
||||
requireRemovalFixtureEnvironment,
|
||||
runRemovalFixturePnpm,
|
||||
} from "./lib/removal-fixture.ts";
|
||||
|
||||
const fixtureRoot = path.resolve(".tmp/optional-recipe-removal");
|
||||
const pnpmCli = requireEnvironment("npm_execpath");
|
||||
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
|
||||
const copyTargets = [
|
||||
"src",
|
||||
"tests",
|
||||
@@ -44,51 +50,55 @@ const copyTargets = [
|
||||
".nvmrc",
|
||||
];
|
||||
|
||||
function requireEnvironment(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required to run removal verification`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function runPnpm(script: string): boolean {
|
||||
return (
|
||||
spawnSync(process.execPath, [pnpmCli, script], {
|
||||
cwd: fixtureRoot,
|
||||
stdio: "inherit",
|
||||
}).status === 0
|
||||
);
|
||||
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
|
||||
}
|
||||
|
||||
async function filesBelow(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const groups = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const target = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? filesBelow(target) : [target];
|
||||
}),
|
||||
);
|
||||
return groups.flat();
|
||||
await prepareRemovalFixture(fixtureRoot, copyTargets);
|
||||
for (const rootOnlyTest of [
|
||||
"tests/unit/ci-workflow-generation.test.ts",
|
||||
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
|
||||
]) {
|
||||
await rm(path.join(fixtureRoot, rootOnlyTest), { force: true });
|
||||
}
|
||||
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
await mkdir(fixtureRoot, { recursive: true });
|
||||
for (const target of copyTargets) {
|
||||
await cp(target, path.join(fixtureRoot, target), { recursive: true });
|
||||
}
|
||||
await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
|
||||
await rm(path.join(fixtureRoot, "recipes"), { recursive: true, force: true });
|
||||
await rm(path.join(fixtureRoot, "tests/recipes"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
const removedCiScripts = new Set([
|
||||
"check:types:recipes",
|
||||
"test:recipes",
|
||||
"check:optional-recipes",
|
||||
"check:optional-recipes:source",
|
||||
"check:optional-recipe-fixtures",
|
||||
"test:optional-recipe-removal",
|
||||
]);
|
||||
const fixturePackagePath = path.join(fixtureRoot, "package.json");
|
||||
const fixturePackage = JSON.parse(await readFile(fixturePackagePath, "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
pruneScriptOrchestration(fixturePackage.scripts, "check:types", removedCiScripts);
|
||||
pruneScriptOrchestration(fixturePackage.scripts, "test:all", removedCiScripts);
|
||||
await writeFile(fixturePackagePath, `${JSON.stringify(fixturePackage, null, 2)}\n`);
|
||||
await pruneRemovalFixtureCiContract({
|
||||
root: fixtureRoot,
|
||||
removedScripts: removedCiScripts,
|
||||
removedEvidencePathFragments: [
|
||||
"optional-recipes",
|
||||
"optional-recipe-fixtures",
|
||||
"optional-recipe-removal",
|
||||
],
|
||||
});
|
||||
await regenerateRemovalFixtureWorkflow(fixtureRoot);
|
||||
|
||||
const checks: Array<[string, boolean]> = [
|
||||
["typecheck", runPnpm("check:types")],
|
||||
["architecture", runPnpm("check:architecture")],
|
||||
["test", runPnpm("test:all")],
|
||||
["build", runPnpm("build")],
|
||||
["ci-contract", runPnpm("check:ci")],
|
||||
];
|
||||
const residue: string[] = [];
|
||||
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
cp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { parseCiGateContract } from "./contracts/ci-gates.ts";
|
||||
import { generateCiWorkflow } from "./generate-ci-workflow.ts";
|
||||
import {
|
||||
assertNoRuntimeImports,
|
||||
prepareRemovalFixture,
|
||||
pruneRemovalFixtureCiContract,
|
||||
regenerateRemovalFixtureWorkflow,
|
||||
removeRuntimeDependentTests,
|
||||
requireRemovalFixtureEnvironment,
|
||||
runRemovalFixturePnpm,
|
||||
} from "./lib/removal-fixture.ts";
|
||||
|
||||
const fixtureRoot = path.resolve(".tmp/realtime-runtime-removal");
|
||||
const pnpmCli = requireEnvironment("npm_execpath");
|
||||
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
|
||||
const runtimePaths = [
|
||||
"src/application/ports/realtime",
|
||||
"src/application/ports/out/web-push-control.ts",
|
||||
@@ -38,212 +41,14 @@ const removedEvidencePathFragments = [
|
||||
"realtime-boundaries",
|
||||
"realtime-runtime-removal",
|
||||
] as const;
|
||||
const copyTargets = [
|
||||
"src",
|
||||
"tests",
|
||||
"recipes",
|
||||
"scripts",
|
||||
"config",
|
||||
"schemas",
|
||||
"public",
|
||||
".gitea",
|
||||
".storybook",
|
||||
"index.html",
|
||||
"package.json",
|
||||
"tsconfig.base.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.node.json",
|
||||
"tsconfig.test.json",
|
||||
"tsconfig.recipes.json",
|
||||
"tsconfig.web-worker.json",
|
||||
"tsconfig.service-worker.json",
|
||||
"vite.service-worker.config.ts",
|
||||
"vite.config.ts",
|
||||
"vitest.config.ts",
|
||||
"playwright.config.ts",
|
||||
"playwright.capabilities.config.ts",
|
||||
"playwright.dev.config.ts",
|
||||
"playwright.storybook.config.ts",
|
||||
"playwright.visual.config.ts",
|
||||
"eslint.config.ts",
|
||||
".dependency-cruiser.json",
|
||||
".nvmrc",
|
||||
] as const;
|
||||
|
||||
function requireEnvironment(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required for runtime removal verification`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function runPnpm(script: string): boolean {
|
||||
return (
|
||||
spawnSync(process.execPath, [pnpmCli, script], {
|
||||
cwd: fixtureRoot,
|
||||
stdio: "inherit",
|
||||
}).status === 0
|
||||
);
|
||||
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
|
||||
}
|
||||
|
||||
async function sourceFiles(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
return (
|
||||
await Promise.all(
|
||||
entries.map(async (entry): Promise<string[]> => {
|
||||
const target = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) return await sourceFiles(target);
|
||||
return /\.(?:[cm]?ts|tsx)$/u.test(entry.name)
|
||||
? [path.resolve(target)]
|
||||
: [];
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
|
||||
function staticImportSpecifiers(source: string): string[] {
|
||||
return [
|
||||
...source.matchAll(
|
||||
/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu,
|
||||
),
|
||||
]
|
||||
.map((match) => match[1])
|
||||
.filter((specifier): specifier is string =>
|
||||
typeof specifier === "string",
|
||||
);
|
||||
}
|
||||
|
||||
function isWithin(target: string, root: string): boolean {
|
||||
const relative = path.relative(root, target);
|
||||
return (
|
||||
relative === "" ||
|
||||
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
||||
);
|
||||
}
|
||||
|
||||
function resolvedImport(
|
||||
importer: string,
|
||||
specifier: string,
|
||||
sourceSet: ReadonlySet<string>,
|
||||
): string | null {
|
||||
if (!specifier.startsWith(".")) return null;
|
||||
const base = path.resolve(path.dirname(importer), specifier);
|
||||
const candidates = [
|
||||
base,
|
||||
`${base}.ts`,
|
||||
`${base}.tsx`,
|
||||
`${base}.mts`,
|
||||
`${base}.cts`,
|
||||
path.join(base, "index.ts"),
|
||||
path.join(base, "index.tsx"),
|
||||
];
|
||||
return candidates.find((candidate) => sourceSet.has(candidate)) ?? base;
|
||||
}
|
||||
|
||||
async function runtimeImportGraph(root: string): Promise<Readonly<{
|
||||
dependentTests: readonly string[];
|
||||
importingFiles: readonly string[];
|
||||
}>> {
|
||||
const files = await sourceFiles(root);
|
||||
const sourceSet = new Set(files);
|
||||
const runtimeRoots = runtimeSourceRoots.map((entry) =>
|
||||
path.resolve(root, entry),
|
||||
);
|
||||
const imports = new Map<string, readonly string[]>();
|
||||
for (const file of files) {
|
||||
const source = await readFile(file, "utf8");
|
||||
imports.set(
|
||||
file,
|
||||
staticImportSpecifiers(source)
|
||||
.map((specifier) => resolvedImport(file, specifier, sourceSet))
|
||||
.filter((target): target is string => target !== null),
|
||||
);
|
||||
}
|
||||
|
||||
const memo = new Map<string, boolean>();
|
||||
const reachesRuntime = (
|
||||
file: string,
|
||||
visiting = new Set<string>(),
|
||||
): boolean => {
|
||||
if (runtimeRoots.some((root) => isWithin(file, root))) return true;
|
||||
const known = memo.get(file);
|
||||
if (known !== undefined) return known;
|
||||
if (visiting.has(file)) return false;
|
||||
visiting.add(file);
|
||||
const reaches = (imports.get(file) ?? []).some(
|
||||
(dependency) =>
|
||||
runtimeRoots.some((runtimeRoot) =>
|
||||
isWithin(dependency, runtimeRoot),
|
||||
) ||
|
||||
(sourceSet.has(dependency) &&
|
||||
reachesRuntime(dependency, visiting)),
|
||||
);
|
||||
visiting.delete(file);
|
||||
memo.set(file, reaches);
|
||||
return reaches;
|
||||
};
|
||||
|
||||
const testsRoot = path.resolve(root, "tests");
|
||||
return Object.freeze({
|
||||
dependentTests: Object.freeze(
|
||||
files.filter(
|
||||
(file) => isWithin(file, testsRoot) && reachesRuntime(file),
|
||||
),
|
||||
),
|
||||
importingFiles: Object.freeze(
|
||||
files.filter(
|
||||
(file) =>
|
||||
!runtimeRoots.some((runtimeRoot) =>
|
||||
isWithin(file, runtimeRoot),
|
||||
) &&
|
||||
(imports.get(file) ?? []).some((dependency) =>
|
||||
runtimeRoots.some((runtimeRoot) =>
|
||||
isWithin(dependency, runtimeRoot),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function removeRuntimeDependentTests(root: string): Promise<number> {
|
||||
const graph = await runtimeImportGraph(root);
|
||||
await Promise.all(
|
||||
graph.dependentTests.map(async (file) => {
|
||||
if (isWithin(file, path.resolve(root, "tests"))) {
|
||||
await rm(file, { force: true });
|
||||
}
|
||||
}),
|
||||
);
|
||||
return graph.dependentTests.length;
|
||||
}
|
||||
|
||||
async function assertNoRuntimeImports(root: string): Promise<void> {
|
||||
const graph = await runtimeImportGraph(root);
|
||||
if (graph.importingFiles.length > 0) {
|
||||
throw new Error(
|
||||
`Removed realtime runtime is still imported by: ${graph.importingFiles
|
||||
.map((file) => path.relative(root, file))
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
await mkdir(fixtureRoot, { recursive: true });
|
||||
for (const target of copyTargets) {
|
||||
await cp(target, path.join(fixtureRoot, target), { recursive: true });
|
||||
}
|
||||
await symlink(
|
||||
path.resolve("node_modules"),
|
||||
path.join(fixtureRoot, "node_modules"),
|
||||
"dir",
|
||||
);
|
||||
await prepareRemovalFixture(fixtureRoot);
|
||||
|
||||
const removedRuntimeTests =
|
||||
await removeRuntimeDependentTests(fixtureRoot);
|
||||
await removeRuntimeDependentTests(fixtureRoot, runtimeSourceRoots);
|
||||
for (const runtimePath of runtimePaths) {
|
||||
await rm(path.join(fixtureRoot, runtimePath), {
|
||||
recursive: true,
|
||||
@@ -280,17 +85,6 @@ if (!realtimeRecipe || !Object.hasOwn(realtimeRecipe, "referenceRuntime")) {
|
||||
delete realtimeRecipe.referenceRuntime;
|
||||
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
||||
|
||||
const packagePath = path.join(fixtureRoot, "package.json");
|
||||
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
for (const script of runtimeScripts) {
|
||||
delete packageDocument.scripts[script];
|
||||
}
|
||||
await writeFile(
|
||||
packagePath,
|
||||
`${JSON.stringify(packageDocument, null, 2)}\n`,
|
||||
);
|
||||
for (const scriptPath of [
|
||||
"scripts/check-realtime-boundaries.ts",
|
||||
"scripts/check-realtime-boundary-fixtures.ts",
|
||||
@@ -313,68 +107,13 @@ await rm(
|
||||
{ force: true },
|
||||
);
|
||||
|
||||
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
|
||||
const gatesDocument = structuredClone(
|
||||
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
|
||||
);
|
||||
const removedScripts = new Set<string>(runtimeScripts);
|
||||
const removedCommandIds = new Set(
|
||||
gatesDocument.commands
|
||||
.filter(({ script }) => removedScripts.has(script))
|
||||
.map(({ id }) => id),
|
||||
);
|
||||
if (removedCommandIds.size !== runtimeScripts.length) {
|
||||
throw new Error("Realtime CI command removal set is incomplete");
|
||||
}
|
||||
const removedArtifactIds = new Set(
|
||||
gatesDocument.artifacts
|
||||
.filter(({ path: artifactPath }) =>
|
||||
removedEvidencePathFragments.some((fragment) =>
|
||||
artifactPath.includes(fragment),
|
||||
),
|
||||
)
|
||||
.map(({ id }) => id),
|
||||
);
|
||||
for (const fragment of removedEvidencePathFragments) {
|
||||
if (
|
||||
!gatesDocument.artifacts.some(({ path: artifactPath }) =>
|
||||
artifactPath.includes(fragment),
|
||||
)
|
||||
) {
|
||||
throw new Error(`Realtime CI evidence is missing: ${fragment}`);
|
||||
}
|
||||
}
|
||||
gatesDocument.commands = gatesDocument.commands.filter(
|
||||
({ id }) => !removedCommandIds.has(id),
|
||||
);
|
||||
gatesDocument.artifacts = gatesDocument.artifacts.filter(
|
||||
({ id }) => !removedArtifactIds.has(id),
|
||||
);
|
||||
for (const gate of gatesDocument.gates) {
|
||||
gate.commandIds = gate.commandIds.filter(
|
||||
(commandId) => !removedCommandIds.has(commandId),
|
||||
);
|
||||
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
|
||||
(artifactId) => !removedArtifactIds.has(artifactId),
|
||||
);
|
||||
}
|
||||
const referencedSchemaIds = new Set(
|
||||
gatesDocument.artifacts.map(({ schemaId }) => schemaId),
|
||||
);
|
||||
gatesDocument.artifactSchemas = gatesDocument.artifactSchemas.filter(
|
||||
({ id }) => referencedSchemaIds.has(id),
|
||||
);
|
||||
const validatedGates = parseCiGateContract(gatesDocument);
|
||||
await writeFile(
|
||||
gatesPath,
|
||||
`${JSON.stringify(validatedGates, null, 2)}\n`,
|
||||
);
|
||||
await generateCiWorkflow({
|
||||
await pruneRemovalFixtureCiContract({
|
||||
root: fixtureRoot,
|
||||
contract: validatedGates,
|
||||
check: false,
|
||||
removedScripts: new Set<string>(runtimeScripts),
|
||||
removedEvidencePathFragments,
|
||||
});
|
||||
await assertNoRuntimeImports(fixtureRoot);
|
||||
await regenerateRemovalFixtureWorkflow(fixtureRoot);
|
||||
await assertNoRuntimeImports(fixtureRoot, runtimeSourceRoots, "realtime");
|
||||
|
||||
const checks: Array<readonly [string, boolean]> = [
|
||||
["typecheck", runPnpm("check:types")],
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
access,
|
||||
cp,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { isProductionModulePath } from "./lib/risk-coverage.ts";
|
||||
import {
|
||||
filesBelow,
|
||||
prepareRemovalFixture,
|
||||
pruneRemovalFixtureCiContract,
|
||||
pruneScriptOrchestration,
|
||||
regenerateRemovalFixtureWorkflow,
|
||||
requireRemovalFixtureEnvironment,
|
||||
runRemovalFixturePnpm,
|
||||
} from "./lib/removal-fixture.ts";
|
||||
|
||||
const fixtureParent = path.resolve(".tmp");
|
||||
await mkdir(fixtureParent, { recursive: true });
|
||||
const fixtureRoot = await mkdtemp(
|
||||
path.join(fixtureParent, "reference-feature-removal-"),
|
||||
);
|
||||
const pnpmCli = requireEnvironment("npm_execpath");
|
||||
const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
|
||||
const featureSource = "src/features/reference-feature";
|
||||
const featureTests = "tests/features/reference-feature";
|
||||
const commonTestPaths = [
|
||||
@@ -162,36 +167,20 @@ type GovernanceRegistry = Record<string, unknown> & {
|
||||
};
|
||||
type RemovalGovernance = { registries: GovernanceRegistry[] };
|
||||
|
||||
function requireEnvironment(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) throw new Error(`${name} is required for sample removal`);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function filesBelow(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const groups = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const target = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? filesBelow(target) : [target];
|
||||
}),
|
||||
);
|
||||
return groups.flat();
|
||||
}
|
||||
|
||||
function runPnpm(script: string, extra: string[] = []): boolean {
|
||||
const result = spawnSync(process.execPath, [pnpmCli, script, ...extra], {
|
||||
cwd: fixtureRoot,
|
||||
stdio: "inherit",
|
||||
});
|
||||
return result.status === 0;
|
||||
return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script, extra);
|
||||
}
|
||||
|
||||
try {
|
||||
for (const target of copyTargets) {
|
||||
await cp(target, path.join(fixtureRoot, target), { recursive: true });
|
||||
await prepareRemovalFixture(fixtureRoot, copyTargets);
|
||||
for (const excludedFixtureTest of [
|
||||
"tests/unit/ci-workflow-generation.test.ts",
|
||||
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
|
||||
"tests/unit/removal-fixture.test.ts",
|
||||
"tests/unit/http-scenario-evidence.test.ts",
|
||||
]) {
|
||||
await rm(path.join(fixtureRoot, excludedFixtureTest), { force: true });
|
||||
}
|
||||
await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
|
||||
|
||||
const coveragePolicyFile = path.join(
|
||||
fixtureRoot,
|
||||
@@ -329,6 +318,33 @@ try {
|
||||
`${JSON.stringify(removalGovernance, null, 2)}\n`,
|
||||
);
|
||||
|
||||
const removedCiScripts = new Set([
|
||||
"check:types:fixture:feature-input",
|
||||
"check:types:fixture:reference-operation",
|
||||
"test:http-scenario-evidence",
|
||||
"test:reference-feature",
|
||||
]);
|
||||
const fixturePackagePath = path.join(fixtureRoot, "package.json");
|
||||
const fixturePackage = JSON.parse(await readFile(fixturePackagePath, "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
pruneScriptOrchestration(fixturePackage.scripts, "test:all", removedCiScripts);
|
||||
fixturePackage.scripts["test:coverage"] = fixturePackage.scripts["test:coverage"]
|
||||
.replace(" tests/features/reference-feature", "");
|
||||
delete fixturePackage.scripts["check:http-scenario-evidence"];
|
||||
delete fixturePackage.scripts["check:http-scenario-evidence:fixture"];
|
||||
await writeFile(fixturePackagePath, `${JSON.stringify(fixturePackage, null, 2)}\n`);
|
||||
await pruneRemovalFixtureCiContract({
|
||||
root: fixtureRoot,
|
||||
removedScripts: removedCiScripts,
|
||||
removedEvidencePathFragments: [
|
||||
"reference-feature.xml",
|
||||
"http-scenario-executions",
|
||||
"http-scenario-evidence",
|
||||
],
|
||||
});
|
||||
await regenerateRemovalFixtureWorkflow(fixtureRoot);
|
||||
|
||||
const residue: string[] = [];
|
||||
for (const root of ["src", "tests"]) {
|
||||
for (const file of await filesBelow(path.join(fixtureRoot, root))) {
|
||||
@@ -366,6 +382,7 @@ try {
|
||||
["unit-integration", runPnpm("test:all")],
|
||||
["coverage", runPnpm("test:coverage")],
|
||||
["test-evidence-source", runPnpm("check:test-evidence:source")],
|
||||
["ci-contract", runPnpm("check:ci")],
|
||||
[
|
||||
"home-smoke",
|
||||
runPnpm("exec", [
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"providerAdapter": ".gitea/workflows/quality-gates.yml",
|
||||
"commands": [],
|
||||
"artifactSchemas": [{ "id": "text", "kind": "text", "maxBytes": 1024 }],
|
||||
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "text" }],
|
||||
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "text", "production": "runner-generated" }],
|
||||
"gates": [
|
||||
{ "id": "FE-GATE-001", "name": "one", "commandIds": ["command"], "logArtifactId": "log", "evidenceArtifactIds": ["log"], "retentionClassId": "merge" },
|
||||
{ "id": "FE-GATE-001", "name": "duplicate", "commandIds": ["command"], "logArtifactId": "log", "evidenceArtifactIds": ["log"], "retentionClassId": "merge" }
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"providerAdapter": ".gitea/workflows/quality-gates.yml",
|
||||
"commands": [],
|
||||
"artifactSchemas": [],
|
||||
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "missing" }],
|
||||
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "missing", "production": "runner-generated" }],
|
||||
"gates": [],
|
||||
"stages": [],
|
||||
"jobs": [],
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Install Playwright browsers
|
||||
if: \${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Install Playwright browsers
|
||||
if: \${{ matrix.browser }}
|
||||
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Build candidate once and verify local evidence
|
||||
run: corepack pnpm ci:gate -- FE-GATE-015
|
||||
- name: Archive and validate the exact candidate file set
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Download release candidate
|
||||
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
|
||||
with:
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- \${{ matrix.gate }}
|
||||
- name: Upload production gate evidence
|
||||
@@ -369,7 +369,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run blocking gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-018
|
||||
- name: Upload field gate evidence
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
- name: Frozen install
|
||||
run: |
|
||||
corepack enable
|
||||
corepack pnpm install --frozen-lockfile
|
||||
corepack pnpm install --frozen-lockfile --ignore-scripts
|
||||
- name: Run documentation gate
|
||||
run: corepack pnpm ci:gate -- FE-GATE-017
|
||||
- name: Upload documentation gate evidence
|
||||
|
||||
@@ -55,7 +55,12 @@ async function writeArtifact(root: string, relative: string, value: string | Buf
|
||||
}
|
||||
|
||||
function artifact(pathname: string, schemaId: string): CiGateArtifact {
|
||||
return { id: `artifact-${schemaId}`, path: pathname, schemaId };
|
||||
return {
|
||||
id: `artifact-${schemaId}`,
|
||||
path: pathname,
|
||||
schemaId,
|
||||
production: "source-controlled",
|
||||
};
|
||||
}
|
||||
|
||||
describe("CI artifact validator", () => {
|
||||
|
||||
@@ -123,6 +123,7 @@ describe("CI gate contract", () => {
|
||||
["duplicate stage id", (value: Record<string, any>) => value.stages.push({ ...value.stages[0] }), /duplicate stage id/i],
|
||||
["duplicate job id", (value: Record<string, any>) => value.jobs.push({ ...value.jobs[0] }), /duplicate job id/i],
|
||||
["duplicate artifact path", (value: Record<string, any>) => value.artifacts.push({ ...value.artifacts[0], id: "duplicate-path" }), /duplicate artifact path/i],
|
||||
["missing artifact production classification", (value: Record<string, any>) => delete value.artifacts[0].production, /production/i],
|
||||
["unknown command reference", (value: Record<string, any>) => value.gates[0].commandIds.push("missing-command"), /unknown command missing-command/i],
|
||||
["unknown artifact reference", (value: Record<string, any>) => (value.gates[0].logArtifactId = "missing-artifact"), /unknown artifact missing-artifact/i],
|
||||
["unknown schema reference", (value: Record<string, any>) => (value.artifacts[0].schemaId = "missing-schema"), /unknown artifact schema missing-schema/i],
|
||||
@@ -177,6 +178,27 @@ describe("CI gate contract", () => {
|
||||
await expect(loadCiGateContract(root)).rejects.toThrow(/missing package scripts/i);
|
||||
});
|
||||
|
||||
it("rejects swapped canonical gate command ownership", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-gate-shape-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
const contract = JSON.parse(
|
||||
JSON.stringify(await loadCiGateContract(process.cwd())),
|
||||
) as Record<string, any>;
|
||||
const security = contract.gates.find((gate: Record<string, any>) => gate.id === "FE-GATE-013");
|
||||
const documentation = contract.gates.find((gate: Record<string, any>) => gate.id === "FE-GATE-017");
|
||||
[security.commandIds, documentation.commandIds] = [
|
||||
documentation.commandIds,
|
||||
security.commandIds,
|
||||
];
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
||||
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
|
||||
|
||||
await expect(loadCiGateContract(root)).rejects.toThrow(
|
||||
/canonical gate semantic shape|lacks a bound producer command/i,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["check:artifact-schemas", "check:ci-workflow"])(
|
||||
"rejects a missing nested check:ci dependency: %s",
|
||||
async (removedScript) => {
|
||||
@@ -191,11 +213,29 @@ describe("CI gate contract", () => {
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
||||
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
||||
await expect(loadCiGateContract(root)).rejects.toThrow(
|
||||
/missing package scripts|package script graph invalid/i,
|
||||
/missing package scripts|package script graph invalid|canonical check:ci dependency/i,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["check:artifact-schemas", "check:ci-workflow"])(
|
||||
"rejects a no-op nested check:ci dependency: %s",
|
||||
async (bypassedScript) => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-script-meaning-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
const contract = await loadCiGateContract(process.cwd());
|
||||
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
packageDocument.scripts[bypassedScript] = "true";
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
||||
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
||||
|
||||
await expect(loadCiGateContract(root)).rejects.toThrow(/canonical check:ci dependency/i);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["true bypass", "true"],
|
||||
["direct self recursion", "corepack pnpm check:ci"],
|
||||
@@ -292,6 +332,79 @@ describe("CI gate contract", () => {
|
||||
expect(log.byteLength).toBeLessThanOrEqual(8_192);
|
||||
expect(log.toString("utf8")).toMatch(/aggregate output|INFRASTRUCTURE_FAILURE/i);
|
||||
}, 20_000);
|
||||
|
||||
it("rejects stale command-generated evidence from a successful no-op producer", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-stale-evidence-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
await mkdir(path.join(root, "artifacts/tests"), { recursive: true });
|
||||
const contract = JSON.parse(
|
||||
JSON.stringify(await loadCiGateContract(process.cwd())),
|
||||
) as Record<string, any>;
|
||||
const command = contract.commands.find(
|
||||
(entry: Record<string, any>) => entry.id === "test-runtime-schema",
|
||||
);
|
||||
command.script = "test:stale-evidence-noop";
|
||||
const evidence = contract.artifacts.find(
|
||||
(entry: Record<string, any>) => entry.path === "artifacts/tests/runtime-schema.xml",
|
||||
);
|
||||
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
packageDocument.scripts[command.script] = "true";
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
||||
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
||||
await writeFile(
|
||||
path.join(root, evidence.path),
|
||||
'<testsuite name="stale" tests="0" failures="0"/>\n',
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-004"],
|
||||
{ cwd: root, encoding: "utf8", env: { ...process.env, CI: "false" } },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(
|
||||
await readFile(path.join(root, "artifacts/quality/gates/FE-GATE-004.txt"), "utf8"),
|
||||
).toMatch(/not freshly produced/i);
|
||||
});
|
||||
|
||||
it("accepts a fresh deterministic rewrite with identical evidence bytes", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-identical-rewrite-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
await mkdir(path.join(root, "artifacts/tests"), { recursive: true });
|
||||
const contract = JSON.parse(
|
||||
JSON.stringify(await loadCiGateContract(process.cwd())),
|
||||
) as Record<string, any>;
|
||||
const command = contract.commands.find(
|
||||
(entry: Record<string, any>) => entry.id === "test-runtime-schema",
|
||||
);
|
||||
command.script = "test:identical-evidence-rewrite";
|
||||
const evidence = contract.artifacts.find(
|
||||
(entry: Record<string, any>) => entry.path === "artifacts/tests/runtime-schema.xml",
|
||||
);
|
||||
const evidenceBytes = '<testsuite name="deterministic" tests="0" failures="0"/>\n';
|
||||
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
|
||||
scripts: Record<string, string>;
|
||||
};
|
||||
packageDocument.scripts[command.script] =
|
||||
`node -e 'require("node:fs").writeFileSync("${evidence.path}", Buffer.from("${Buffer.from(evidenceBytes).toString("base64")}", "base64"))'`;
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
||||
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
||||
await writeFile(path.join(root, evidence.path), evidenceBytes);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-004"],
|
||||
{ cwd: root, encoding: "utf8", env: { ...process.env, CI: "false" } },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toMatch(/FE-GATE-004 runtime-schema: PASS/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CI workflow generation", () => {
|
||||
@@ -311,6 +424,10 @@ describe("CI workflow generation", () => {
|
||||
);
|
||||
expect(first).not.toContain("process_dist_sha256");
|
||||
expect(first).toContain("persist-credentials: false");
|
||||
expect(
|
||||
first.match(/corepack pnpm install --frozen-lockfile --ignore-scripts/gu),
|
||||
).toHaveLength(9);
|
||||
expect(first).not.toMatch(/corepack pnpm install --frozen-lockfile$/mu);
|
||||
expect(first).toContain("verify-ci-candidate-archive.ts --archive");
|
||||
expect(first).toContain("--extract-to");
|
||||
expect(first).not.toMatch(/\btar\s+[^\n]*--extract/u);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
|
||||
import { loadCiGateContract } from "../../scripts/contracts/ci-gates.ts";
|
||||
import {
|
||||
pruneRemovalFixtureCiContract,
|
||||
} from "../../scripts/lib/removal-fixture.ts";
|
||||
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
it("prunes removed command and evidence references from a reduced CI contract", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-contract-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), await readFile("config/ci/gates.json"));
|
||||
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
|
||||
|
||||
await pruneRemovalFixtureCiContract({
|
||||
root,
|
||||
removedScripts: new Set(["test:reference-feature"]),
|
||||
removedEvidencePathFragments: ["reference-feature.xml"],
|
||||
});
|
||||
|
||||
const contract = await loadCiGateContract(root, { mode: "removal-fixture" });
|
||||
expect(contract.commands.some(({ script }) => script === "test:reference-feature")).toBe(false);
|
||||
expect(contract.artifacts.some(({ path }) => path.includes("reference-feature.xml"))).toBe(false);
|
||||
expect(contract.gates.some(({ commandIds }) => commandIds.includes("test-reference-feature"))).toBe(false);
|
||||
expect(contract.gates.some(({ evidenceArtifactIds }) =>
|
||||
evidenceArtifactIds.includes("artifact-artifacts-tests-reference-feature-xml")
|
||||
)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects pruning that leaves a reduced gate without commands", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "removal-fixture-empty-gate-"));
|
||||
temporaryRoots.push(root);
|
||||
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
||||
await writeFile(path.join(root, "config/ci/gates.json"), await readFile("config/ci/gates.json"));
|
||||
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
|
||||
|
||||
await expect(pruneRemovalFixtureCiContract({
|
||||
root,
|
||||
removedScripts: new Set(["test:runtime-schema"]),
|
||||
removedEvidencePathFragments: ["runtime-schema.xml"],
|
||||
})).rejects.toThrow(/commandIds|too small|at least 1/i);
|
||||
});
|
||||
@@ -704,7 +704,11 @@ describe("supply-chain policy", () => {
|
||||
});
|
||||
|
||||
it("wires the exact security fixture checker as a passing CI gate", async () => {
|
||||
const contract = await loadCiGateContract(process.cwd());
|
||||
const contract = await loadCiGateContract(process.cwd(), {
|
||||
mode: process.env.CI_CONTRACT_MODE === "removal-fixture"
|
||||
? "removal-fixture"
|
||||
: "canonical",
|
||||
});
|
||||
const index = indexCiGateContract(contract);
|
||||
const securityGate = index.gates.get("FE-GATE-013");
|
||||
expect(securityGate).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user