Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,69 @@ Two entrypoints share one installer:
version: 0.1.0
```

Main entrypoint accepts `source` or `plan` mode. It writes Architecture IR,
self-contained HTML, policy JSON, SARIF, and CLI Markdown; uploads only four
named machine/render files; and appends only CLI Markdown to Job Summary. It
never parses artifacts to invent semantic or policy conclusions.
Main entrypoint accepts `source` or `plan` mode. By default it writes
Architecture IR, self-contained HTML, policy JSON, SARIF, and CLI Markdown;
uploads only four named machine/render files; and appends exact CLI policy
Markdown to Job Summary. It never parses artifacts to invent semantic or
policy conclusions.

## Pull request architecture review

Opt-in reporting compares caller-owned exact checkouts, publishes Rootform CLI
diff and policy Markdown in Job Summary, updates one pull-request comment, and
uploads complete machine evidence:

```yaml
name: Rootform architecture

on:
pull_request:

permissions:
contents: read
pull-requests: write

jobs:
architecture:
runs-on: ubuntu-latest
steps:
- name: Check out head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: rootform-head
ref: ${{ github.event.pull_request.head.sha }}

- name: Check out base
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: rootform-base
ref: ${{ github.event.pull_request.base.sha }}

- name: Review architecture
uses: rootform-dev/action@v1
with:
path: rootform-head
baseline-path: rootform-base
report-diff: true
pull-request-token: ${{ github.token }}
```

Each source checkout must carry its exact project dialect set: vendored
`.rootform/dialects`, or `rootform.lock` plus already installed dialects. Action
runs each source project from its own root; it does not fetch Git revisions or
install dialects. Fork pull requests still receive Summary and artifact
evidence, but Action never uses a write token on them. Workflows must use
`pull_request`, not `pull_request_target`.

`report-diff` defaults to `false`. `fail-on-changes` gates documented diff exit
`1` independently from `fail-on-violations`. Plan mode needs no
`baseline-path`: Rootform derives before and planned architectures from the
named plan JSON.

The artifact inventory stays fixed. Existing analysis uploads current
Architecture IR and HTML plus policy JSON and SARIF. Source diff reporting adds
baseline Architecture IR and HTML plus exact diff JSON and Markdown. Plan diff
reporting adds exact diff JSON and Markdown.

Release archive and `SHA256SUMS` must both match GitHub asset metadata. Binary
enters tool cache and `PATH` only after archive checksum and reported version
Expand Down
30 changes: 28 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ inputs:
required: false
default: latest
github-token:
description: Optional token for private published Rootform releases
description: Optional token for authenticated Rootform release access
required: false
default: ""
mode:
Expand All @@ -27,13 +27,29 @@ inputs:
required: false
default: rootform
upload-artifact:
description: Upload JSON, HTML, and SARIF result files
description: Upload fixed Rootform result evidence
required: false
default: "true"
fail-on-violations:
description: Fail job when Rootform check exits 1
required: false
default: "true"
report-diff:
description: Generate exact Rootform architecture diff evidence
required: false
default: "false"
baseline-path:
description: Baseline Terraform directory for source diff reporting
required: false
default: ""
fail-on-changes:
description: Fail job when Rootform diff exits 1
required: false
default: "false"
pull-request-token:
description: Optional same-repository pull request comment token
required: false
default: ""
outputs:
version:
description: Exact installed Rootform version
Expand All @@ -47,6 +63,16 @@ outputs:
description: Workspace-relative policy JSON path
sarif:
description: Workspace-relative SARIF path
diff-exit-code:
description: Exact Rootform diff exit code when report-diff is enabled
diff-json:
description: Workspace-relative architecture diff JSON path
diff-markdown:
description: Workspace-relative architecture diff Markdown path
baseline-architecture:
description: Workspace-relative baseline Architecture IR path in source mode
baseline-html:
description: Workspace-relative baseline self-contained HTML path in source mode
artifact-id:
description: Uploaded GitHub Actions artifact identifier
artifact-url:
Expand Down
272 changes: 150 additions & 122 deletions dist/main/index.js

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions dist/setup/index.js

Large diffs are not rendered by default.

139 changes: 139 additions & 0 deletions docs/adr/003-pull-request-reporting-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# ADR-003: Pull request reporting boundary

- Status: Accepted
- Date: 2026-08-31
- Owners: @soulbah
- Owner approval: @soulbah — 2026-08-31 (explicit directive to implement
end-to-end pull-request UX and return real evidence without another proposal)
- Related spec: `specs/002-pull-request-reporting/spec.md`

## Context

SPEC-002 adds architecture diff presentation to a JavaScript Action that must
remain a thin host for Rootform CLI semantics. Wrong placement would either
make Action parse machine output, expose write credentials to fork code, fetch
an ambiguous base revision, or use `pull_request_target` while executing
untrusted pull-request content.

Verified GitHub behavior on 2026-08-31:

- issue-comment create and update endpoints accept a token with repository
`Pull requests: write` permission;
- `pull_request` workflows from forks normally receive a read-only
`GITHUB_TOKEN` and no secrets, and workflow YAML cannot generally elevate it;
- `pull_request_target` receives elevated base-repository trust and is unsafe
when pull-request code or dependencies execute;
- each Job Summary step is limited to 1 MiB and GitHub displays at most twenty
summaries per job;
- `@actions/core` notice, warning, and error commands produce GitHub
annotations without creating a custom check run.

The published Rootform `0.1.0-dev.2` CLI was exercised against two synthetic
Architecture IR documents. `rootform diff before.json after.json --format
markdown --exit-code` returned documented exit `1` and complete Markdown for
added and removed entities and contexts. Action therefore needs no semantic
parser.

## Decision

- Caller owns exact checkout of current and baseline source projects. Action
performs no Git fetch, GitHub content download, or revision selection.
- Change reporting is additive and disabled by default. `report-diff` enables
it; `baseline-path` supplies source baseline; plan mode uses CLI plan diff;
`fail-on-changes` controls only documented diff exit `1`.
- Existing `github-token` remains private-release read credential only. New
`pull-request-token` performs issue-comment list, create, and update only.
- Action reads `pull_request` event metadata to identify repository, pull
request, base SHA, head SHA, and fork status. It writes only for a
same-repository pull request with explicit token. Fork and non-PR runs retain
analysis, summary, annotation, and artifact but skip comments.
- No `pull_request_target` workflow is introduced.
- Action finds at most one `github-actions[bot]` comment carrying stable hidden
marker. It updates that comment or creates one. Duplicate bot markers fail
explicitly.
- Job Summary and comment frame exact CLI Markdown with stable identity,
outcome, and evidence links. Outcome words derive only from documented CLI
exits. Action never derives counts or resource meaning from JSON or Markdown.
- Internal report ceilings are 60,000 UTF-8 bytes for comment and 900 KiB for
one summary write. Oversized CLI Markdown is replaced as a whole by explicit
artifact-link state, never partially truncated.
- Action emits one overall notice for unchanged and one overall warning for
changed. Policy failures retain existing Action failure behavior. Detailed
SARIF upload stays caller-owned because it requires separate permissions and
GitHub product configuration.
- CLI child environments remove `INPUT_GITHUB-TOKEN` and
`INPUT_PULL-REQUEST-TOKEN`. Both values are registered with runner masking
before network or subprocess work.

## Alternatives considered

- **Action fetches base revision.** Rejected: adds network after install,
ambiguous revision ownership, fork credentials, and hidden Git behavior.
- **`pull_request_target` reporter.** Rejected: elevated token and secrets share
trust boundary with potentially untrusted code or artifacts.
- **Parse diff JSON for custom cards and counts.** Rejected: duplicates CLI
semantics and creates format coupling.
- **Require comment permission for every run.** Rejected: analysis-only callers
need only read access, and fork runs must remain useful without secrets.
- **Reuse private-release token.** Rejected: combines unrelated read and write
authority and would make public release usage require excessive scope.
- **Create a custom check run.** Rejected: extra `checks: write`, lifecycle, and
annotation semantics add no evidence unavailable from native Action
annotation, Summary, comment, and artifact.
- **Generate PDF report.** Rejected: static duplicate cannot update in place,
hides review links, and adds rendering/runtime surface.

## Consequences

Normal snapshot callers remain unchanged. Opt-in workflows add
`pull-requests: write` and pass `${{ github.token }}` through dedicated input.
Repositories must supply exact dialects and exact base/current checkout paths.

Comments stay current instead of accumulating. Large diffs remain complete in
artifact while PR surface states why inline content is absent. Fork contributors
still receive analysis evidence but not a write credential.

Action performs one new optional post-install network operation. REST failures
are explicit because silently missing requested PR evidence would mislead
reviewers.

## Validation

Primary-source evidence:

- https://docs.github.com/en/rest/issues/comments
- https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository
- https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target
- https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions
- https://github.com/actions/toolkit/tree/main/packages/core

Disposable CLI spike:

```text
rootform 0.1.0-dev.2
rootform diff before.json after.json --format markdown --output diff.md --exit-code
exit 1
```

Observed Markdown contained added and removed contexts and entities. Fixture
used synthetic VPC, subnet, and internet-gateway declarations only. No spike
artifact enters Git.

Implementation proof:

```bash
bun test src/diff.test.ts src/report.test.ts src/pull-request.test.ts
bun run verify
```

Real-run proof records pull request, run, comment, artifact, annotation, and
screenshots under SPEC-002 evidence.

## Reversal

Remove additive inputs, outputs, report modules, comment client, diff artifact
files, and report framing. Existing SPEC-001 analysis, summary, artifact,
installer, and tag behavior remains. Reconsider direct base acquisition only if
Rootform publishes a separate authenticated protocol with equivalent fork and
offline guarantees.
54 changes: 54 additions & 0 deletions specs/002-pull-request-reporting/evidence/changed-comment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!-- rootform:architecture-review -->

## Rootform architecture review

`base:ddc7884` → `head:c3548d2`

> [!WARNING]
> Architecture changes detected.

| Review gate | Result |
| --- | --- |
| Architecture | ⚠️ Changes detected |
| Policies | ✅ Passed |
| Rootform | `0.1.0-dev.2` |

<details open>
<summary><strong>Architecture changes</strong></summary>

## Rootform diff

| Change | What |
| --- | --- |
| added | context "core/network" from entity:aws_internet_gateway.main to scope:aws_vpc.main |
| removed | context "core/network" from scope:aws_subnet.application to scope:aws_vpc.main |
| added | context "core/network" from scope:aws_subnet.public to scope:aws_vpc.main |
| added | aws/internet-gateway "main" |
| removed | core/subnet "application" |
| added | core/subnet "public" |

</details>

<details>
<summary><strong>Policy checks</strong></summary>

## Rootform check

0 policies, 0 evaluations, 0 passed, 0 violated, 0 indeterminate

</details>

### Evidence

[Download complete evidence](https://github.com/rootform-dev/action/actions/runs/33447403911/artifacts/9778517589) · [Open workflow run](https://github.com/rootform-dev/action/actions/runs/33447403911)

<details>
<summary>Run provenance</summary>

- Mode: `source`
- Current input: `test/e2e/pull-request-reporting/current`
- Baseline input: `test/e2e/pull-request-reporting/before`
- Base commit: `ddc78843e488fff794de42b3f4732884ce1b22cc`
- Head commit: `c3548d2bb51d80e2168d90f211e047575c378461`

</details>
Loading