Use Actions and AW ASTs for precise scanner differentials by pelikhan · Pull Request #70868 · github/gh-aw · GitHub
Skip to content

Use Actions and AW ASTs for precise scanner differentials - #70868

Merged
pelikhan merged 6 commits into
mainfrom
pelikhan-potential-telegram
Oct 21, 2026
Merged

pelikhan merged 6 commits into
mainfrom
pelikhan-potential-telegram

Conversation

@pelikhan

@pelikhan pelikhan commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

The differential scanner previously used the nearest preceding YAML node and raw line text. This could misattribute findings, conflate distinct steps, or report harmless formatting changes as new findings. Adversarial analysis also reproduced source/sink identity collisions and fail-open handling of partial scanner results.

Summary

  • Use Actions/AW semantic ASTs with precise source anchors, AW frontmatter offsets, alias resolution, and result-scoped source caching.
  • Locate steps by ID or unique name, otherwise fingerprint their contents. A step label is not sufficient evidence that its configuration is unchanged.
  • Preserve every Zizmor primary/related symbolic scope, including cross-file source/sink relationships, and fingerprint the complete reported scopes.
  • Reject malformed diagnostics even when mixed with valid findings. Traverse every joined error branch so later findings and operational failures cannot disappear.
  • Preserve ShellCheck diagnostic text and reject invalid script coordinates.
  • Retain conservative coordinate/file-fingerprint fallback, documentation, and adversarial regressions.

Main integration

Merged origin/main at 27cc912e2d, preserving its stricter policy: existing and added medium-or-higher or unclassified findings fail the gate. Baseline matching only classifies findings; it does not waive them. Also preserved the concurrently added draft ADR and aligned it with the final behavior.

Validation

Passed:

  • make build and make fmt
  • Workflow regeneration through the merge-resolution script: all 346 workflows compiled; generated locks match main.
  • go test ./pkg/cli -run '^(TestDryRunZizmor|TestDryRunScanner|TestScanner|TestActionlintShellcheckIdentity|TestZizmor|TestParseAndDisplayZizmor|TestDevelopmentCompileScanner|TestCollectDryRunBaseline)' -count=1
  • go test ./pkg/scanfindings ./pkg/workflow/actions ./pkg/workflow/aw -count=1
  • Native Zizmor 1.30.1 offline reproduction: replacing checkout credentials with a stable step ID is classified as Added=1, Existing=0.

Validation limitations:

  • Full Go lint did not finish: retries timed out, including a timeout/importer error for the standard time package. There is no passing full-lint claim for the final revision.
  • The combined repository gate's impacted unit tests passed, but custom lint still reports pre-existing diagnostics on unchanged lines.
  • go test ./pkg/workflow -run 'DryRun|Development' -count=1 fails in TestDryRunCompiledCacheMode and TestRepositoryWorkflowsCompileDryRun: incoming main's fail-closed policy rejects pre-steps/custom steps these tests expect to compile. The workflow sources and tests are unchanged from origin/main; this PR does not relax that policy.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@pelikhan
pelikhan marked this pull request as ready for review October 21, 2026 12:28
Copilot AI max review requested due to automatic review settings October 21, 2026 12:28
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Test Quality Sentinel · sonnet50 · 14.1 AIC · ⌖ 16.1 AIC · ◇ 6.64 AIC · ⊞ 12.7K · ◷

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Commit pushed: bef2fce

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Ponytail Reviewer

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Ponytail Reviewer · gpt56 · 9.48 AIC · ⌖ 5.34 AIC · ⊞ 14.8K · ◷

Generated by Ponytail Reviewer for #70868

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (596 new lines in pkg/) but does not link an Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/70868-use-actions-and-aw-asts-for-scanner-finding-identity.md. Review and complete it before merging.

