Skip to content

Fix on.bots/on.roles state leakage that corrupts workflow_run triggers#41018

Merged
pelikhan merged 6 commits into
mainfrom
copilot/fix-workflow-run-trigger
Jun 23, 2026
Merged

Fix on.bots/on.roles state leakage that corrupts workflow_run triggers#41018
pelikhan merged 6 commits into
mainfrom
copilot/fix-workflow-run-trigger

Conversation

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

workflow_run triggers were being corrupted when on.bots or on.roles appeared in the same on: block: list-item commenting logic leaked extension-array state and could strip effective workflow_run.workflows/types, which then surfaced as the non-empty-workflows validation error in newer versions.

  • Compiler fix: isolate on: extension array state per event section

    • In commentOutProcessedFieldsInOnSection, reset top-level extension array trackers before activating a new event section (pull_request, issues, discussion, issue_comment, deployment_status, workflow_run).
    • This prevents bots/roles tracking from carrying into workflow_run parsing/commenting.
  • Regression coverage: workflow_run + sibling auth filters

    • Added direct on: comment-out tests for:
      • workflow_run followed by bots
      • bots before workflow_run
      • roles before workflow_run
      • roles: all before workflow_run
    • Assertions verify workflow_run.workflows and types remain intact/uncommented.
  • Validation coverage: strict-mode compile path

    • Added strict-mode workflow tests showing workflow_run with non-empty workflows passes when sibling bots or roles: all are present.
  • Docs: supported composition made explicit

    • Updated trigger reference with an explicit workflow_run + bots example to lock in the supported pattern.
on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
  bots: ["dependabot[bot]"]

Copilot AI and others added 3 commits June 23, 2026 13:44
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix corruption of workflow_run trigger with bots or roles keys Fix on.bots/on.roles state leakage that corrupts workflow_run triggers Jun 23, 2026
Copilot AI requested a review from pelikhan June 23, 2026 14:02
@pelikhan pelikhan marked this pull request as ready for review June 23, 2026 14:08
Copilot AI review requested due to automatic review settings June 23, 2026 14:08

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.

Pull request overview

Fixes a compiler bug in gh-aw where on.bots / on.roles list-item comment-out state could leak across sibling on: entries and corrupt workflow_run trigger fields (notably workflows / types). The PR also adds regression tests and documentation to codify the supported trigger composition.

Changes:

  • Reset top-level on: extension-array trackers when entering a new event section during frontmatter comment-out processing, preventing cross-section state leakage.
  • Add regression tests covering workflow_run combined with sibling bots / roles in both direct comment-out behavior and strict-mode compilation validation.
  • Document the supported workflow_run + bots composition in the trigger reference.
Show a summary per file
File Description
pkg/workflow/frontmatter_extraction_yaml.go Resets top-level on: array-tracking state on event-section transitions to prevent state leakage into workflow_run.
pkg/workflow/compiler_draft_test.go Adds comment-out regression tests ensuring workflow_run.workflows/types remain intact with sibling bots/roles.
pkg/workflow/workflow_run_validation_test.go Adds strict-mode validation coverage for workflow_run with sibling bots / roles: all.
docs/src/content/docs/reference/triggers.md Documents workflow_run combined with top-level bots:/roles: authorization filters.
.github/workflows/smoke-claude-on-copilot.lock.yml Updates compiled workflow output; includes a detection job if: condition change that should be confirmed as intentional.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 1

