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

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@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

@copilot address the following outstanding work in one pass:

  1. Update this branch with the latest main using make merge-main, resolving any conflicts and preserving the intended changes.
  2. Review (pkg/cli/compile_scanner_differential.go:251): [/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. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)
  3. Review (pkg/cli/compile_scanner_source.go:339): [/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. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)
  4. Review (pkg/cli/compile_scanner_differential.go:324): Real actionlint messages start with shellcheck reported issue in this script: , as shown in actionlint_test.go:45. parseActionlintShellcheckMessage instead expects the first colon-delimited field to be an SC rule, so this ShellCheck-specific identity never handles that output. The generic fallback now hashes the entire script at the run: key, making an unchanged warning appear added after an unrelated script edit. Strip the known prefix before parsing, and add a regression using prefixed messages through withActionlintFindings. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)
  5. Review (pkg/cli/compile_scanner_source.go:230): actions.Parse and actions.Lower sort and deduplicate job.Needs (pkg/workflow/actions/graph.go:38–40), but this loop pairs the lowered values with source elements by index. For needs: [z, a], the source location of z is fingerprinted as a. Changing it to [y, a] therefore preserves the first element's identity and can suppress a finding on a changed dependency; other edits can also change an untouched finding's identity. Use source-ordered resolved values, or fall back when positional correspondence is not guaranteed, and add an unsorted-needs regression. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)
  6. Review (pkg/cli/compile_scanner_source.go:291): tk.Value is decoded text, not the authored scalar spelling. For TOKEN: 'x', this endpoint excludes the x; an empty quoted string has a zero-width span. Findings on those valid source positions therefore lose structural matching, and unrelated edits can make them appear newly added through the file-fingerprint fallback. Calculate the endpoint from the authored token extent, including quotes and escapes, and cover positions inside short quoted strings and at their endpoints. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)
  7. Review (pkg/cli/compile_scanner_source.go:318): A plain scalar's tk.Origin can include the following newline and indentation, such as " 2\n ". TrimRight(..., "\r\n") stops at the trailing space, so this counts the next line as part of the scalar. locate then bypasses column bounds on that line and can attach a finding to the preceding field, while line-only findings can become falsely ambiguous. Trim the surrounding whitespace before counting internal newlines, and cover an indented comment or sibling immediately after a plain value. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)
  8. Review (pkg/cli/compile_scanner_source.go:336): Poutine and runner-guard set Column: 1 for line-only findings (poutine.go:480, runner_guard.go:328). This treats that placeholder as an exact column and skips the intended field on indented lines. When lookup falls back to coordinates plus the file fingerprint, an unrelated edit makes existing medium/high findings appear added and fail the differential gate. Preserve unknown columns as 0 in both adapters; Finding.CompilerError already displays missing columns as 1. Add regression coverage using native adapter results. - Use Actions and AW ASTs for precise scanner differentials #70868 (comment)

Push the necessary fixes, reply to each listed review thread and resolve it when addressed. Ignore feedback already answered or resolved. Use the pr-finisher skill and stop when only human review or CI remains; do not trigger CI.

Sous-chef head: ab8814f
Sous-chef work: 1e0ac04ccf9711ca1d823ec8eb8d7a20c91df5c15e57fa3748819d1e02b8c0a8 736bbfff3e36d4e1b4f19337ce7003eefd00cdfa5dafd4353341d4e8c379568b 7a017a75cdd4f46e8d9ecb5545ea3ab36fbdc8dafb187bdd83b808035ce5f1cf 9fc21c18b61a0a1c0a0fc1613742be7f34991e1464712d70b588f93d94afd2fe a95d63c0f1ad95a64fd7780c721fca42b4d398c5e99d10646c342b80f5b1590a bd3ad157a86b02bc0a28d48855151a2862e73a5967531d7f9945d014d63673ce e8171b4e67aae732fb9911e61c267b62733b7879ff091afa23b3a9eb85470b9f
Sous-chef state: a58c2e28d2dae6976a6d3eae642829ddc18452161589a9bc72e03678958adc03

Generated by 👨‍🍳 PR Sous Chef · pi · haiku45 · 7.74 AIC · ⌖ 5.74 AIC · ⊞ 1.1K · ◷
Comment /souschef to run again

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
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.93.0

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