The draft records the decision to stop using nearest-preceding raw YAML nodes and raw line text to identify scanner findings, and to use semantic Actions/AW AST anchors instead. Steps are matched by ID or unique name, otherwise by a content fingerprint. Unmapped or unparseable sources fall back to coordinates plus a file digest.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch. It was generated from the PR diff.
  2. Complete the missing sections: confirm the deciders, refine the rationale, and check the alternatives. The draft lists three: tuning the raw-YAML visitor, matching by coordinates and file fingerprint only, and matching steps by index.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-70868: Use Actions and AW ASTs to Anchor Scanner Finding Identities

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context: What is the problem? What forces are at play?
  • Decision: What did you decide? Why?
  • Alternatives Considered: What else could have been done?
  • Consequences: What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files named by PR number.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · opus55 · 28.7 AIC · ⌖ 40.5 AIC · ⊞ 1.9K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: COMMENT

No actionable blocking issues found in the changed lines. The review was static; repository code, tests, and build commands were not executed.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt50 · 3.13 AIC · ⌖ 0.598 AIC · ⊞ 1.6K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /codebase-design and /diagnosing-bugs. The new scannerSource abstraction (pkg/cli/compile_scanner_source.go) is a well-designed, deep module — single load/locate/identity interface hiding substantial AST-walking complexity, with strong regression coverage for the hard cases (aliases, flow mappings, duplicate names, AW frontmatter offsets). Two issues worth addressing, left as inline comments.

📋 Key Themes & Highlights

Key Themes

  • Caching defeated at one call site: withActionlintFindings in pkg/cli/actionlint.go uses the standalone scannerFindingIdentity/actionlintShellcheckIdentity wrappers per-issue, each allocating a fresh scannerSources map and re-parsing the file, unlike withScannerFindings which shares one map across all findings. This undercuts the per-file caching this PR specifically introduces.
  • Ambiguity short-circuit in locate: the column <= 0 conflict branch returns nil as soon as it sees any two conflicting candidates, which could discard a later unambiguous match depending on iteration order. Worth a 3+-location test case to confirm intended semantics.

Positive Highlights

  • ✅ Clear separation: indexAW / indexActions feed a single indexNode AST walker, keeping YAML-vs-Actions-vs-AW concerns well isolated.
  • ✅ Excellent regression test matrix covering 15 realistic edge cases (reordered flow maps, YAML aliases, duplicate step names, AW post-steps, dotted job IDs) — this is textbook /tdd coverage for a precision-sensitive matcher.
  • ✅ Conservative fallback (coordinate + file digest) when parsing fails, rather than guessing a match, matches the stated design goal.
  • ✅ Docs updated to explain the new matching semantics for future readers.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 72.3 AIC · ⌖ 15 AIC · ⊞ 10.2K
Comment /matt to run again

return make(scannerSources).findingIdentity(finding)
}

func (sources scannerSources) findingIdentity(finding scanfindings.Finding) string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Per-finding path reconstructs the file map from scratch: withActionlintFindings (pkg/cli/actionlint.go) calls scannerFindingIdentity and actionlintShellcheckIdentity separately for every issue, and each wrapper does make(scannerSources).findingIdentity(...), discarding the parsed/cached scannerSource after a single lookup. For a lock file with many findings this re-reads and re-parses the YAML/AW AST once per finding instead of once per file.

💡 Suggested fix

Plumb a single scannerSources map through the withActionlintFindings loop (as withScannerFindings already does) instead of relying on the standalone wrapper functions:

sources := make(scannerSources)
for _, issue := range native {
    ...
    identity := sources.findingIdentity(finding)
    if issue.Kind == "shellcheck" {
        if stableIdentity, ok := sources.shellcheckIdentity(issue); ok {
            identity = stableIdentity
        }
    }
}

This keeps the new per-file caching (sources.load) effective across the whole actionlint result set, which is exactly the optimization the PR introduces elsewhere.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withActionlintFindings now reuses one scannerSources map across the full actionlint result, retaining parsed source per file. Fixed in 9afe60d.