Comment on lines 1379 to 1383
needs:
- activation
- agent
if: >
always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true')
if: always() && needs.agent.result != 'skipped'
runs-on: ubuntu-latest
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (133 new lines across pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/41018-reset-on-section-array-trackers.md — review and complete it before merging. It was generated from the PR diff and captures the decision to reset on: extension-array trackers at event-section boundaries rather than restructuring the line-based scanner.

📋 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 decision rationale, refine the alternatives, and add any context the AI couldn't infer (e.g. why a structural YAML-scoping rewrite is out of scope for now)
  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-41018: Reset on: Extension-Array Trackers at Event-Section Boundaries

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. Even for a focused bug fix, recording that the state-leak was patched with a per-section reset (rather than a parser rewrite) tells future contributors the fragile flat-flag scanner was a known, deliberate trade-off — not an oversight.

📋 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 numbered by PR number (e.g., 41018-reset-on-section-array-trackers.md for PR #41018).

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 95.5 AIC · ⌖ 10.4 AIC · ⊞ 6.7K ·

@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.

REQUEST_CHANGES — The bots/roles leakage is correctly fixed, but the same exit-check-ordering bug exists for inSkipAuthorAssociations and inSkipIfCheckFailing, leaving live corruption paths open. The call-site reset pattern also makes future regressions easy.

Blocking issues

Incomplete tracker reset (high)

inSkipAuthorAssociations is missing from resetOnArrayTrackers(). Its commenting rule (inSkipAuthorAssociations && lineIndent > 2 → comment) would silence every sub-field of workflow_run (workflows:, types:, branches:) when skip-author-associations: precedes workflow_run: in the same on: block — identical in mechanism to the bug being fixed. inSkipIfCheckFailing similarly endangers workflow_run.branches list items. Both are missing from the reset and from the new test matrix.

Fragile call-site pattern (medium)

resetOnArrayTrackers() is manually invoked at 6 separate call sites instead of being folded into activateEventSection(). Any future event addition that skips the call silently reintroduces the class of bug this PR is meant to eliminate.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • patchdiff.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "patchdiff.githubusercontent.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · 88.5 AIC · ⌖ 7.77 AIC · ⊞ 5.1K

inRolesArray = false
inBotsArray = false
inLabelsArray = false
inNeedsArray = false

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.

Incomplete fix: inSkipAuthorAssociations (and inSkipIfCheckFailing) are not cleared, leaving an identical workflow_run corruption path still open.

💡 Details and fix

The exit-check that normally clears inSkipAuthorAssociations (lines 431–438) fires after the event-section detection block, which does continue — so it is skipped entirely for the header line itself. If skip-author-associations: appears before workflow_run: in the same on: block:

on:
  skip-author-associations:
    - FIRST_TIME_CONTRIBUTOR
  workflow_run:
    workflows: ["CI"]
    types: [completed]

inSkipAuthorAssociations stays true through the entire workflow_run section. The commenting rule at line 585 is:

} else if inSkipAuthorAssociations && lineIndent > 2 {
    shouldComment = true

Since workflows:, types:, and branches: all have indent > 2, every sub-field of workflow_run is commented out — the same corruption this PR claims to fix for bots/roles.

inSkipIfCheckFailing has the same ordering problem; its rule matches - list items, which would hit workflow_run.branches entries.

Fix — expand resetOnArrayTrackers:

resetOnArrayTrackers := func() {
    inSkipRolesArray = false
    inSkipBotsArray = false
    inRolesArray = false
    inBotsArray = false
    inLabelsArray = false
    inNeedsArray = false
    // These share the same exit-check-ordering bug:
    inSkipAuthorAssociations = false
    inSkipIfCheckFailing = false
    inSkipIfNoMatch = false
    inSkipIfMatch = false
}

Also add a regression test with skip-author-associations before workflow_run to prevent this from regressing silently.

// the permission comment-out logic.
if !inOnPermissions && !inOnSteps && !inSkipAuthorAssociations {
if (lineIndent == 2 || lineIndent == 4) && trimmedLine == "pull_request:" {
resetOnArrayTrackers()

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.

Fragile call-site pattern: resetOnArrayTrackers() is invoked manually at 6 separate sites instead of being embedded inside activateEventSection(), making it a regression trap for every future event addition.

💡 Suggested refactor

Move the reset into activateEventSection (line 167) so it fires automatically:

activateEventSection := func(section string, indent int) {
    // Reset all top-level on: extension state so no sibling section leaks in.
    inSkipRolesArray = false
    inSkipBotsArray = false
    inRolesArray = false
    inBotsArray = false
    inLabelsArray = false
    inNeedsArray = false
    inSkipAuthorAssociations = false  // see separate comment
    inSkipIfCheckFailing = false
    // ...

    inPullRequest = section == "pull_request"
    // ... rest unchanged
}

Then remove the 6 resetOnArrayTrackers() call sites and delete the standalone function. Any future activateEventSection("check_run", ...) call will automatically be safe, with no secondary ritual required.

@github-actions

Copy link
Copy Markdown
Contributor

{
"body": "### 🧪 Test Quality Sentinel Report\n\n✅ Test Quality Score: 90/100 — Excellent\n\n> Analyzed 6 test(s) across 2 modified test files: 6 design tests (behavioral contracts), 0 implementation tests, 0 guideline violations. Test inflation penalty applies due to large test-to-production line ratio, but is expected for a regression fix.\n\n

\n📊 Metrics & Test Classification (6 tests analyzed)\n\n| Metric | Value |\n|--------|-------|\n| New/modified tests analyzed | 6 |\n| ✅ Design tests (behavioral contracts) | 6 (100%) |\n| ⚠️ Implementation tests (low value) | 0 (0%) |\n| Tests with error/edge cases | 6 (100%) |\n| Duplicate test clusters | 0 |\n| Test inflation detected | YES (see note) |\n| 🚨 Coding-guideline violations | 0 |\n\n| Test | File | Classification | Issues Detected |\n|------|------|----------------|-----------------|\n| TestCommentOutProcessedFieldsInOnSection — row: workflow_run followed by bots keeps workflows and types uncommented | pkg/workflow/compiler_draft_test.go:~356 | ✅ Design | — |\n| TestCommentOutProcessedFieldsInOnSection — row: bots before workflow_run do not comment workflow_run list items | pkg/workflow/compiler_draft_test.go:~371 | ✅ Design | — |\n| TestCommentOutProcessedFieldsInOnSection — row: roles before workflow_run do not comment workflow_run list items | pkg/workflow/compiler_draft_test.go:~388 | ✅ Design | — |\n| TestCommentOutProcessedFieldsInOnSection — row: roles all before workflow_run keeps workflow_run intact | pkg/workflow/compiler_draft_test.go:~405 | ✅ Design | — |\n| workflow_run validation table — row: workflow_run with sibling bots - strict mode - should pass | pkg/workflow/workflow_run_validation_test.go:~120 | ✅ Design | — |\n| workflow_run validation table — row: workflow_run with sibling roles all - strict mode - should pass | pkg/workflow/workflow_run_validation_test.go:~148 | ✅ Design | — |\n\nGo: 6 (*_test.go); JavaScript: 0. No other languages detected.\n\nTest inflation note: compiler_draft_test.go added 70 lines and workflow_run_validation_test.go added 47 lines (117 total), while the production fix in frontmatter_extraction_yaml.go added only 16 lines. The 7.3:1 ratio technically exceeds the 2:1 threshold and triggers a 10-point penalty. This is the expected pattern for a regression fix — the production change was a targeted bug fix while the tests enumerate multiple ordering scenarios to prevent recurrence.\n\n
\n\n### Verdict\n\n> ✅ Check passed. 0% implementation tests (threshold: 30%). All 6 new test rows are high-value behavioral regression tests that directly verify the fixed state-leakage invariant: bots/roles processing state must not corrupt workflow_run field handling. Deleting any of these tests would allow the original bug to regress silently.\n\nReferences:\n- §28032382734"
}

🧪 Test quality analysis by Test Quality Sentinel · 99.2 AIC · ⌖ 15.2 AIC · ⊞ 8.3K ·

@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.

✅ Test Quality Sentinel: 90/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 6 new test rows are high-value behavioral regression tests covering the fixed bots/roles state-leakage invariant.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@github-actions

Copy link
Copy Markdown
Contributor

Please add the ADR link to the PR body and rerun the gate.

Generated by 👨‍🍳 PR Sous Chef · 45.4 AIC · ⌖ 0.975 AIC · ⊞ 17.3K ·

@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 /tdd, /diagnose, and /zoom-out — requesting changes on test coverage gaps.

📋 Key Themes & Highlights

Key Issue: Regression Tests Don't Reproduce the Bug

The root cause is that activateEventSection() calls end with continue, which bypasses the indent-based array-tracker deactivation logic (lines ~464–528). This means stale inBotsArray / inRolesArray flags persist into the next event section's children — but only when those children use multi-line list syntax (- item). Lines using inline syntax (workflows: ["CI"]) are never matched by the inBotsArray && strings.HasPrefix(trimmedLine, "-") check.

All four new unit tests use inline array syntax and all two new validation tests have the extension field after workflow_run:. Neither configuration reproduces the actual failure path, so the tests would pass on the unfixed code.

What Needs to Change

  1. Add a unit test with multi-line workflows:/types: syntax — specifically with bots: (or roles:, labels:, needs:) before workflow_run: and multi-line arrays inside workflow_run:.
  2. Flip the extension-field ordering in the validation tests — put bots:/roles: before workflow_run: to match the real production scenario.

Positive Highlights

  • ✅ Fix is surgical and correct — resetOnArrayTrackers() is exactly the right leverage point.
  • ✅ Good coverage of all 4 sibling-ordering permutations conceptually — just needs the inline→multi-line syntax correction.
  • ✅ Docs example locks in the supported workflow_run + bots composition pattern.
  • ✅ Resetting inLabelsArray and inNeedsArray proactively covers those extension fields too.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 102.4 AIC · ⌖ 9.67 AIC · ⊞ 6.5K

description: "Should not let bots array state leak into workflow_run fields",
},
{
name: "bots before workflow_run do not comment workflow_run list items",

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.

[/tdd] The test input uses inline array syntax workflows: ["CI"] and types: [completed], which does not exercise the failure path — the bug only activates when inBotsArray is stale and a line starts with -. This test would pass even without the fix.

💡 Suggested additional test case

Add a variant with multi-line list syntax so the test actually fails without the fix:

{
    name: "bots before workflow_run with multi-line arrays do not comment workflow_run list items",
    input: `on:
  bots:
    - dependabot
  workflow_run:
    workflows:
      - CI
    types:
      - completed
  workflow_dispatch:`,
    expected: `on:
  # bots: # Bots processed as bot check in pre-activation job
    # - dependabot # Bots processed as bot check in pre-activation job
  workflow_run:
    workflows:
      - CI
    types:
      - completed
  workflow_dispatch:`,
    description: "Should not comment out multi-line workflow_run.workflows/types items when bots precedes workflow_run",
},

This directly triggers the inBotsArray && strings.HasPrefix(trimmedLine, "-") code path and will fail on the unfixed code.

workflow_run:
workflows: ["build"]
types: [completed]
branches:

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.

[/tdd] In both new validation test cases, bots: / roles: appears after workflow_run: in the YAML, so inBotsArray is only set after workflow_run's children have already been processed — the original failure mode required the extension field to come before workflow_run:.

💡 Flip the ordering to cover the real failure path

Change the frontmatter so bots: precedes workflow_run: and use multi-line workflows: syntax, e.g.:

---
on:
  bots:
    - dependabot
  workflow_run:
    workflows:
      - build
    types:
      - completed
    branches:
      - main
tools:
  github:
    toolsets: [repos]
---

This ordering (extension field first) is what causes the stale-flag leakage and is the scenario that was broken in production.

workflowRunIndent = -1
}
}
// resetOnArrayTrackers clears top-level on: extension array state when a new

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.

[/diagnose] The comment accurately describes what the reset does, but the why — the mechanism that makes it necessary — is the real insight worth capturing. Without this context, a future developer adding a new event section might omit the call.

💡 Suggested comment wording
// resetOnArrayTrackers clears top-level on: extension-array state (inBotsArray,
// inRolesArray, etc.) before entering a new event section.
// This is required because each activateEventSection() call ends with "continue",
// which bypasses the indent-based deactivation logic at lines ~464-528. Without
// the explicit reset here, stale flags from a preceding bots:/roles:/labels: block
// would cause that event section's list items (e.g. workflow_run.workflows - CI)
// to be incorrectly commented out as bot/role/label entries.

- agent
if: >
always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true')
if: always() && needs.agent.result != 'skipped'

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.

[/zoom-out] This if: simplification is unrelated to the on.bots/on.roles state-leakage fix. Removing needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' means the downstream job now runs whenever the agent wasn't skipped, regardless of whether it produced any output — a behavioural change with its own risk surface. Would be cleaner to land this independently with explicit reasoning.

…overage, revert smoke if

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

All review threads addressed in commit Fix review feedback: inline reset into activateEventSection, expand coverage, revert smoke if:

Code changes (frontmatter_extraction_yaml.go)

  • Removed the standalone resetOnArrayTrackers helper and its 6 call sites; the reset is now embedded directly in activateEventSection so every future event addition is automatically safe.
  • Expanded the reset to cover inSkipIfCheckFailing, inSkipAuthorAssociations, inSkipIfNoMatch, and inSkipIfMatch — fixing the real corruption path where skip-if-check-failing: list items before workflow_run: would cause workflow_run.workflows/types dash-entries to be incorrectly commented out.
  • Updated the comment to explain the mechanism: each activateEventSection call ends with continue, bypassing the indent-based deactivation blocks further down, so the reset must happen here explicitly.

Tests (compiler_draft_test.go)

  • Added a multi-line workflows:/types: variant for the bots before workflow_run case — this directly exercises the inBotsArray && strings.HasPrefix(trimmedLine, "-") code path that the inline-array tests did not cover.
  • Added a skip-if-check-failing before workflow_run test (multi-line arrays) to lock in the newly fixed path.

Validation tests (workflow_run_validation_test.go)

  • Flipped bots/roles: all to appear before workflow_run: and switched to multi-line workflows: syntax — this is the ordering that caused stale-flag leakage in production.

Smoke workflow (.github/workflows/smoke-claude-on-copilot.lock.yml)

  • Reverted the unintentional if: broadening; restored needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' gating so the detection job is skipped on noop runs.

@pelikhan pelikhan merged commit 18af8d6 into main Jun 23, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/fix-workflow-run-trigger branch June 23, 2026 16:32
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.

3 participants