fix: harden CI evidence and removal contracts

This commit is contained in:
DongHyeonka
2026-08-02 14:48:04 +09:00
parent 1bb2cc4a20
commit f49d147b01
20 changed files with 1175 additions and 767 deletions
+9 -9
View File
@@ -63,7 +63,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Install Playwright browsers - name: Install Playwright browsers
if: ${{ matrix.browser }} if: ${{ matrix.browser }}
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
@@ -103,7 +103,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Install Playwright browsers - name: Install Playwright browsers
if: ${{ matrix.browser }} if: ${{ matrix.browser }}
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
@@ -136,7 +136,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Build candidate once and verify local evidence - name: Build candidate once and verify local evidence
run: corepack pnpm ci:gate -- FE-GATE-015 run: corepack pnpm ci:gate -- FE-GATE-015
- name: Archive and validate the exact candidate file set - name: Archive and validate the exact candidate file set
@@ -192,7 +192,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Download release candidate - name: Download release candidate
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
with: with:
@@ -234,7 +234,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Download release candidate - name: Download release candidate
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
with: with:
@@ -278,7 +278,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Download release candidate - name: Download release candidate
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
with: with:
@@ -336,7 +336,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Run blocking gate - name: Run blocking gate
run: corepack pnpm ci:gate -- ${{ matrix.gate }} run: corepack pnpm ci:gate -- ${{ matrix.gate }}
- name: Upload production gate evidence - name: Upload production gate evidence
@@ -366,7 +366,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Run blocking gate - name: Run blocking gate
run: corepack pnpm ci:gate -- FE-GATE-018 run: corepack pnpm ci:gate -- FE-GATE-018
- name: Upload field gate evidence - name: Upload field gate evidence
@@ -392,7 +392,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Run documentation gate - name: Run documentation gate
run: corepack pnpm ci:gate -- FE-GATE-017 run: corepack pnpm ci:gate -- FE-GATE-017
- name: Upload documentation gate evidence - name: Upload documentation gate evidence
+448 -105
View File
@@ -648,527 +648,870 @@
{ {
"id": "artifact-artifacts-quality-install-txt", "id": "artifact-artifacts-quality-install-txt",
"path": "artifacts/quality/install.txt", "path": "artifacts/quality/install.txt",
"schemaId": "text" "schemaId": "text",
"production": "runner-generated"
}, },
{ {
"id": "artifact-artifacts-quality-lint-txt", "id": "artifact-artifacts-quality-lint-txt",
"path": "artifacts/quality/lint.txt", "path": "artifacts/quality/lint.txt",
"schemaId": "text" "schemaId": "text",
"production": "runner-generated"
}, },
{ {
"id": "artifact-artifacts-quality-check-types-txt", "id": "artifact-artifacts-quality-check-types-txt",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-004-txt",
"path": "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", "id": "artifact-artifacts-tests-runtime-schema-xml",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-005-txt",
"path": "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", "id": "artifact-artifacts-tests-unit-xml",
"path": "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", "id": "artifact-artifacts-tests-coverage-xml",
"path": "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", "id": "artifact-artifacts-tests-coverage-coverage-summary-json",
"path": "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", "id": "artifact-artifacts-quality-risk-coverage-json",
"path": "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", "id": "artifact-artifacts-quality-risk-coverage-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-006-txt",
"path": "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", "id": "artifact-artifacts-tests-component-xml",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-007-txt",
"path": "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", "id": "artifact-artifacts-tests-integration-xml",
"path": "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", "id": "artifact-artifacts-tests-http-scenario-executions-json",
"path": "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", "id": "artifact-artifacts-quality-http-scenario-evidence-json",
"path": "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", "id": "artifact-artifacts-quality-http-scenario-evidence-fixture-json",
"path": "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", "id": "artifact-artifacts-tests-reference-feature-xml",
"path": "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", "id": "artifact-artifacts-tests-optional-recipes-xml",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-008-txt",
"path": "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", "id": "artifact-artifacts-tests-e2e-report-index-html",
"path": "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", "id": "artifact-artifacts-tests-e2e-results-xml",
"path": "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", "id": "artifact-artifacts-tests-browser-capabilities-report-index-html",
"path": "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", "id": "artifact-artifacts-tests-browser-capabilities-results-xml",
"path": "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", "id": "artifact-artifacts-tests-storybook-report-index-html",
"path": "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", "id": "artifact-artifacts-tests-storybook-results-xml",
"path": "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", "id": "artifact-artifacts-tests-visual-report-index-html",
"path": "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", "id": "artifact-artifacts-tests-visual-results-xml",
"path": "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", "id": "artifact-artifacts-quality-test-evidence-json",
"path": "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", "id": "artifact-artifacts-quality-test-evidence-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-009-txt",
"path": "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", "id": "artifact-artifacts-tests-a11y-json",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-APP-HOME-md",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-UI-md",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-STATES-md",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-EXAMPLES-AUTH-md",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-REFERENCE-RESOURCE-LIST-md",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md",
"path": "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", "id": "artifact-artifacts-tests-a11y-manual-report-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-010-txt",
"path": "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", "id": "artifact-artifacts-quality-dependency-report-json",
"path": "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", "id": "artifact-artifacts-quality-design-system-json",
"path": "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", "id": "artifact-artifacts-quality-design-system-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-i18n-json",
"path": "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", "id": "artifact-artifacts-quality-i18n-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-diagnostics-json",
"path": "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", "id": "artifact-artifacts-quality-diagnostics-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-realtime-boundaries-json",
"path": "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", "id": "artifact-artifacts-quality-optional-recipes-json",
"path": "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", "id": "artifact-artifacts-quality-optional-recipe-fixtures-json",
"path": "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", "id": "artifact-artifacts-quality-registries-json",
"path": "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", "id": "artifact-artifacts-quality-registry-compatibility-fixtures-json",
"path": "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", "id": "artifact-artifacts-quality-registry-baseline-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-registry-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-route-registry-fixture-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-011-txt",
"path": "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", "id": "artifact-artifacts-release-build-manifest-json",
"path": "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", "id": "artifact-artifacts-release-runtime-config-schema-json",
"path": "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", "id": "artifact-artifacts-storybook-static-index-html",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-012-txt",
"path": "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", "id": "artifact-artifacts-performance-bundle-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-013-txt",
"path": "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", "id": "artifact-artifacts-security-scan-sarif",
"path": "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", "id": "artifact-artifacts-release-dependency-inventory-json",
"path": "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", "id": "artifact-artifacts-release-sbom-cdx-json",
"path": "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", "id": "artifact-artifacts-release-provenance-json",
"path": "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", "id": "artifact-artifacts-release-reproducible-build-json",
"path": "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", "id": "artifact-artifacts-security-dependency-diff-json",
"path": "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", "id": "artifact-artifacts-security-license-report-json",
"path": "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", "id": "artifact-artifacts-security-vulnerability-report-json",
"path": "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", "id": "artifact-artifacts-security-supply-chain-verification-json",
"path": "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", "id": "artifact-artifacts-security-supply-chain-coherence-json",
"path": "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", "id": "artifact-artifacts-security-supply-chain-fixtures-json",
"path": "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", "id": "artifact-artifacts-security-supply-chain-provider-fixtures-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-014-txt",
"path": "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", "id": "artifact-artifacts-release-compatibility-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-015-txt",
"path": "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", "id": "artifact-artifacts-release-release-candidate-json",
"path": "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", "id": "artifact-artifacts-release-verification-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-016-txt",
"path": "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", "id": "artifact-artifacts-runbooks-FE-RB-005-record-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-017-txt",
"path": "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", "id": "artifact-artifacts-quality-documentation-review-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-018-txt",
"path": "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", "id": "artifact-artifacts-performance-field-web-vitals-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-019-txt",
"path": "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", "id": "artifact-artifacts-release-hosting-headers-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-020-txt",
"path": "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", "id": "artifact-artifacts-tests-sample-removal-xml",
"path": "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", "id": "artifact-artifacts-tests-optional-recipe-removal-xml",
"path": "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", "id": "artifact-artifacts-tests-browser-file-storage-runtime-removal-xml",
"path": "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", "id": "artifact-artifacts-tests-realtime-runtime-removal-xml",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-021-txt",
"path": "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", "id": "artifact-artifacts-runbooks-FE-RB-001-record-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-022-txt",
"path": "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", "id": "artifact-artifacts-runbooks-FE-RB-002-record-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-023-txt",
"path": "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", "id": "artifact-artifacts-runbooks-FE-RB-003-record-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-024-txt",
"path": "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", "id": "artifact-artifacts-runbooks-FE-RB-004-record-json",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-025-txt",
"path": "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", "id": "artifact-artifacts-quality-gates-FE-GATE-026-txt",
"path": "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", "id": "artifact-artifacts-performance-lab-json",
"path": "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", "id": "artifact-artifacts-quality-ci-contract-json",
"path": "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": [ "gates": [
+2 -2
View File
@@ -56,8 +56,8 @@ Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이 Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. 이
workflow의 provider job은 Linux runner에서 실행 권한이 있는 workflow의 provider job은 Linux runner에서 실행 권한이 있는
`/usr/bin/bwrap`를 필수로 요구하며, 사용할 Node 실행 파일도 sandbox 안의 `/usr/bin/bwrap`를 필수로 요구하며, trusted `process.execPath` sandbox 안의
`/usr/local/bin/node`로 고정한다. Provider command는 bubblewrap 안에서 `/tmp/node`에 read-only bind한다. Provider command는 bubblewrap 안에서
`/bin/sh -eu -c`로 비대화식 실행되고 30분 안에 종료되어야 한다. Sandbox는 `/bin/sh -eu -c`로 비대화식 실행되고 30분 안에 종료되어야 한다. Sandbox는
workspace를 read-only로 bind하고 `.git`을 가리며, 별도의 `untrusted` workspace를 read-only로 bind하고 `.git`을 가리며, 별도의 `untrusted`
raw-evidence 하위 디렉터리만 writable로 노출한다. 따라서 command는 전달된 raw-evidence 하위 디렉터리만 writable로 노출한다. 따라서 command는 전달된
+2 -2
View File
@@ -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 External provider supervision is fail-closed and requires a Linux runner with
an executable `/usr/bin/bwrap`. Bubblewrap mounts the repository workspace an executable `/usr/bin/bwrap`. Bubblewrap mounts the repository workspace
read-only, hides `.git`, pins the trusted Node executable at read-only, hides `.git`, and read-only binds the trusted `process.execPath`
`/usr/local/bin/node`, and exposes only the sibling `untrusted` raw-evidence inside the sandbox at `/tmp/node`. It exposes only the sibling `untrusted` raw-evidence
directory as writable. Provider commands run non-interactively through directory as writable. Provider commands run non-interactively through
`/bin/sh -eu -c`, receive a minimized environment plus only their own `/bin/sh -eu -c`, receive a minimized environment plus only their own
provider-prefixed credentials, and have a 30-minute limit. They must consume provider-prefixed credentials, and have a 30-minute limit. They must consume
+4 -1
View File
@@ -17,7 +17,10 @@ import {
import { validatePackageScriptGraph } from "./lib/package-script-graph.ts"; import { validatePackageScriptGraph } from "./lib/package-script-graph.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.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 index = indexCiGateContract(contract);
const [packageDocument, nodeVersion] = await Promise.all([ const [packageDocument, nodeVersion] = await Promise.all([
readFile("package.json", "utf8").then((value) => JSON.parse(value) as { scripts?: Record<string, string> }), readFile("package.json", "utf8").then((value) => JSON.parse(value) as { scripts?: Record<string, string> }),
+92 -5
View File
@@ -1,4 +1,5 @@
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import path from "node:path"; import path from "node:path";
import { z } from "zod"; import { z } from "zod";
@@ -246,9 +247,18 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
.strict(), .strict(),
]); ]);
const artifactSchema = z const artifactBaseShape = { id, path: repositoryPath, schemaId: id } as const;
.object({ id, path: repositoryPath, schemaId: id }) const artifactSchema = z.discriminatedUnion("production", [
.strict(); 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 const gateSchema = z
.object({ .object({
@@ -405,6 +415,35 @@ export type CiGateContractIndex = Readonly<{
jobs: ReadonlyMap<string, CiWorkflowJob>; jobs: ReadonlyMap<string, CiWorkflowJob>;
retentionClasses: ReadonlyMap<string, CiGateContract["retention"]["classes"][number]>; 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 { export function parseCiGateContract(value: unknown): CiGateContract {
const result = ciGateContractSchema.safeParse(value); const result = ciGateContractSchema.safeParse(value);
@@ -417,12 +456,22 @@ export function parseCiGateContract(value: unknown): CiGateContract {
return result.data; 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([ const [rawContract, rawPackage] = await Promise.all([
readFile(path.join(root, "config/ci/gates.json"), "utf8"), readFile(path.join(root, "config/ci/gates.json"), "utf8"),
readFile(path.join(root, "package.json"), "utf8"), readFile(path.join(root, "package.json"), "utf8"),
]); ]);
const contract = parseCiGateContract(JSON.parse(rawContract)); 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 const packageDocument = z
.object({ scripts: z.record(z.string(), z.string()).default({}) }) .object({ scripts: z.record(z.string(), z.string()).default({}) })
.passthrough() .passthrough()
@@ -434,10 +483,23 @@ export async function loadCiGateContract(root = process.cwd()): Promise<CiGateCo
if (missing.length > 0) { if (missing.length > 0) {
throw new TypeError(`CI gate contract missing package scripts: ${missing.join(", ")}`); 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) { if (packageDocument.scripts["check:ci"] !== expectedCheckCi) {
throw new TypeError("check:ci must use the exact canonical non-recursive orchestration"); 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"); const graphFailures = validatePackageScriptGraph(packageDocument.scripts, "check:ci");
if (graphFailures.length > 0) { if (graphFailures.length > 0) {
throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`); throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`);
@@ -510,6 +572,16 @@ function validateContractSemantics(
if (!schemaIds.has(artifact.schemaId)) { if (!schemaIds.has(artifact.schemaId)) {
issue(`unknown artifact schema ${artifact.schemaId} for ${artifact.id}`); 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) { for (const gate of contract.gates) {
if (new Set(gate.commandIds).size !== gate.commandIds.length) { if (new Set(gate.commandIds).size !== gate.commandIds.length) {
@@ -524,6 +596,21 @@ function validateContractSemantics(
for (const artifactId of [gate.logArtifactId, ...gate.evidenceArtifactIds]) { for (const artifactId of [gate.logArtifactId, ...gate.evidenceArtifactIds]) {
if (!artifactIds.has(artifactId)) issue(`unknown artifact ${artifactId} for ${gate.id}`); 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)) { if (!retentionIds.has(gate.retentionClassId)) {
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`); issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
} }
+9 -3
View File
@@ -28,6 +28,7 @@ export type GenerateCiWorkflowOptions = Readonly<{
root: string; root: string;
contract?: CiGateContract; contract?: CiGateContract;
check: boolean; check: boolean;
contractMode?: "canonical" | "removal-fixture";
}>; }>;
export type GenerateCiWorkflowResult = Readonly<{ export type GenerateCiWorkflowResult = Readonly<{
@@ -176,7 +177,7 @@ function renderStep(
" - name: Frozen install", " - name: Frozen install",
" run: |", " run: |",
" corepack enable", " corepack enable",
" corepack pnpm install --frozen-lockfile", " corepack pnpm install --frozen-lockfile --ignore-scripts",
]; ];
case "browser-install": case "browser-install":
return [ return [
@@ -335,7 +336,9 @@ export function createCiWorkflowGenerator(
const createNonce = dependencies.createNonce ?? randomUUID; const createNonce = dependencies.createNonce ?? randomUUID;
return async function generate(options: GenerateCiWorkflowOptions): Promise<GenerateCiWorkflowResult> { return async function generate(options: GenerateCiWorkflowOptions): Promise<GenerateCiWorkflowResult> {
const root = path.resolve(options.root); 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); const target = path.resolve(root, contract.providerAdapter);
if (path.relative(root, target).startsWith("..") || path.relative(root, target) === "") { if (path.relative(root, target).startsWith("..") || path.relative(root, target) === "") {
throw new TypeError(`workflow target escapes repository root: ${contract.providerAdapter}`); 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); const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isCli) { if (isCli) {
const check = process.argv.includes("--check"); const check = process.argv.includes("--check");
const contractMode = process.argv.includes("--reduced-removal-fixture")
? "removal-fixture" as const
: "canonical" as const;
try { try {
const result = await generateCiWorkflow({ root: process.cwd(), check }); const result = await generateCiWorkflow({ root: process.cwd(), check, contractMode });
if (!result.matches) { if (!result.matches) {
process.stderr.write( process.stderr.write(
`CI workflow drift: ${result.target} differs at byte ${result.firstDifferenceByte ?? 0}, line ${result.firstDifferenceLine ?? 1}\n`, `CI workflow drift: ${result.target} differs at byte ${result.firstDifferenceByte ?? 0}, line ${result.firstDifferenceLine ?? 1}\n`,
+233
View File
@@ -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`),
]);
}
+50
View File
@@ -1,4 +1,6 @@
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import { lstat } from "node:fs/promises";
import path from "node:path";
import { import {
ciCheckoutIdentityFailures, ciCheckoutIdentityFailures,
@@ -37,6 +39,28 @@ let passed = true;
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60 * 1_000; const DEFAULT_STEP_TIMEOUT_MS = 30 * 60 * 1_000;
const MAX_STEP_OUTPUT_BYTES = 16 * 1024 * 1_024; const MAX_STEP_OUTPUT_BYTES = 16 * 1024 * 1_024;
const LOG_DIAGNOSTIC_RESERVE_BYTES = 4_096; 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 => { const appendOutput = (...values: readonly string[]): boolean => {
for (const value of values.filter(Boolean)) { for (const value of values.filter(Boolean)) {
const addedBytes = Buffer.byteLength(value, "utf8") + 1; const addedBytes = Buffer.byteLength(value, "utf8") + 1;
@@ -97,6 +121,17 @@ if (passed) {
for (const commandId of gate.commandIds) { for (const commandId of gate.commandIds) {
const step = contractIndex.commands.get(commandId); const step = contractIndex.commands.get(commandId);
if (!step) throw new TypeError(`CI gate command disappeared after validation: ${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(); const commandLine = `$ corepack pnpm ${step.script} ${(step.args ?? []).join(" ")}`.trim();
if (!appendOutput(commandLine) || logSchema.maxBytes - outputBytes <= LOG_DIAGNOSTIC_RESERVE_BYTES) { if (!appendOutput(commandLine) || logSchema.maxBytes - outputBytes <= LOG_DIAGNOSTIC_RESERVE_BYTES) {
appendOutput("gate aggregate output budget exhausted before command execution"); appendOutput("gate aggregate output budget exhausted before command execution");
@@ -159,6 +194,21 @@ if (passed) {
passed = false; passed = false;
break; 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 { import {
cp,
mkdir, mkdir,
readFile, readFile,
readdir,
rm, rm,
symlink,
writeFile, writeFile,
} from "node:fs/promises"; } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { parseCiGateContract } from "./contracts/ci-gates.ts"; import {
import { generateCiWorkflow } from "./generate-ci-workflow.ts"; assertNoRuntimeImports,
prepareRemovalFixture,
pruneRemovalFixtureCiContract,
regenerateRemovalFixtureWorkflow,
removeRuntimeDependentTests,
requireRemovalFixtureEnvironment,
runRemovalFixturePnpm,
} from "./lib/removal-fixture.ts";
const fixtureRoot = path.resolve( const fixtureRoot = path.resolve(
".tmp/browser-file-storage-runtime-removal", ".tmp/browser-file-storage-runtime-removal",
); );
const pnpmCli = requireEnvironment("npm_execpath"); const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const runtimePaths = [ const runtimePaths = [
"src/application/ports/browser-file-storage", "src/application/ports/browser-file-storage",
"src/application/ports/browser-transfer", "src/application/ports/browser-transfer",
@@ -42,216 +45,14 @@ const removedEvidencePathFragments = [
"browser-capabilities", "browser-capabilities",
"browser-file-storage-runtime-removal", "browser-file-storage-runtime-removal",
] as const; ] 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 { function runPnpm(script: string): boolean {
return ( return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
spawnSync(process.execPath, [pnpmCli, script], {
cwd: fixtureRoot,
stdio: "inherit",
}).status === 0
);
} }
async function sourceFiles(directory: string): Promise<string[]> { await prepareRemovalFixture(fixtureRoot);
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",
);
const removedRuntimeTests = const removedRuntimeTests =
await removeRuntimeDependentTests(fixtureRoot); await removeRuntimeDependentTests(fixtureRoot, runtimeSourceRoots);
for (const runtimePath of runtimePaths) { for (const runtimePath of runtimePaths) {
await rm(path.join(fixtureRoot, runtimePath), { await rm(path.join(fixtureRoot, runtimePath), {
recursive: true, recursive: true,
@@ -290,17 +91,6 @@ if (removedRuntimeEntries !== 3) {
} }
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); 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"), { await rm(path.join(fixtureRoot, "playwright.capabilities.config.ts"), {
force: true, force: true,
}); });
@@ -330,67 +120,17 @@ await rm(
{ force: true }, { force: true },
); );
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json"); await pruneRemovalFixtureCiContract({
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({
root: fixtureRoot, root: fixtureRoot,
contract: validatedGates, removedScripts,
check: false, removedEvidencePathFragments,
}); });
await assertNoRuntimeImports(fixtureRoot); await regenerateRemovalFixtureWorkflow(fixtureRoot);
await assertNoRuntimeImports(
fixtureRoot,
runtimeSourceRoots,
"browser file/storage",
);
const checks: Array<readonly [string, boolean]> = [ const checks: Array<readonly [string, boolean]> = [
["typecheck", runPnpm("check:types")], ["typecheck", runPnpm("check:types")],
+45 -35
View File
@@ -1,17 +1,23 @@
import { spawnSync } from "node:child_process";
import { import {
cp,
mkdir, mkdir,
readFile, readFile,
readdir,
rm, rm,
symlink,
writeFile, writeFile,
} from "node:fs/promises"; } from "node:fs/promises";
import path from "node:path"; 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 fixtureRoot = path.resolve(".tmp/optional-recipe-removal");
const pnpmCli = requireEnvironment("npm_execpath"); const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const copyTargets = [ const copyTargets = [
"src", "src",
"tests", "tests",
@@ -44,51 +50,55 @@ const copyTargets = [
".nvmrc", ".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 { function runPnpm(script: string): boolean {
return ( return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
spawnSync(process.execPath, [pnpmCli, script], {
cwd: fixtureRoot,
stdio: "inherit",
}).status === 0
);
} }
async function filesBelow(directory: string): Promise<string[]> { await prepareRemovalFixture(fixtureRoot, copyTargets);
const entries = await readdir(directory, { withFileTypes: true }); for (const rootOnlyTest of [
const groups = await Promise.all( "tests/unit/ci-workflow-generation.test.ts",
entries.map((entry) => { "tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
const target = path.join(directory, entry.name); ]) {
return entry.isDirectory() ? filesBelow(target) : [target]; await rm(path.join(fixtureRoot, rootOnlyTest), { force: true });
}),
);
return groups.flat();
} }
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, "recipes"), { recursive: true, force: true });
await rm(path.join(fixtureRoot, "tests/recipes"), { await rm(path.join(fixtureRoot, "tests/recipes"), {
recursive: true, recursive: true,
force: 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]> = [ const checks: Array<[string, boolean]> = [
["typecheck", runPnpm("check:types")], ["typecheck", runPnpm("check:types")],
["architecture", runPnpm("check:architecture")], ["architecture", runPnpm("check:architecture")],
["test", runPnpm("test:all")], ["test", runPnpm("test:all")],
["build", runPnpm("build")], ["build", runPnpm("build")],
["ci-contract", runPnpm("check:ci")],
]; ];
const residue: string[] = []; const residue: string[] = [];
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) { for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
+18 -279
View File
@@ -1,20 +1,23 @@
import { spawnSync } from "node:child_process";
import { import {
cp,
mkdir, mkdir,
readFile, readFile,
readdir,
rm, rm,
symlink,
writeFile, writeFile,
} from "node:fs/promises"; } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { parseCiGateContract } from "./contracts/ci-gates.ts"; import {
import { generateCiWorkflow } from "./generate-ci-workflow.ts"; assertNoRuntimeImports,
prepareRemovalFixture,
pruneRemovalFixtureCiContract,
regenerateRemovalFixtureWorkflow,
removeRuntimeDependentTests,
requireRemovalFixtureEnvironment,
runRemovalFixturePnpm,
} from "./lib/removal-fixture.ts";
const fixtureRoot = path.resolve(".tmp/realtime-runtime-removal"); const fixtureRoot = path.resolve(".tmp/realtime-runtime-removal");
const pnpmCli = requireEnvironment("npm_execpath"); const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const runtimePaths = [ const runtimePaths = [
"src/application/ports/realtime", "src/application/ports/realtime",
"src/application/ports/out/web-push-control.ts", "src/application/ports/out/web-push-control.ts",
@@ -38,212 +41,14 @@ const removedEvidencePathFragments = [
"realtime-boundaries", "realtime-boundaries",
"realtime-runtime-removal", "realtime-runtime-removal",
] as const; ] 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 { function runPnpm(script: string): boolean {
return ( return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script);
spawnSync(process.execPath, [pnpmCli, script], {
cwd: fixtureRoot,
stdio: "inherit",
}).status === 0
);
} }
async function sourceFiles(directory: string): Promise<string[]> { await prepareRemovalFixture(fixtureRoot);
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",
);
const removedRuntimeTests = const removedRuntimeTests =
await removeRuntimeDependentTests(fixtureRoot); await removeRuntimeDependentTests(fixtureRoot, runtimeSourceRoots);
for (const runtimePath of runtimePaths) { for (const runtimePath of runtimePaths) {
await rm(path.join(fixtureRoot, runtimePath), { await rm(path.join(fixtureRoot, runtimePath), {
recursive: true, recursive: true,
@@ -280,17 +85,6 @@ if (!realtimeRecipe || !Object.hasOwn(realtimeRecipe, "referenceRuntime")) {
delete realtimeRecipe.referenceRuntime; delete realtimeRecipe.referenceRuntime;
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`); 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 [ for (const scriptPath of [
"scripts/check-realtime-boundaries.ts", "scripts/check-realtime-boundaries.ts",
"scripts/check-realtime-boundary-fixtures.ts", "scripts/check-realtime-boundary-fixtures.ts",
@@ -313,68 +107,13 @@ await rm(
{ force: true }, { force: true },
); );
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json"); await pruneRemovalFixtureCiContract({
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({
root: fixtureRoot, root: fixtureRoot,
contract: validatedGates, removedScripts: new Set<string>(runtimeScripts),
check: false, removedEvidencePathFragments,
}); });
await assertNoRuntimeImports(fixtureRoot); await regenerateRemovalFixtureWorkflow(fixtureRoot);
await assertNoRuntimeImports(fixtureRoot, runtimeSourceRoots, "realtime");
const checks: Array<readonly [string, boolean]> = [ const checks: Array<readonly [string, boolean]> = [
["typecheck", runPnpm("check:types")], ["typecheck", runPnpm("check:types")],
+47 -30
View File
@@ -1,25 +1,30 @@
import { spawnSync } from "node:child_process";
import { import {
access, access,
cp,
mkdir, mkdir,
mkdtemp, mkdtemp,
readFile, readFile,
readdir,
rm, rm,
symlink,
writeFile, writeFile,
} from "node:fs/promises"; } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { isProductionModulePath } from "./lib/risk-coverage.ts"; 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"); const fixtureParent = path.resolve(".tmp");
await mkdir(fixtureParent, { recursive: true }); await mkdir(fixtureParent, { recursive: true });
const fixtureRoot = await mkdtemp( const fixtureRoot = await mkdtemp(
path.join(fixtureParent, "reference-feature-removal-"), path.join(fixtureParent, "reference-feature-removal-"),
); );
const pnpmCli = requireEnvironment("npm_execpath"); const pnpmCli = requireRemovalFixtureEnvironment("npm_execpath");
const featureSource = "src/features/reference-feature"; const featureSource = "src/features/reference-feature";
const featureTests = "tests/features/reference-feature"; const featureTests = "tests/features/reference-feature";
const commonTestPaths = [ const commonTestPaths = [
@@ -162,36 +167,20 @@ type GovernanceRegistry = Record<string, unknown> & {
}; };
type RemovalGovernance = { registries: GovernanceRegistry[] }; 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 { function runPnpm(script: string, extra: string[] = []): boolean {
const result = spawnSync(process.execPath, [pnpmCli, script, ...extra], { return runRemovalFixturePnpm(fixtureRoot, pnpmCli, script, extra);
cwd: fixtureRoot,
stdio: "inherit",
});
return result.status === 0;
} }
try { try {
for (const target of copyTargets) { await prepareRemovalFixture(fixtureRoot, copyTargets);
await cp(target, path.join(fixtureRoot, target), { recursive: true }); 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( const coveragePolicyFile = path.join(
fixtureRoot, fixtureRoot,
@@ -329,6 +318,33 @@ try {
`${JSON.stringify(removalGovernance, null, 2)}\n`, `${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[] = []; const residue: string[] = [];
for (const root of ["src", "tests"]) { for (const root of ["src", "tests"]) {
for (const file of await filesBelow(path.join(fixtureRoot, root))) { for (const file of await filesBelow(path.join(fixtureRoot, root))) {
@@ -366,6 +382,7 @@ try {
["unit-integration", runPnpm("test:all")], ["unit-integration", runPnpm("test:all")],
["coverage", runPnpm("test:coverage")], ["coverage", runPnpm("test:coverage")],
["test-evidence-source", runPnpm("check:test-evidence:source")], ["test-evidence-source", runPnpm("check:test-evidence:source")],
["ci-contract", runPnpm("check:ci")],
[ [
"home-smoke", "home-smoke",
runPnpm("exec", [ runPnpm("exec", [
+1 -1
View File
@@ -3,7 +3,7 @@
"providerAdapter": ".gitea/workflows/quality-gates.yml", "providerAdapter": ".gitea/workflows/quality-gates.yml",
"commands": [], "commands": [],
"artifactSchemas": [{ "id": "text", "kind": "text", "maxBytes": 1024 }], "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": [ "gates": [
{ "id": "FE-GATE-001", "name": "one", "commandIds": ["command"], "logArtifactId": "log", "evidenceArtifactIds": ["log"], "retentionClassId": "merge" }, { "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" } { "id": "FE-GATE-001", "name": "duplicate", "commandIds": ["command"], "logArtifactId": "log", "evidenceArtifactIds": ["log"], "retentionClassId": "merge" }
+1 -1
View File
@@ -3,7 +3,7 @@
"providerAdapter": ".gitea/workflows/quality-gates.yml", "providerAdapter": ".gitea/workflows/quality-gates.yml",
"commands": [], "commands": [],
"artifactSchemas": [], "artifactSchemas": [],
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "missing" }], "artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "missing", "production": "runner-generated" }],
"gates": [], "gates": [],
"stages": [], "stages": [],
"jobs": [], "jobs": [],
@@ -66,7 +66,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Install Playwright browsers - name: Install Playwright browsers
if: \${{ matrix.browser }} if: \${{ matrix.browser }}
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
@@ -106,7 +106,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Install Playwright browsers - name: Install Playwright browsers
if: \${{ matrix.browser }} if: \${{ matrix.browser }}
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
@@ -139,7 +139,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Build candidate once and verify local evidence - name: Build candidate once and verify local evidence
run: corepack pnpm ci:gate -- FE-GATE-015 run: corepack pnpm ci:gate -- FE-GATE-015
- name: Archive and validate the exact candidate file set - name: Archive and validate the exact candidate file set
@@ -195,7 +195,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Download release candidate - name: Download release candidate
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
with: with:
@@ -237,7 +237,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Download release candidate - name: Download release candidate
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
with: with:
@@ -281,7 +281,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Download release candidate - name: Download release candidate
uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7 uses: https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7
with: with:
@@ -339,7 +339,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Run blocking gate - name: Run blocking gate
run: corepack pnpm ci:gate -- \${{ matrix.gate }} run: corepack pnpm ci:gate -- \${{ matrix.gate }}
- name: Upload production gate evidence - name: Upload production gate evidence
@@ -369,7 +369,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Run blocking gate - name: Run blocking gate
run: corepack pnpm ci:gate -- FE-GATE-018 run: corepack pnpm ci:gate -- FE-GATE-018
- name: Upload field gate evidence - name: Upload field gate evidence
@@ -395,7 +395,7 @@ jobs:
- name: Frozen install - name: Frozen install
run: | run: |
corepack enable corepack enable
corepack pnpm install --frozen-lockfile corepack pnpm install --frozen-lockfile --ignore-scripts
- name: Run documentation gate - name: Run documentation gate
run: corepack pnpm ci:gate -- FE-GATE-017 run: corepack pnpm ci:gate -- FE-GATE-017
- name: Upload documentation gate evidence - name: Upload documentation gate evidence
+6 -1
View File
@@ -55,7 +55,12 @@ async function writeArtifact(root: string, relative: string, value: string | Buf
} }
function artifact(pathname: string, schemaId: string): CiGateArtifact { 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", () => { describe("CI artifact validator", () => {
+118 -1
View File
@@ -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 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 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], ["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 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 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], ["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); 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"])( it.each(["check:artifact-schemas", "check:ci-workflow"])(
"rejects a missing nested check:ci dependency: %s", "rejects a missing nested check:ci dependency: %s",
async (removedScript) => { 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, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`); await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
await expect(loadCiGateContract(root)).rejects.toThrow( 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([ it.each([
["true bypass", "true"], ["true bypass", "true"],
["direct self recursion", "corepack pnpm check:ci"], ["direct self recursion", "corepack pnpm check:ci"],
@@ -292,6 +332,79 @@ describe("CI gate contract", () => {
expect(log.byteLength).toBeLessThanOrEqual(8_192); expect(log.byteLength).toBeLessThanOrEqual(8_192);
expect(log.toString("utf8")).toMatch(/aggregate output|INFRASTRUCTURE_FAILURE/i); expect(log.toString("utf8")).toMatch(/aggregate output|INFRASTRUCTURE_FAILURE/i);
}, 20_000); }, 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", () => { describe("CI workflow generation", () => {
@@ -311,6 +424,10 @@ describe("CI workflow generation", () => {
); );
expect(first).not.toContain("process_dist_sha256"); expect(first).not.toContain("process_dist_sha256");
expect(first).toContain("persist-credentials: false"); 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("verify-ci-candidate-archive.ts --archive");
expect(first).toContain("--extract-to"); expect(first).toContain("--extract-to");
expect(first).not.toMatch(/\btar\s+[^\n]*--extract/u); expect(first).not.toMatch(/\btar\s+[^\n]*--extract/u);
+54
View File
@@ -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);
});
+5 -1
View File
@@ -704,7 +704,11 @@ describe("supply-chain policy", () => {
}); });
it("wires the exact security fixture checker as a passing CI gate", async () => { 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 index = indexCiGateContract(contract);
const securityGate = index.gates.get("FE-GATE-013"); const securityGate = index.gates.get("FE-GATE-013");
expect(securityGate).toBeDefined(); expect(securityGate).toBeDefined();