Comment thread pkg/cli/compile_scanner_source.go Outdated
if line == location.line && column > 0 && (column < location.column || line == location.endLine && column >= location.endColumn) {
continue
}
if found != nil && column <= 0 && found.path != location.path {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Ambiguity handling in locate looks fragile: when column <= 0 and two overlapping locations disagree on path, it returns nil for the whole call, discarding found even if a later, unambiguous location would have matched. Loop order (map iteration order of source.locations, which is a slice so it is deterministic, but the set of locations considered “found so far” depends on where in the loop the conflict is detected) makes the early-return behavior order-sensitive rather than based on the full candidate set.

💡 Why this matters

Consider three overlapping locations A, B, C at the same unresolved column where A and B conflict but C (visited later) is actually the correct unambiguous match — the current code bails out on the A/B conflict before ever considering C, even though the regression tests only exercise two-location conflicts (see TestScannerFindingIdentityAST/flow_mapping_columns_distinguish_fields). A test with 3+ overlapping scalars at column 0 would help pin down the intended semantics here (should ambiguity be “any conflicting pair” or “the final candidate set still conflicts”?).

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locate now evaluates the full set of best-ranked candidates before deciding whether their paths are ambiguous; added regressions for a later specific candidate and a genuinely tied conflict. Fixed in 9afe60d.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Identity-matching errors can suppress new scanner findings or incorrectly fail the differential gate.

5 open issues
What changed in this PR

Updates gh-aw’s scanner differentials to match semantic workflow content rather than raw YAML positions.

Changes:

  • Add Actions/AW source anchors and logical step identities.
  • Introduce source snapshots, conservative fallbacks, and stricter ShellCheck coordinate handling.
  • Add regression coverage and document matching behavior.
File Description
pkg/​cli/​compile_scanner_source.go Adds semantic source indexing and identity generation.
pkg/​cli/​compile_scanner_differential.go Integrates source-aware matching and ShellCheck handling.
pkg/​cli/​compile_scanner_differential_test.go Adds identity and fallback regression cases.
docs/​src/​content/​docs/​reference/​compilation-process.md Documents differential matching behavior.

Review effort: Max

Comment thread pkg/cli/compile_scanner_differential.go Outdated
if !found {
return "", false
}
message := issue.Message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The known actionlint ShellCheck prefix is stripped before parsing and identity construction. Added a prefixed-message regression through withActionlintFindings that preserves identity across a comment insertion. Fixed in 9afe60d.

return source.indexNode(value.Value, child, field)
case *ast.SequenceNode:
for index, child := range value.Values {
item, err := scannerSourceItem(semantic, index)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source index now validates authored needs order against lowered values and falls back to coordinate/file-digest identity when they differ. Added an unsorted-needs regression. Fixed in 9afe60d.

}
location := scannerSourceLocation{
path: path, value: value, digest: scannerValueDigest(string(content)), line: tk.Position.Line, column: tk.Position.Column,
endLine: tk.Position.Line, endColumn: tk.Position.Column + len([]rune(tk.Value)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scalar endpoints now use the trimmed authored token extent, including quotes. Regressions cover one-character and empty quoted strings, their characters, and exclusive endpoints. Fixed in 9afe60d.

Comment thread pkg/cli/compile_scanner_source.go Outdated
location.endLine = index + 1
}
} else if !key {
location.endLine += strings.Count(strings.TrimRight(tk.Origin, "\r\n"), "\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scalar source origin is trimmed before calculating its line span; a following indented comment is no longer attributed to the preceding scalar. Fixed in 9afe60d.

if line < location.line || line > location.endLine {
continue
}
if line == location.line && column > 0 && (column < location.column || line == location.endLine && column >= location.endColumn) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poutine and runner-guard now preserve unknown columns as zero; native adapter regressions verify it. Fixed in 9afe60d.

pelikhan and others added 2 commits October 21, 2026 06:00
Preserve fail-closed scanner gating alongside AST identity and partial-failure hardening.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Copilot AI requested a review from gh-aw-bot October 21, 2026 14:08
@pelikhan
pelikhan merged commit 8034da4 into main Oct 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants