From a3694e51aa461c76338c2a65f7f468090f7dc14a Mon Sep 17 00:00:00 2001 From: Thierno Bah Date: Fri, 2 Oct 2026 16:07:05 +0200 Subject: [PATCH 1/4] docs: preview CLI Markdown review reports --- evidence/markdown-review.md | 515 ++++++++++++++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 evidence/markdown-review.md diff --git a/evidence/markdown-review.md b/evidence/markdown-review.md new file mode 100644 index 0000000..40c8c73 --- /dev/null +++ b/evidence/markdown-review.md @@ -0,0 +1,515 @@ +# Rootform Markdown review gallery + +Actual public CLI formatters, synthetic test fixtures. Reports below are copied verbatim from test goldens; no hand-edited tables. + +[Uncertainty and planned changes](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/uncertainty.md) | [Policy violation, both sides](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/policies.md) | [Plan](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/plan.md) | [State](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/state.md) | [Comparison and Before/After provenance](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/comparison.md) | [Drift and net change](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/drift-net.md) | [Changes and long lists](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/changes.md) | [Policy result with one side not evaluated](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/policy-failed.md) + +### Uncertainty and planned changes + +## Rootform architecture + +**76 instances, 18 Relations, 68 Contexts, and 14 Contributions added.** + +Plan analyzed. Planned changes compare **Refreshed** with **Planned**. + +**Uncertainty** + +| Stage | Indeterminate closures | Unavailable | Unknown until apply | +| --- | ---: | ---: | ---: | +| Planned | 27 | 26 | 1 | + +| Change | Instances | Relations | Contexts | Contributions | +| --- | ---: | ---: | ---: | ---: | +| Added | 76 | 18 | 68 | 14 | +| Removed | 0 | 0 | 0 | 0 | + +### Planned changes + +
+Instances: 76 added (10 of 76 shown) + +- `object.added.00` +- `object.added.01` +- `object.added.02` +- `object.added.03` +- `object.added.04` +- `object.added.05` +- `object.added.06` +- `object.added.07` +- `object.added.08` +- `object.added.09` + +
+ +
+Relations: 18 added (10 of 18 shown) + +- **routes-to** from `object.part.00` to `object.whole` +- **routes-to** from `object.part.01` to `object.whole` +- **routes-to** from `object.part.02` to `object.whole` +- **routes-to** from `object.part.03` to `object.whole` +- **routes-to** from `object.part.04` to `object.whole` +- **routes-to** from `object.part.05` to `object.whole` +- **routes-to** from `object.part.06` to `object.whole` +- **routes-to** from `object.part.07` to `object.whole` +- **routes-to** from `object.part.08` to `object.whole` +- **routes-to** from `object.part.09` to `object.whole` + +
+ +
+Contexts: 68 added (10 of 68 shown) + +- **network**: `object.part.00` within `object.whole` +- **network**: `object.part.01` within `object.whole` +- **network**: `object.part.02` within `object.whole` +- **network**: `object.part.03` within `object.whole` +- **network**: `object.part.04` within `object.whole` +- **network**: `object.part.05` within `object.whole` +- **network**: `object.part.06` within `object.whole` +- **network**: `object.part.07` within `object.whole` +- **network**: `object.part.08` within `object.whole` +- **network**: `object.part.09` within `object.whole` + +
+ +
+Contributions: 14 added (10 of 14 shown) + +- `object.part.00` contributes to `object.whole` +- `object.part.01` contributes to `object.whole` +- `object.part.02` contributes to `object.whole` +- `object.part.03` contributes to `object.whole` +- `object.part.04` contributes to `object.whole` +- `object.part.05` contributes to `object.whole` +- `object.part.06` contributes to `object.whole` +- `object.part.07` contributes to `object.whole` +- `object.part.08` contributes to `object.whole` +- `object.part.09` contributes to `object.whole` + +
+ +### Provenance + +- **Input:** `plan.json` + +Each list above shows at most 10 entries. A report written with `--details` lists every entry. + +### Policy violation, both sides + +## Rootform Policies + +**Overall verdict: VIOLATED** + +Evaluation scope: **Both sides**. 1 Policy selected. + +| Side | Verdict | Origin | Stage | Evaluations | Violated | Indeterminate | Passed | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Before | VIOLATED | State | Recorded | 1 | 1 | 0 | 0 | +| After | PASSED | Plan | Planned | 1 | 0 | 0 | 1 | + +### Before + +#### `review.policy.requirement` + +**Requirement:** Each instance must satisfy the recorded requirement. + +**Violated: 1 evaluation** + +- `object.before` + +### After + +All selected evaluations passed. + +### Provenance + +- **Input:** `comparison.json` + +### Plan + +
+Open Plan + +## Rootform architecture + +**1 instance, 1 external endpoint, and 1 Contribution added.** + +Plan analyzed. Planned changes compare **Refreshed** with **Planned**. + +| Change | Instances | External endpoints | Contributions | +| --- | ---: | ---: | ---: | +| Added | 1 | 1 | 1 | +| Removed | 0 | 0 | 0 | + +### Reported drift + +No drift reported in this plan. The export does not establish the refresh scope. + +### Net change + +Same determined changes as Planned changes. + +### Planned changes + +**Instances: 1 added** + +- `local_file.c` + +**External endpoints: 1 added** + +- `external file` + +**Contributions: 1 added** + +- `local_file.c` contributes to `external file` + +### Planned architecture + +- **Instances:** 3 +- **Interpreted:** 3 +- **Contributions:** 2 +- **External endpoints:** 1 + +### Provenance + +- **Input:** `plan.json` +- **Producer:** Terraform 1.12.2 \(attested\) +- **Plan completeness:** Complete, as reported in the plan +- **Enrichment:** None; the plan JSON was analyzed alone +- **Stage:** Planned +- **Stages:** Recorded \(reconstructed\), Refreshed, Planned + +
+ +### State + +
+Open State + +## Rootform architecture + +**Recorded architecture: 2 instances and 1 Contribution.** + +State analyzed. + +### Recorded architecture + +- **Instances:** 2 +- **Interpreted:** 2 +- **Contributions:** 1 + +### Provenance + +- **Input:** `state.json` +- **Producer:** Terraform or OpenTofu 1.10.7 +- **Stage:** Recorded + +
+ +### Comparison and Before/After provenance + +
+Open Comparison and Before/After provenance + +## Rootform architecture + +**1 instance, 1 external endpoint, and 1 Contribution added.** + +Comparison Form loaded. Differences compare the **Recorded** stage of **Before** with the **Planned** stage of **After**. + +| Change | Instances | External endpoints | Contributions | +| --- | ---: | ---: | ---: | +| Added | 1 | 1 | 1 | +| Removed | 0 | 0 | 0 | + +### Differences + +**Instances: 1 added** + +- `local_file.c` + +**External endpoints: 1 added** + +- `external file` + +**Contributions: 1 added** + +- `local_file.c` contributes to `external file` + +### Architecture + +| Side | Instances | Interpreted | Contributions | +| --- | ---: | ---: | ---: | +| Before | 2 | 2 | 1 | +| After | 3 | 3 | 2 | + +### Provenance + +- **Input:** `comparison.json` +- **Form:** Comparison, saved by rootform 0.1.0 + +| Field | Before | After | +| --- | --- | --- | +| Origin | State | Plan | +| Producer | Terraform or OpenTofu 1.10.7 | Terraform 1.12.2 \(attested\) | +| Enrichment | Not applicable | None | +| Stage | Recorded | Planned | + +
+ +### Drift and net change + +
+Open Drift and net change + +## Rootform architecture + +**76 instances, 18 Relations, 68 Contexts, and 14 Contributions added.** + +Plan analyzed. Planned changes compare **Refreshed** with **Planned**. + +| Change | Instances | Relations | Contexts | Contributions | +| --- | ---: | ---: | ---: | ---: | +| Added | 76 | 18 | 68 | 14 | +| Removed | 0 | 0 | 0 | 0 | + +### Reported drift + +**1 drift entry reported: 1 architectural.** + +Drift compares **Recorded** with **Refreshed**. The export does not establish the refresh scope. + +Architectural effect: no architectural difference determined under the selected Dialects. + +| Stage | Indeterminate closures | Unavailable | +| --- | ---: | ---: | +| Recorded | 1 | 1 | +| Refreshed | 0 | 0 | + +- `object.drift`: changes the architecture; 1 fact change + +### Net change + +**2 instances added, 1 removed.** + +Net change compares **Recorded** with **Planned**. + +| Change | Instances | +| --- | ---: | +| Added | 2 | +| Removed | 1 | + +**Instances: 2 added, 1 removed** + +- Added + - `object.added.00` + - `object.added.01` +- Removed + - `object.removed.00` + +### Planned changes + +
+Instances: 76 added (10 of 76 shown) + +- `object.added.00` +- `object.added.01` +- `object.added.02` +- `object.added.03` +- `object.added.04` +- `object.added.05` +- `object.added.06` +- `object.added.07` +- `object.added.08` +- `object.added.09` + +
+ +
+Relations: 18 added (10 of 18 shown) + +- **routes-to** from `object.part.00` to `object.whole` +- **routes-to** from `object.part.01` to `object.whole` +- **routes-to** from `object.part.02` to `object.whole` +- **routes-to** from `object.part.03` to `object.whole` +- **routes-to** from `object.part.04` to `object.whole` +- **routes-to** from `object.part.05` to `object.whole` +- **routes-to** from `object.part.06` to `object.whole` +- **routes-to** from `object.part.07` to `object.whole` +- **routes-to** from `object.part.08` to `object.whole` +- **routes-to** from `object.part.09` to `object.whole` + +
+ +
+Contexts: 68 added (10 of 68 shown) + +- **network**: `object.part.00` within `object.whole` +- **network**: `object.part.01` within `object.whole` +- **network**: `object.part.02` within `object.whole` +- **network**: `object.part.03` within `object.whole` +- **network**: `object.part.04` within `object.whole` +- **network**: `object.part.05` within `object.whole` +- **network**: `object.part.06` within `object.whole` +- **network**: `object.part.07` within `object.whole` +- **network**: `object.part.08` within `object.whole` +- **network**: `object.part.09` within `object.whole` + +
+ +
+Contributions: 14 added (10 of 14 shown) + +- `object.part.00` contributes to `object.whole` +- `object.part.01` contributes to `object.whole` +- `object.part.02` contributes to `object.whole` +- `object.part.03` contributes to `object.whole` +- `object.part.04` contributes to `object.whole` +- `object.part.05` contributes to `object.whole` +- `object.part.06` contributes to `object.whole` +- `object.part.07` contributes to `object.whole` +- `object.part.08` contributes to `object.whole` +- `object.part.09` contributes to `object.whole` + +
+ +### Provenance + +- **Input:** `plan.json` + +Each list above shows at most 10 entries. A report written with `--details` lists every entry. + +
+ +### Changes and long lists + +
+Open Changes and long lists + +## Rootform architecture + +**76 instances, 18 Relations, 68 Contexts, and 14 Contributions added.** + +Plan analyzed. Planned changes compare **Refreshed** with **Planned**. + +| Change | Instances | Relations | Contexts | Contributions | +| --- | ---: | ---: | ---: | ---: | +| Added | 76 | 18 | 68 | 14 | +| Removed | 0 | 0 | 0 | 0 | + +### Planned changes + +
+Instances: 76 added (10 of 76 shown) + +- `object.added.00` +- `object.added.01` +- `object.added.02` +- `object.added.03` +- `object.added.04` +- `object.added.05` +- `object.added.06` +- `object.added.07` +- `object.added.08` +- `object.added.09` + +
+ +
+Relations: 18 added (10 of 18 shown) + +- **routes-to** from `object.part.00` to `object.whole` +- **routes-to** from `object.part.01` to `object.whole` +- **routes-to** from `object.part.02` to `object.whole` +- **routes-to** from `object.part.03` to `object.whole` +- **routes-to** from `object.part.04` to `object.whole` +- **routes-to** from `object.part.05` to `object.whole` +- **routes-to** from `object.part.06` to `object.whole` +- **routes-to** from `object.part.07` to `object.whole` +- **routes-to** from `object.part.08` to `object.whole` +- **routes-to** from `object.part.09` to `object.whole` + +
+ +
+Contexts: 68 added (10 of 68 shown) + +- **network**: `object.part.00` within `object.whole` +- **network**: `object.part.01` within `object.whole` +- **network**: `object.part.02` within `object.whole` +- **network**: `object.part.03` within `object.whole` +- **network**: `object.part.04` within `object.whole` +- **network**: `object.part.05` within `object.whole` +- **network**: `object.part.06` within `object.whole` +- **network**: `object.part.07` within `object.whole` +- **network**: `object.part.08` within `object.whole` +- **network**: `object.part.09` within `object.whole` + +
+ +
+Contributions: 14 added (10 of 14 shown) + +- `object.part.00` contributes to `object.whole` +- `object.part.01` contributes to `object.whole` +- `object.part.02` contributes to `object.whole` +- `object.part.03` contributes to `object.whole` +- `object.part.04` contributes to `object.whole` +- `object.part.05` contributes to `object.whole` +- `object.part.06` contributes to `object.whole` +- `object.part.07` contributes to `object.whole` +- `object.part.08` contributes to `object.whole` +- `object.part.09` contributes to `object.whole` + +
+ +### Provenance + +- **Input:** `plan.json` + +Each list above shows at most 10 entries. A report written with `--details` lists every entry. + +
+ +### Policy result with one side not evaluated + +
+Open Policy result with one side not evaluated + +## Rootform Policies + +**Overall verdict: VIOLATED** + +Evaluation scope: **Both sides**. 1 Policy selected. + +| Side | Verdict | Origin | Stage | Evaluations | Violated | Indeterminate | Passed | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Before | VIOLATED | State | Recorded | 1 | 1 | 0 | 0 | +| After | NOT EVALUATED | Plan | Planned | - | - | - | - | + +### Before + +#### `review.policy.requirement` + +**Requirement:** Each instance must satisfy the recorded requirement. + +**Violated: 1 evaluation** + +- `object.before` + +### After + +#### Not evaluated + +- This side could not be evaluated. + +### Provenance + +- **Input:** `comparison.json` + +
+ +--- + +The plan, state and comparison command tests also preserve the saved Form JSON byte for byte and perform no backend export. This PR changes CLI source; distributed binaries receive the presentation when they consume this commit. From 96dc753a52d99feb2459229602b18017f6ffeace Mon Sep 17 00:00:00 2001 From: Thierno Bah Date: Fri, 2 Oct 2026 16:30:28 +0200 Subject: [PATCH 2/4] test: publish actual CLI Markdown from released Forms --- .github/workflows/cache.yml | 6 + .github/workflows/native.yml | 2 + .github/workflows/report.yml | 78 +++-- README.md | 10 + evidence/markdown-review.md | 515 -------------------------------- scripts/markdown-review/go.mod | 11 + scripts/markdown-review/go.sum | 13 + scripts/markdown-review/main.go | 43 +++ scripts/publish-review.ts | 59 ++++ 9 files changed, 204 insertions(+), 533 deletions(-) delete mode 100644 evidence/markdown-review.md create mode 100644 scripts/markdown-review/go.mod create mode 100644 scripts/markdown-review/go.sum create mode 100644 scripts/markdown-review/main.go create mode 100644 scripts/publish-review.ts diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml index 2109c95..85cbb6c 100644 --- a/.github/workflows/cache.yml +++ b/.github/workflows/cache.yml @@ -2,6 +2,12 @@ name: exact external content cache on: pull_request: branches: [dev] + paths-ignore: + - 'README.md' + - 'evidence/**' + - '.github/workflows/report.yml' + - 'scripts/markdown-review/**' + - 'scripts/publish-review.ts' workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index c7ce9c9..c36d2b0 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -5,6 +5,8 @@ on: paths-ignore: - 'README.md' - 'evidence/**' + - 'scripts/markdown-review/**' + - 'scripts/publish-review.ts' - '.github/workflows/cache.yml' - '.github/workflows/report.yml' - 'scripts/assert-cache.mjs' diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index 666daba..02c4149 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -27,30 +27,72 @@ jobs: path: fixtures/rootform sparse-checkout: examples/playground/shared-data-platform persist-credentials: false - - name: Integrated analysis and genuine negative gate - id: rootform - continue-on-error: true - uses: rootform-dev/action@aa619a35a674a0be901650379db3b0551ee645ad + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: rootform-dev/action + ref: 17cf0cbefae80ac388b54e0329dd40f3cd0f659e + path: fixtures/action + sparse-checkout: src + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.26.7' + cache: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.4.0' + - name: Generate actual Form with the published release + id: analyze + uses: rootform-dev/action/analyze@17cf0cbefae80ac388b54e0329dd40f3cd0f659e with: version: 0.1.0-pr.117.1 input: fixtures/rootform/examples/playground/shared-data-platform/base/plan.json plan-file: fixtures/rootform/examples/playground/shared-data-platform/base/plan.tfplan + upload-artifact: 'false' + summary: 'false' + - name: Reopen actual Form with the modified public CLI + env: + INPUT_FORM: ${{ steps.analyze.outputs.form }} + GOWORK: 'off' + GOTOOLCHAIN: go1.26.7 + run: | + mkdir -p rendered + go -C scripts/markdown-review run . "$INPUT_FORM" "$GITHUB_WORKSPACE/rendered/architecture.md" "$GITHUB_WORKSPACE/rendered/form.json" + cmp "$INPUT_FORM" rendered/form.json + - name: Produce actual negative Policy result + id: check + continue-on-error: true + uses: rootform-dev/action/check@17cf0cbefae80ac388b54e0329dd40f3cd0f659e + with: + input: ${{ steps.analyze.outputs.form }} policy-pack: fixtures/negative - comment: 'true' - artifact-name: integrated-evidence - - name: Verify integrated union and failed gate evidence + upload-artifact: 'false' + summary: 'false' + - name: Inspect and assemble actual outputs env: - ROOTFORM_OUTPUTS: ${{ toJSON(steps.rootform.outputs) }} - ROOTFORM_EXPECT_ARTIFACT: 'true' - STEP_OUTCOME: ${{ steps.rootform.outcome }} + ROOTFORM_OUTPUTS: ${{ toJSON(steps.check.outputs) }} + ROOTFORM_ACTION_SOURCE: fixtures/action + ROOTFORM_INPUT_FORM: ${{ steps.analyze.outputs.form }} + ROOTFORM_ARCHITECTURE: rendered/architecture.md + ROOTFORM_POLICY_REPORT: ${{ steps.check.outputs.report }} + ROOTFORM_POLICY_RESULT: ${{ steps.check.outputs.result }} + ROOTFORM_SARIF: ${{ steps.check.outputs.sarif }} + ROOTFORM_HTML: ${{ steps.analyze.outputs.html }} + STEP_OUTCOME: ${{ steps.check.outcome }} run: | - node -e "require('node:assert/strict').equal(process.env.STEP_OUTCOME,'failure')" - node scripts/assert-output.mjs analyze node scripts/assert-output.mjs check 1 - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + bun scripts/publish-review.ts assemble + - name: Retain generated outputs + id: artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - artifact-ids: ${{ steps.rootform.outputs.artifact-id }} - path: downloaded-integrated - merge-multiple: true - - name: Verify exact integrated artifact inventory - run: node scripts/verify-artifact.mjs downloaded-integrated form.json,report.md,explorer.html,result.json,results.sarif + name: actual-markdown-review + path: rendered/ + retention-days: 7 + if-no-files-found: error + - name: Publish exact report to Summary and the single PR comment + env: + ROOTFORM_ACTION_SOURCE: fixtures/action + ROOTFORM_ARTIFACT_URL: ${{ steps.artifact.outputs.artifact-url }} + GITHUB_TOKEN: ${{ github.token }} + run: bun scripts/publish-review.ts publish diff --git a/README.md b/README.md index e58339f..ad0aee4 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,13 @@ one serialized root reporter per PR; no fork or privileged-target comment. Fork qualification uses a pull request into the public upstream repository: the integrated Action keeps its Form, reports, artifacts and Summary with the fork's reduced token, while PR commenting is skipped. + +The PR report currently qualifies the public CLI Markdown source at +`7f0ef17c418f76754ddd20d6a48310a0b257d17b`. A published binary produces the +actual plan Form, then a small public CLI embedding reopens that saved Form +and exports Markdown/JSON. Compilation is unavailable in the embedding; +Form bytes must remain identical. The unchanged single-side negative Policy +report still comes from the published binary. Shared reporting functions +from the exact Action commit publish the generated report to Summary and the +single protected bot comment. This qualifies source presentation, not a new +distributed candidate. The artifact records both sources and report digests. diff --git a/evidence/markdown-review.md b/evidence/markdown-review.md deleted file mode 100644 index 40c8c73..0000000 --- a/evidence/markdown-review.md +++ /dev/null @@ -1,515 +0,0 @@ -# Rootform Markdown review gallery - -Actual public CLI formatters, synthetic test fixtures. Reports below are copied verbatim from test goldens; no hand-edited tables. - -[Uncertainty and planned changes](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/uncertainty.md) | [Policy violation, both sides](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/policies.md) | [Plan](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/plan.md) | [State](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/state.md) | [Comparison and Before/After provenance](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/comparison.md) | [Drift and net change](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/drift-net.md) | [Changes and long lists](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/changes.md) | [Policy result with one side not evaluated](https://github.com/rootform-dev/rootform/blob/57ba8e3f9b5692bc26fb3a193d8f2a4e26476fdd/cli/internal/app/testdata/review/policy-failed.md) - -### Uncertainty and planned changes - -## Rootform architecture - -**76 instances, 18 Relations, 68 Contexts, and 14 Contributions added.** - -Plan analyzed. Planned changes compare **Refreshed** with **Planned**. - -**Uncertainty** - -| Stage | Indeterminate closures | Unavailable | Unknown until apply | -| --- | ---: | ---: | ---: | -| Planned | 27 | 26 | 1 | - -| Change | Instances | Relations | Contexts | Contributions | -| --- | ---: | ---: | ---: | ---: | -| Added | 76 | 18 | 68 | 14 | -| Removed | 0 | 0 | 0 | 0 | - -### Planned changes - -
-Instances: 76 added (10 of 76 shown) - -- `object.added.00` -- `object.added.01` -- `object.added.02` -- `object.added.03` -- `object.added.04` -- `object.added.05` -- `object.added.06` -- `object.added.07` -- `object.added.08` -- `object.added.09` - -
- -
-Relations: 18 added (10 of 18 shown) - -- **routes-to** from `object.part.00` to `object.whole` -- **routes-to** from `object.part.01` to `object.whole` -- **routes-to** from `object.part.02` to `object.whole` -- **routes-to** from `object.part.03` to `object.whole` -- **routes-to** from `object.part.04` to `object.whole` -- **routes-to** from `object.part.05` to `object.whole` -- **routes-to** from `object.part.06` to `object.whole` -- **routes-to** from `object.part.07` to `object.whole` -- **routes-to** from `object.part.08` to `object.whole` -- **routes-to** from `object.part.09` to `object.whole` - -
- -
-Contexts: 68 added (10 of 68 shown) - -- **network**: `object.part.00` within `object.whole` -- **network**: `object.part.01` within `object.whole` -- **network**: `object.part.02` within `object.whole` -- **network**: `object.part.03` within `object.whole` -- **network**: `object.part.04` within `object.whole` -- **network**: `object.part.05` within `object.whole` -- **network**: `object.part.06` within `object.whole` -- **network**: `object.part.07` within `object.whole` -- **network**: `object.part.08` within `object.whole` -- **network**: `object.part.09` within `object.whole` - -
- -
-Contributions: 14 added (10 of 14 shown) - -- `object.part.00` contributes to `object.whole` -- `object.part.01` contributes to `object.whole` -- `object.part.02` contributes to `object.whole` -- `object.part.03` contributes to `object.whole` -- `object.part.04` contributes to `object.whole` -- `object.part.05` contributes to `object.whole` -- `object.part.06` contributes to `object.whole` -- `object.part.07` contributes to `object.whole` -- `object.part.08` contributes to `object.whole` -- `object.part.09` contributes to `object.whole` - -
- -### Provenance - -- **Input:** `plan.json` - -Each list above shows at most 10 entries. A report written with `--details` lists every entry. - -### Policy violation, both sides - -## Rootform Policies - -**Overall verdict: VIOLATED** - -Evaluation scope: **Both sides**. 1 Policy selected. - -| Side | Verdict | Origin | Stage | Evaluations | Violated | Indeterminate | Passed | -| --- | --- | --- | --- | --- | --- | --- | --- | -| Before | VIOLATED | State | Recorded | 1 | 1 | 0 | 0 | -| After | PASSED | Plan | Planned | 1 | 0 | 0 | 1 | - -### Before - -#### `review.policy.requirement` - -**Requirement:** Each instance must satisfy the recorded requirement. - -**Violated: 1 evaluation** - -- `object.before` - -### After - -All selected evaluations passed. - -### Provenance - -- **Input:** `comparison.json` - -### Plan - -
-Open Plan - -## Rootform architecture - -**1 instance, 1 external endpoint, and 1 Contribution added.** - -Plan analyzed. Planned changes compare **Refreshed** with **Planned**. - -| Change | Instances | External endpoints | Contributions | -| --- | ---: | ---: | ---: | -| Added | 1 | 1 | 1 | -| Removed | 0 | 0 | 0 | - -### Reported drift - -No drift reported in this plan. The export does not establish the refresh scope. - -### Net change - -Same determined changes as Planned changes. - -### Planned changes - -**Instances: 1 added** - -- `local_file.c` - -**External endpoints: 1 added** - -- `external file` - -**Contributions: 1 added** - -- `local_file.c` contributes to `external file` - -### Planned architecture - -- **Instances:** 3 -- **Interpreted:** 3 -- **Contributions:** 2 -- **External endpoints:** 1 - -### Provenance - -- **Input:** `plan.json` -- **Producer:** Terraform 1.12.2 \(attested\) -- **Plan completeness:** Complete, as reported in the plan -- **Enrichment:** None; the plan JSON was analyzed alone -- **Stage:** Planned -- **Stages:** Recorded \(reconstructed\), Refreshed, Planned - -
- -### State - -
-Open State - -## Rootform architecture - -**Recorded architecture: 2 instances and 1 Contribution.** - -State analyzed. - -### Recorded architecture - -- **Instances:** 2 -- **Interpreted:** 2 -- **Contributions:** 1 - -### Provenance - -- **Input:** `state.json` -- **Producer:** Terraform or OpenTofu 1.10.7 -- **Stage:** Recorded - -
- -### Comparison and Before/After provenance - -
-Open Comparison and Before/After provenance - -## Rootform architecture - -**1 instance, 1 external endpoint, and 1 Contribution added.** - -Comparison Form loaded. Differences compare the **Recorded** stage of **Before** with the **Planned** stage of **After**. - -| Change | Instances | External endpoints | Contributions | -| --- | ---: | ---: | ---: | -| Added | 1 | 1 | 1 | -| Removed | 0 | 0 | 0 | - -### Differences - -**Instances: 1 added** - -- `local_file.c` - -**External endpoints: 1 added** - -- `external file` - -**Contributions: 1 added** - -- `local_file.c` contributes to `external file` - -### Architecture - -| Side | Instances | Interpreted | Contributions | -| --- | ---: | ---: | ---: | -| Before | 2 | 2 | 1 | -| After | 3 | 3 | 2 | - -### Provenance - -- **Input:** `comparison.json` -- **Form:** Comparison, saved by rootform 0.1.0 - -| Field | Before | After | -| --- | --- | --- | -| Origin | State | Plan | -| Producer | Terraform or OpenTofu 1.10.7 | Terraform 1.12.2 \(attested\) | -| Enrichment | Not applicable | None | -| Stage | Recorded | Planned | - -
- -### Drift and net change - -
-Open Drift and net change - -## Rootform architecture - -**76 instances, 18 Relations, 68 Contexts, and 14 Contributions added.** - -Plan analyzed. Planned changes compare **Refreshed** with **Planned**. - -| Change | Instances | Relations | Contexts | Contributions | -| --- | ---: | ---: | ---: | ---: | -| Added | 76 | 18 | 68 | 14 | -| Removed | 0 | 0 | 0 | 0 | - -### Reported drift - -**1 drift entry reported: 1 architectural.** - -Drift compares **Recorded** with **Refreshed**. The export does not establish the refresh scope. - -Architectural effect: no architectural difference determined under the selected Dialects. - -| Stage | Indeterminate closures | Unavailable | -| --- | ---: | ---: | -| Recorded | 1 | 1 | -| Refreshed | 0 | 0 | - -- `object.drift`: changes the architecture; 1 fact change - -### Net change - -**2 instances added, 1 removed.** - -Net change compares **Recorded** with **Planned**. - -| Change | Instances | -| --- | ---: | -| Added | 2 | -| Removed | 1 | - -**Instances: 2 added, 1 removed** - -- Added - - `object.added.00` - - `object.added.01` -- Removed - - `object.removed.00` - -### Planned changes - -
-Instances: 76 added (10 of 76 shown) - -- `object.added.00` -- `object.added.01` -- `object.added.02` -- `object.added.03` -- `object.added.04` -- `object.added.05` -- `object.added.06` -- `object.added.07` -- `object.added.08` -- `object.added.09` - -
- -
-Relations: 18 added (10 of 18 shown) - -- **routes-to** from `object.part.00` to `object.whole` -- **routes-to** from `object.part.01` to `object.whole` -- **routes-to** from `object.part.02` to `object.whole` -- **routes-to** from `object.part.03` to `object.whole` -- **routes-to** from `object.part.04` to `object.whole` -- **routes-to** from `object.part.05` to `object.whole` -- **routes-to** from `object.part.06` to `object.whole` -- **routes-to** from `object.part.07` to `object.whole` -- **routes-to** from `object.part.08` to `object.whole` -- **routes-to** from `object.part.09` to `object.whole` - -
- -
-Contexts: 68 added (10 of 68 shown) - -- **network**: `object.part.00` within `object.whole` -- **network**: `object.part.01` within `object.whole` -- **network**: `object.part.02` within `object.whole` -- **network**: `object.part.03` within `object.whole` -- **network**: `object.part.04` within `object.whole` -- **network**: `object.part.05` within `object.whole` -- **network**: `object.part.06` within `object.whole` -- **network**: `object.part.07` within `object.whole` -- **network**: `object.part.08` within `object.whole` -- **network**: `object.part.09` within `object.whole` - -
- -
-Contributions: 14 added (10 of 14 shown) - -- `object.part.00` contributes to `object.whole` -- `object.part.01` contributes to `object.whole` -- `object.part.02` contributes to `object.whole` -- `object.part.03` contributes to `object.whole` -- `object.part.04` contributes to `object.whole` -- `object.part.05` contributes to `object.whole` -- `object.part.06` contributes to `object.whole` -- `object.part.07` contributes to `object.whole` -- `object.part.08` contributes to `object.whole` -- `object.part.09` contributes to `object.whole` - -
- -### Provenance - -- **Input:** `plan.json` - -Each list above shows at most 10 entries. A report written with `--details` lists every entry. - -
- -### Changes and long lists - -
-Open Changes and long lists - -## Rootform architecture - -**76 instances, 18 Relations, 68 Contexts, and 14 Contributions added.** - -Plan analyzed. Planned changes compare **Refreshed** with **Planned**. - -| Change | Instances | Relations | Contexts | Contributions | -| --- | ---: | ---: | ---: | ---: | -| Added | 76 | 18 | 68 | 14 | -| Removed | 0 | 0 | 0 | 0 | - -### Planned changes - -
-Instances: 76 added (10 of 76 shown) - -- `object.added.00` -- `object.added.01` -- `object.added.02` -- `object.added.03` -- `object.added.04` -- `object.added.05` -- `object.added.06` -- `object.added.07` -- `object.added.08` -- `object.added.09` - -
- -
-Relations: 18 added (10 of 18 shown) - -- **routes-to** from `object.part.00` to `object.whole` -- **routes-to** from `object.part.01` to `object.whole` -- **routes-to** from `object.part.02` to `object.whole` -- **routes-to** from `object.part.03` to `object.whole` -- **routes-to** from `object.part.04` to `object.whole` -- **routes-to** from `object.part.05` to `object.whole` -- **routes-to** from `object.part.06` to `object.whole` -- **routes-to** from `object.part.07` to `object.whole` -- **routes-to** from `object.part.08` to `object.whole` -- **routes-to** from `object.part.09` to `object.whole` - -
- -
-Contexts: 68 added (10 of 68 shown) - -- **network**: `object.part.00` within `object.whole` -- **network**: `object.part.01` within `object.whole` -- **network**: `object.part.02` within `object.whole` -- **network**: `object.part.03` within `object.whole` -- **network**: `object.part.04` within `object.whole` -- **network**: `object.part.05` within `object.whole` -- **network**: `object.part.06` within `object.whole` -- **network**: `object.part.07` within `object.whole` -- **network**: `object.part.08` within `object.whole` -- **network**: `object.part.09` within `object.whole` - -
- -
-Contributions: 14 added (10 of 14 shown) - -- `object.part.00` contributes to `object.whole` -- `object.part.01` contributes to `object.whole` -- `object.part.02` contributes to `object.whole` -- `object.part.03` contributes to `object.whole` -- `object.part.04` contributes to `object.whole` -- `object.part.05` contributes to `object.whole` -- `object.part.06` contributes to `object.whole` -- `object.part.07` contributes to `object.whole` -- `object.part.08` contributes to `object.whole` -- `object.part.09` contributes to `object.whole` - -
- -### Provenance - -- **Input:** `plan.json` - -Each list above shows at most 10 entries. A report written with `--details` lists every entry. - -
- -### Policy result with one side not evaluated - -
-Open Policy result with one side not evaluated - -## Rootform Policies - -**Overall verdict: VIOLATED** - -Evaluation scope: **Both sides**. 1 Policy selected. - -| Side | Verdict | Origin | Stage | Evaluations | Violated | Indeterminate | Passed | -| --- | --- | --- | --- | --- | --- | --- | --- | -| Before | VIOLATED | State | Recorded | 1 | 1 | 0 | 0 | -| After | NOT EVALUATED | Plan | Planned | - | - | - | - | - -### Before - -#### `review.policy.requirement` - -**Requirement:** Each instance must satisfy the recorded requirement. - -**Violated: 1 evaluation** - -- `object.before` - -### After - -#### Not evaluated - -- This side could not be evaluated. - -### Provenance - -- **Input:** `comparison.json` - -
- ---- - -The plan, state and comparison command tests also preserve the saved Form JSON byte for byte and perform no backend export. This PR changes CLI source; distributed binaries receive the presentation when they consume this commit. diff --git a/scripts/markdown-review/go.mod b/scripts/markdown-review/go.mod new file mode 100644 index 0000000..ceafd2d --- /dev/null +++ b/scripts/markdown-review/go.mod @@ -0,0 +1,11 @@ +module github.com/rootform-dev/action-qualification/markdown-review + +go 1.26.7 + +require github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect +) diff --git a/scripts/markdown-review/go.sum b/scripts/markdown-review/go.sum new file mode 100644 index 0000000..635ea93 --- /dev/null +++ b/scripts/markdown-review/go.sum @@ -0,0 +1,13 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f h1:b/nm36ILuk3AL7b6jld/5QAAU9fD32d5Defjq57TQsc= +github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f/go.mod h1:3mpH3CHzXAPAIutuAhZS7ENGerHCZvyDvZDOJ+3RRLk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/scripts/markdown-review/main.go b/scripts/markdown-review/main.go new file mode 100644 index 0000000..87e0145 --- /dev/null +++ b/scripts/markdown-review/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/rootform-dev/rootform/cli" + "github.com/rootform-dev/rootform/cli/backend" + "github.com/rootform-dev/rootform/cli/form" +) + +// This qualification harness reopens one saved Form through the public CLI. +// It has no compiler, selection loader or Policy evaluator. +type savedFormBackend struct{ backend.Backend } + +type savedFormSession struct{ backend.Session } + +func (savedFormBackend) Open(context.Context, backend.Selection, io.Writer) backend.Session { + return savedFormSession{} +} + +func (savedFormSession) Compile(context.Context, backend.Export) (backend.Compiled, error) { + return backend.Compiled{}, fmt.Errorf("qualification requires a saved Form; compilation is unavailable") +} + +func (savedFormSession) Presentation(context.Context, *form.InputForm) backend.Presentation { + return backend.Presentation{} +} + +func main() { + if len(os.Args) != 4 { + fmt.Fprintln(os.Stderr, "usage: markdown-review FORM MARKDOWN REOPENED_FORM") + os.Exit(2) + } + os.Exit(cli.Run(cli.Env{ + Args: []string{"run", os.Args[1], "--no-serve", "-o", os.Args[2], "-o", os.Args[3]}, + Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, + Getwd: os.Getwd, Getenv: os.Getenv, LookupEnv: os.LookupEnv, + Backend: savedFormBackend{}, Version: "0.1.0", + })) +} diff --git a/scripts/publish-review.ts b/scripts/publish-review.ts new file mode 100644 index 0000000..b7df2af --- /dev/null +++ b/scripts/publish-review.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { appendFileSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const cliSource = '7f0ef17c418f76754ddd20d6a48310a0b257d17b'; +const actionSource = '17cf0cbefae80ac388b54e0329dd40f3cd0f659e'; +const source = resolve(process.env.ROOTFORM_ACTION_SOURCE || 'fixtures/action'); +const { combineReports, renderReport, REPORT_MARKER } = await import(pathToFileURL(resolve(source, 'src/report.ts')).href); +const { readGitHubContext, upsertPullRequestComment } = await import(pathToFileURL(resolve(source, 'src/pull-request.ts')).href); +const required = (name: string) => { + const value = process.env[name]; + assert.ok(value, `${name} is required`); + return value; +}; +const digest = (path: string) => createHash('sha256').update(readFileSync(path)).digest('hex'); + +if (process.argv[2] === 'assemble') { + assert.equal(process.env.STEP_OUTCOME, 'failure', 'the negative Policy gate must fail'); + const original = required('ROOTFORM_INPUT_FORM'); + assert.deepEqual(readFileSync('rendered/form.json'), readFileSync(original), 'reopening must preserve exact Form bytes'); + assert.ok(readFileSync('scripts/markdown-review/go.mod', 'utf8').includes('v0.0.0-20261002141911-7f0ef17c418f')); + const report = combineReports([required('ROOTFORM_ARCHITECTURE'), required('ROOTFORM_POLICY_REPORT')], 'rendered'); + assert.equal(report, 'rendered/report.md'); + for (const [variable, filename] of [['ROOTFORM_POLICY_RESULT', 'result.json'], ['ROOTFORM_SARIF', 'results.sarif'], ['ROOTFORM_HTML', 'explorer.html']]) { + copyFileSync(required(variable), `rendered/${filename}`); + } + writeFileSync('rendered/provenance.json', JSON.stringify({ + format_version: '1', + architecture_cli_source: cliSource, + action_source: actionSource, + semantic_cli_release: '0.1.0-pr.117.1', + architecture_report_sha256: digest(required('ROOTFORM_ARCHITECTURE')), + form_sha256: digest('rendered/form.json'), + report_sha256: digest('rendered/report.md'), + policy_report_sha256: digest(required('ROOTFORM_POLICY_REPORT')), + note: 'Architecture Markdown is generated by the public CLI source from the actual released-binary Form. The unchanged single-side Policy report comes from the published CLI. This is source report qualification, not a new distributed candidate.' + }, null, 2) + '\n'); +} else if (process.argv[2] === 'publish') { + const context = readGitHubContext(); + const body = renderReport({ + version: `source-${cliSource.slice(0, 7)}`, + markdown: readFileSync('rendered/report.md', 'utf8'), + workflowUrl: context.workflowUrl, + artifactUrl: required('ROOTFORM_ARTIFACT_URL') + }); + appendFileSync(required('GITHUB_STEP_SUMMARY'), `${body}\n`); + if (context.eventName === 'pull_request' && context.pullRequest?.sameRepository) { + const result = await upsertPullRequestComment({ + token: required('GITHUB_TOKEN'), identity: context.pullRequest, + body: `${REPORT_MARKER}\n${body}`, + runId: context.runId, runAttempt: context.runAttempt + }); + console.log(JSON.stringify(result)); + } +} else { + throw new Error('usage: publish-review.ts assemble|publish'); +} From df4d97967ba6ac5e10ead2a2aa3c2f387f40402d Mon Sep 17 00:00:00 2001 From: Thierno Bah Date: Fri, 2 Oct 2026 17:44:31 +0200 Subject: [PATCH 3/4] test: qualify complete cli review hierarchy on github --- .github/workflows/native.yml | 12 ++--- .github/workflows/report.yml | 19 +++++-- README.md | 11 ++-- scripts/assert-output.mjs | 3 ++ scripts/markdown-review/go.mod | 2 +- scripts/markdown-review/go.sum | 2 + scripts/markdown-review/main.go | 94 ++++++++++++++++++++++++++++----- scripts/publish-review.ts | 23 +++++--- 8 files changed, 129 insertions(+), 37 deletions(-) diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index c36d2b0..900f2ba 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -51,7 +51,7 @@ jobs: persist-credentials: false - name: Autonomous analysis and verified installation id: analyze - uses: rootform-dev/action/analyze@aa619a35a674a0be901650379db3b0551ee645ad + uses: rootform-dev/action/analyze@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: version: 0.1.0-pr.117.1 input: fixtures/rootform/examples/playground/shared-data-platform/base/plan.json @@ -64,7 +64,7 @@ jobs: run: node scripts/assert-output.mjs analyze - name: Optional setup reuses exact verified bytes id: setup - uses: rootform-dev/action/setup@aa619a35a674a0be901650379db3b0551ee645ad + uses: rootform-dev/action/setup@cc457702223d887c9db5da52ba36099ad3e1b4e0 - name: Verify CLI version and executable digest shell: bash env: @@ -80,7 +80,7 @@ jobs: run: node -e "const fs=require('node:fs'),crypto=require('node:crypto');fs.appendFileSync(process.env.GITHUB_ENV,'ROOTFORM_PREVIOUS_SHA='+crypto.createHash('sha256').update(fs.readFileSync(process.env.ORIGINAL_FORM)).digest('hex')+'\n')" - name: Reopen the provided Form id: reopen - uses: rootform-dev/action/analyze@aa619a35a674a0be901650379db3b0551ee645ad + uses: rootform-dev/action/analyze@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: input: ${{ steps.analyze.outputs.form }} upload-artifact: 'false' @@ -92,7 +92,7 @@ jobs: run: node scripts/assert-output.mjs analyze - name: Compare Form with independently paired plan id: compare - uses: rootform-dev/action/compare@aa619a35a674a0be901650379db3b0551ee645ad + uses: rootform-dev/action/compare@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: before: ${{ steps.analyze.outputs.form }} after: fixtures/rootform/examples/playground/shared-data-platform/head/plan.json @@ -104,7 +104,7 @@ jobs: run: node scripts/assert-output.mjs compare - name: Evaluate both saved comparison sides id: check - uses: rootform-dev/action/check@aa619a35a674a0be901650379db3b0551ee645ad + uses: rootform-dev/action/check@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: input: ${{ steps.compare.outputs.form }} side: both @@ -117,7 +117,7 @@ jobs: - name: Genuine negative verdict retains all evidence id: negative continue-on-error: true - uses: rootform-dev/action/check@aa619a35a674a0be901650379db3b0551ee645ad + uses: rootform-dev/action/check@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: input: ${{ steps.analyze.outputs.form }} policy-pack: fixtures/negative diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index 02c4149..94ea121 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: rootform-dev/action - ref: 17cf0cbefae80ac388b54e0329dd40f3cd0f659e + ref: cc457702223d887c9db5da52ba36099ad3e1b4e0 path: fixtures/action sparse-checkout: src persist-credentials: false @@ -43,7 +43,7 @@ jobs: bun-version: '1.4.0' - name: Generate actual Form with the published release id: analyze - uses: rootform-dev/action/analyze@17cf0cbefae80ac388b54e0329dd40f3cd0f659e + uses: rootform-dev/action/analyze@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: version: 0.1.0-pr.117.1 input: fixtures/rootform/examples/playground/shared-data-platform/base/plan.json @@ -57,24 +57,33 @@ jobs: GOTOOLCHAIN: go1.26.7 run: | mkdir -p rendered - go -C scripts/markdown-review run . "$INPUT_FORM" "$GITHUB_WORKSPACE/rendered/architecture.md" "$GITHUB_WORKSPACE/rendered/form.json" + go -C scripts/markdown-review run . run "$INPUT_FORM" "$GITHUB_WORKSPACE/rendered/architecture.md" "$GITHUB_WORKSPACE/rendered/form.json" cmp "$INPUT_FORM" rendered/form.json - name: Produce actual negative Policy result id: check continue-on-error: true - uses: rootform-dev/action/check@17cf0cbefae80ac388b54e0329dd40f3cd0f659e + uses: rootform-dev/action/check@cc457702223d887c9db5da52ba36099ad3e1b4e0 with: input: ${{ steps.analyze.outputs.form }} policy-pack: fixtures/negative upload-artifact: 'false' summary: 'false' + - name: Render recorded Policy evidence with the modified public CLI + env: + INPUT_FORM: ${{ steps.analyze.outputs.form }} + POLICY_RESULT: ${{ steps.check.outputs.result }} + GOWORK: 'off' + GOTOOLCHAIN: go1.26.7 + run: | + go -C scripts/markdown-review run . check "$INPUT_FORM" "$POLICY_RESULT" "$GITHUB_WORKSPACE/rendered/policies.md" "$GITHUB_WORKSPACE/rendered/result.json" + cmp "$POLICY_RESULT" rendered/result.json - name: Inspect and assemble actual outputs env: ROOTFORM_OUTPUTS: ${{ toJSON(steps.check.outputs) }} ROOTFORM_ACTION_SOURCE: fixtures/action ROOTFORM_INPUT_FORM: ${{ steps.analyze.outputs.form }} ROOTFORM_ARCHITECTURE: rendered/architecture.md - ROOTFORM_POLICY_REPORT: ${{ steps.check.outputs.report }} + ROOTFORM_POLICY_REPORT: rendered/policies.md ROOTFORM_POLICY_RESULT: ${{ steps.check.outputs.result }} ROOTFORM_SARIF: ${{ steps.check.outputs.sarif }} ROOTFORM_HTML: ${{ steps.analyze.outputs.html }} diff --git a/README.md b/README.md index ad0aee4..6754963 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,12 @@ the integrated Action keeps its Form, reports, artifacts and Summary with the fork's reduced token, while PR commenting is skipped. The PR report currently qualifies the public CLI Markdown source at -`7f0ef17c418f76754ddd20d6a48310a0b257d17b`. A published binary produces the -actual plan Form, then a small public CLI embedding reopens that saved Form -and exports Markdown/JSON. Compilation is unavailable in the embedding; -Form bytes must remain identical. The unchanged single-side negative Policy -report still comes from the published binary. Shared reporting functions +`0851ed8491599f0120ad5b3b28febbaefd7973d5`. A published binary produces the +actual plan Form and negative Policy result, then a small public CLI embedding +reopens that saved evidence and exports complete Markdown/JSON. Compilation and +Policy evaluation are unavailable in the embedding. A recorded Policy answer +is reused only for its exact Form digest, stage and selection; Form and Policy +result bytes must remain identical. Shared reporting functions from the exact Action commit publish the generated report to Summary and the single protected bot comment. This qualifies source presentation, not a new distributed candidate. The artifact records both sources and report digests. diff --git a/scripts/assert-output.mjs b/scripts/assert-output.mjs index 9c8fa7d..395e94b 100644 --- a/scripts/assert-output.mjs +++ b/scripts/assert-output.mjs @@ -14,6 +14,9 @@ for (const field of expected) { assert.ok(statSync(outputs[field]).size > 0, `${field} file empty`); } execFileSync('rootform', ['validate', 'form', outputs.form], { stdio: 'pipe' }); +const markdown = readFileSync(outputs.report, 'utf8'); +assert.ok(!markdown.includes('Each list above shows at most'), 'Action must request complete Markdown'); +assert.ok(!/\d+ of \d+ (?:entries|evaluations|evidence lines) shown|\(\d+ of \d+ shown\)/.test(markdown), 'Action Markdown must not contain compact previews'); assert.equal(outputs.version, '0.1.0-pr.117.1'); if (expectedCode !== undefined) assert.equal(outputs['exit-code'], expectedCode); if (process.env.ROOTFORM_EXPECT_ARTIFACT === 'true') { diff --git a/scripts/markdown-review/go.mod b/scripts/markdown-review/go.mod index ceafd2d..c30415f 100644 --- a/scripts/markdown-review/go.mod +++ b/scripts/markdown-review/go.mod @@ -2,7 +2,7 @@ module github.com/rootform-dev/action-qualification/markdown-review go 1.26.7 -require github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f +require github.com/rootform-dev/rootform/cli v0.0.0-20261002153428-0851ed849159 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/scripts/markdown-review/go.sum b/scripts/markdown-review/go.sum index 635ea93..d9ccfa0 100644 --- a/scripts/markdown-review/go.sum +++ b/scripts/markdown-review/go.sum @@ -3,6 +3,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f h1:b/nm36ILuk3AL7b6jld/5QAAU9fD32d5Defjq57TQsc= github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f/go.mod h1:3mpH3CHzXAPAIutuAhZS7ENGerHCZvyDvZDOJ+3RRLk= +github.com/rootform-dev/rootform/cli v0.0.0-20261002153428-0851ed849159 h1:OnmqdFakfHX7VcDaApxcKnXDj9xQqZC5XZZ1Cti1eWQ= +github.com/rootform-dev/rootform/cli v0.0.0-20261002153428-0851ed849159/go.mod h1:3mpH3CHzXAPAIutuAhZS7ENGerHCZvyDvZDOJ+3RRLk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= diff --git a/scripts/markdown-review/main.go b/scripts/markdown-review/main.go index 87e0145..5926916 100644 --- a/scripts/markdown-review/main.go +++ b/scripts/markdown-review/main.go @@ -5,20 +5,29 @@ import ( "fmt" "io" "os" + "reflect" "github.com/rootform-dev/rootform/cli" "github.com/rootform-dev/rootform/cli/backend" "github.com/rootform-dev/rootform/cli/form" + "github.com/rootform-dev/rootform/cli/policyresult" ) -// This qualification harness reopens one saved Form through the public CLI. -// It has no compiler, selection loader or Policy evaluator. -type savedFormBackend struct{ backend.Backend } +// Presentation qualification reopens actual saved evidence. It has no compiler +// or evaluator: a recorded Policy answer is reusable only for its exact Form, +// stage and selection. The CLI remains the sole Markdown formatter. +type savedFormBackend struct { + backend.Backend + recorded *policyresult.Result +} -type savedFormSession struct{ backend.Session } +type savedFormSession struct { + backend.Session + recorded *policyresult.Result +} -func (savedFormBackend) Open(context.Context, backend.Selection, io.Writer) backend.Session { - return savedFormSession{} +func (b savedFormBackend) Open(context.Context, backend.Selection, io.Writer) backend.Session { + return savedFormSession{recorded: b.recorded} } func (savedFormSession) Compile(context.Context, backend.Export) (backend.Compiled, error) { @@ -29,15 +38,72 @@ func (savedFormSession) Presentation(context.Context, *form.InputForm) backend.P return backend.Presentation{} } +func (s savedFormSession) Policies(context.Context, []string) (backend.PolicySet, error) { + if s.recorded == nil { + return nil, fmt.Errorf("qualification requires an existing Policy result") + } + return recordedPolicies{result: *s.recorded}, nil +} + +type recordedPolicies struct{ result policyresult.Result } + +func (p recordedPolicies) Packs() []backend.Pack { + return []backend.Pack{{Record: p.result.Architectures[0].PolicyPacks[0], Policies: p.result.Selection.Policies}} +} + +func (p recordedPolicies) Evaluate(_ context.Context, input *form.InputForm, stage form.Stage, selected []string) policyresult.Architecture { + digest, err := form.Digest(form.Form{Input: input}) + a := p.result.Architectures[0] + if err != nil || digest != p.result.Form.Digest || a.Digest != digest || stage != a.Stage || !reflect.DeepEqual(selected, p.result.Selection.Policies) { + return policyresult.ArchitectureUnavailable(len(selected), "EVIDENCE_MISMATCH", "recorded Policy answer does not match the Form, stage or selected Policies") + } + return a +} + +func execute() error { + if len(os.Args) < 2 { + return fmt.Errorf("usage: markdown-review run FORM MARKDOWN REOPENED_FORM | check FORM RESULT MARKDOWN REOPENED_RESULT") + } + b := savedFormBackend{} + version := "0.1.0-pr.117.1" + var args []string + expected := 0 + switch { + case os.Args[1] == "run" && len(os.Args) == 5: + args = []string{"run", os.Args[2], "--no-serve", "--details", "-o", os.Args[3], "-o", os.Args[4]} + case os.Args[1] == "check" && len(os.Args) == 6: + data, err := os.ReadFile(os.Args[3]) + if err != nil { + return err + } + r, err := policyresult.Decode(data) + if err != nil { + return err + } + if r.Form == nil || r.Scope != policyresult.ScopeInput || r.Form.Origin != "saved" || len(r.Architectures) != 1 || len(r.Architectures[0].PolicyPacks) != 1 || r.Architectures[0].Side != "" { + return fmt.Errorf("qualification requires one recorded single-architecture check with one Policy Pack") + } + b.recorded, version = &r, r.Generator.Version + expected = policyresult.ExitCode(r.Status) + args = []string{"check", os.Args[2], "--stage", string(r.Architectures[0].Stage), "--details", "-o", os.Args[4], "-o", os.Args[5]} + default: + return fmt.Errorf("invalid presentation qualification arguments") + } + code := cli.Run(cli.Env{ + Args: args, + Stdin: os.Stdin, Stdout: io.Discard, Stderr: os.Stderr, + Getwd: os.Getwd, Getenv: os.Getenv, LookupEnv: os.LookupEnv, + Backend: b, Version: version, + }) + if code != expected { + return fmt.Errorf("CLI returned %d; recorded evidence requires %d", code, expected) + } + return nil +} + func main() { - if len(os.Args) != 4 { - fmt.Fprintln(os.Stderr, "usage: markdown-review FORM MARKDOWN REOPENED_FORM") + if err := execute(); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(2) } - os.Exit(cli.Run(cli.Env{ - Args: []string{"run", os.Args[1], "--no-serve", "-o", os.Args[2], "-o", os.Args[3]}, - Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, - Getwd: os.Getwd, Getenv: os.Getenv, LookupEnv: os.LookupEnv, - Backend: savedFormBackend{}, Version: "0.1.0", - })) } diff --git a/scripts/publish-review.ts b/scripts/publish-review.ts index b7df2af..45f7db0 100644 --- a/scripts/publish-review.ts +++ b/scripts/publish-review.ts @@ -4,8 +4,8 @@ import { appendFileSync, copyFileSync, readFileSync, writeFileSync } from 'node: import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -const cliSource = '7f0ef17c418f76754ddd20d6a48310a0b257d17b'; -const actionSource = '17cf0cbefae80ac388b54e0329dd40f3cd0f659e'; +const cliSource = '0851ed8491599f0120ad5b3b28febbaefd7973d5'; +const actionSource = 'cc457702223d887c9db5da52ba36099ad3e1b4e0'; const source = resolve(process.env.ROOTFORM_ACTION_SOURCE || 'fixtures/action'); const { combineReports, renderReport, REPORT_MARKER } = await import(pathToFileURL(resolve(source, 'src/report.ts')).href); const { readGitHubContext, upsertPullRequestComment } = await import(pathToFileURL(resolve(source, 'src/pull-request.ts')).href); @@ -20,22 +20,33 @@ if (process.argv[2] === 'assemble') { assert.equal(process.env.STEP_OUTCOME, 'failure', 'the negative Policy gate must fail'); const original = required('ROOTFORM_INPUT_FORM'); assert.deepEqual(readFileSync('rendered/form.json'), readFileSync(original), 'reopening must preserve exact Form bytes'); - assert.ok(readFileSync('scripts/markdown-review/go.mod', 'utf8').includes('v0.0.0-20261002141911-7f0ef17c418f')); + assert.ok(readFileSync('scripts/markdown-review/go.mod', 'utf8').includes('v0.0.0-20261002153428-0851ed849159')); + assert.deepEqual(readFileSync('rendered/result.json'), readFileSync(required('ROOTFORM_POLICY_RESULT')), 'Policy rendering must preserve the actual recorded result bytes'); const report = combineReports([required('ROOTFORM_ARCHITECTURE'), required('ROOTFORM_POLICY_REPORT')], 'rendered'); assert.equal(report, 'rendered/report.md'); - for (const [variable, filename] of [['ROOTFORM_POLICY_RESULT', 'result.json'], ['ROOTFORM_SARIF', 'results.sarif'], ['ROOTFORM_HTML', 'explorer.html']]) { + const markdown = readFileSync(report, 'utf8'); + assert.equal((markdown.match(/^## Rootform$/gm) || []).length, 1); + assert.deepEqual([...markdown.matchAll(/^### (.+)$/gm)].map(match => match[1]), ['Architecture', 'Policies', 'Details']); + assert.equal((markdown.match(/^---$/gm) || []).length, 2); + assert.ok(!/^# |^#####|^#### (?:Uncertainty|Incomplete coverage|Without target|`)/m.test(markdown)); + assert.ok(!markdown.includes('Each list above shows at most')); + assert.ok(!/\d+ of \d+ (?:entries|evaluations|evidence lines) shown|\(\d+ of \d+ shown\)/.test(markdown)); + assert.ok(markdown.includes('> [!CAUTION]\n> **Verdict: VIOLATED**')); + assert.ok(markdown.includes('Instances: 76 added')); + for (const [variable, filename] of [['ROOTFORM_SARIF', 'results.sarif'], ['ROOTFORM_HTML', 'explorer.html']]) { copyFileSync(required(variable), `rendered/${filename}`); } writeFileSync('rendered/provenance.json', JSON.stringify({ format_version: '1', - architecture_cli_source: cliSource, + review_cli_source: cliSource, action_source: actionSource, semantic_cli_release: '0.1.0-pr.117.1', architecture_report_sha256: digest(required('ROOTFORM_ARCHITECTURE')), form_sha256: digest('rendered/form.json'), report_sha256: digest('rendered/report.md'), policy_report_sha256: digest(required('ROOTFORM_POLICY_REPORT')), - note: 'Architecture Markdown is generated by the public CLI source from the actual released-binary Form. The unchanged single-side Policy report comes from the published CLI. This is source report qualification, not a new distributed candidate.' + policy_result_sha256: digest('rendered/result.json'), + note: 'Review Markdown is generated by the exact public CLI source from the actual released-binary Form and recorded Policy answer. Form and Policy result bytes remain identical; no compilation or new Policy evaluation occurs in the presentation embedding. This is source report qualification, not a new distributed candidate.' }, null, 2) + '\n'); } else if (process.argv[2] === 'publish') { const context = readGitHubContext(); From 736277d7c55b12d33005f8e98c0c370572d0af33 Mon Sep 17 00:00:00 2001 From: Thierno Bah Date: Fri, 2 Oct 2026 18:14:06 +0200 Subject: [PATCH 4/4] test: render net change delta from the exact cli source --- README.md | 2 +- scripts/markdown-review/go.mod | 2 +- scripts/markdown-review/go.sum | 6 ++---- scripts/publish-review.ts | 5 +++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6754963..d21b40a 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ the integrated Action keeps its Form, reports, artifacts and Summary with the fork's reduced token, while PR commenting is skipped. The PR report currently qualifies the public CLI Markdown source at -`0851ed8491599f0120ad5b3b28febbaefd7973d5`. A published binary produces the +`a1adfafa66a548b270727cfbce913325e5980f77`. A published binary produces the actual plan Form and negative Policy result, then a small public CLI embedding reopens that saved evidence and exports complete Markdown/JSON. Compilation and Policy evaluation are unavailable in the embedding. A recorded Policy answer diff --git a/scripts/markdown-review/go.mod b/scripts/markdown-review/go.mod index c30415f..7f22ccd 100644 --- a/scripts/markdown-review/go.mod +++ b/scripts/markdown-review/go.mod @@ -2,7 +2,7 @@ module github.com/rootform-dev/action-qualification/markdown-review go 1.26.7 -require github.com/rootform-dev/rootform/cli v0.0.0-20261002153428-0851ed849159 +require github.com/rootform-dev/rootform/cli v0.0.0-20261002160814-a1adfafa66a5 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/scripts/markdown-review/go.sum b/scripts/markdown-review/go.sum index d9ccfa0..8b21ea0 100644 --- a/scripts/markdown-review/go.sum +++ b/scripts/markdown-review/go.sum @@ -1,10 +1,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f h1:b/nm36ILuk3AL7b6jld/5QAAU9fD32d5Defjq57TQsc= -github.com/rootform-dev/rootform/cli v0.0.0-20261002141911-7f0ef17c418f/go.mod h1:3mpH3CHzXAPAIutuAhZS7ENGerHCZvyDvZDOJ+3RRLk= -github.com/rootform-dev/rootform/cli v0.0.0-20261002153428-0851ed849159 h1:OnmqdFakfHX7VcDaApxcKnXDj9xQqZC5XZZ1Cti1eWQ= -github.com/rootform-dev/rootform/cli v0.0.0-20261002153428-0851ed849159/go.mod h1:3mpH3CHzXAPAIutuAhZS7ENGerHCZvyDvZDOJ+3RRLk= +github.com/rootform-dev/rootform/cli v0.0.0-20261002160814-a1adfafa66a5 h1:asbp+U1A78/NJFmEl3tlWBhakHyZCXZZo7AJfl93bZo= +github.com/rootform-dev/rootform/cli v0.0.0-20261002160814-a1adfafa66a5/go.mod h1:3mpH3CHzXAPAIutuAhZS7ENGerHCZvyDvZDOJ+3RRLk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= diff --git a/scripts/publish-review.ts b/scripts/publish-review.ts index 45f7db0..56947d3 100644 --- a/scripts/publish-review.ts +++ b/scripts/publish-review.ts @@ -4,7 +4,7 @@ import { appendFileSync, copyFileSync, readFileSync, writeFileSync } from 'node: import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -const cliSource = '0851ed8491599f0120ad5b3b28febbaefd7973d5'; +const cliSource = 'a1adfafa66a548b270727cfbce913325e5980f77'; const actionSource = 'cc457702223d887c9db5da52ba36099ad3e1b4e0'; const source = resolve(process.env.ROOTFORM_ACTION_SOURCE || 'fixtures/action'); const { combineReports, renderReport, REPORT_MARKER } = await import(pathToFileURL(resolve(source, 'src/report.ts')).href); @@ -20,7 +20,7 @@ if (process.argv[2] === 'assemble') { assert.equal(process.env.STEP_OUTCOME, 'failure', 'the negative Policy gate must fail'); const original = required('ROOTFORM_INPUT_FORM'); assert.deepEqual(readFileSync('rendered/form.json'), readFileSync(original), 'reopening must preserve exact Form bytes'); - assert.ok(readFileSync('scripts/markdown-review/go.mod', 'utf8').includes('v0.0.0-20261002153428-0851ed849159')); + assert.ok(readFileSync('scripts/markdown-review/go.mod', 'utf8').includes(`-${cliSource.slice(0, 12)}`)); assert.deepEqual(readFileSync('rendered/result.json'), readFileSync(required('ROOTFORM_POLICY_RESULT')), 'Policy rendering must preserve the actual recorded result bytes'); const report = combineReports([required('ROOTFORM_ARCHITECTURE'), required('ROOTFORM_POLICY_REPORT')], 'rendered'); assert.equal(report, 'rendered/report.md'); @@ -33,6 +33,7 @@ if (process.argv[2] === 'assemble') { assert.ok(!/\d+ of \d+ (?:entries|evaluations|evidence lines) shown|\(\d+ of \d+ shown\)/.test(markdown)); assert.ok(markdown.includes('> [!CAUTION]\n> **Verdict: VIOLATED**')); assert.ok(markdown.includes('Instances: 76 added')); + assert.ok(markdown.includes('#### Δ Net change')); for (const [variable, filename] of [['ROOTFORM_SARIF', 'results.sarif'], ['ROOTFORM_HTML', 'explorer.html']]) { copyFileSync(required(variable), `rendered/${filename}`); }