Skip to content

Fix JSON-array expressions in approve workflow run safe outputs - #53812

Merged
pelikhan merged 5 commits into
mainfrom
copilot/aw-failures-fix-malformed-json-emission
Aug 19, 2026
Merged

Fix JSON-array expressions in approve workflow run safe outputs#53812
pelikhan merged 5 commits into
mainfrom
copilot/aw-failures-fix-malformed-json-emission

Conversation

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Dynamic allowed-pull-requests values were emitted as quoted JSON strings. When an expression evaluated to a JSON array, its embedded quotes corrupted both safe-output configuration payloads and prevented the handler from starting.

  • Raw JSON expression emission

    • Add a dedicated templatable JSON-slice builder path.
    • Emit approve_workflow_run.allowed_pull_requests expressions as JSON values rather than JSON strings.
    • Apply the rendering consistently to handler-manager and config.json generation.
    {
      "allowed_pull_requests": ${{ needs.approval_allowlist.outputs.eligible_pull_request_numbers }}
    }
  • Regression coverage

    • Verify a substituted multi-element result (["123","456"]) parses as valid JSON.
    • Update integration coverage to validate compiled handler configuration after expression substitution.
  • Generated workflow

    • Recompile pr-sous-chef.lock.yml with the corrected configuration emission.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix malformed JSON emission for approve_workflow_run.allowed_pull_requests Fix JSON-array expressions in approve workflow run safe outputs Aug 18, 2026
Copilot AI requested a review from pelikhan August 18, 2026 20:21
@pelikhan
pelikhan marked this pull request as ready for review August 18, 2026 21:02
Copilot AI balanced review requested due to automatic review settings August 18, 2026 21:02
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

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 malformed safe-output JSON when dynamic pull-request allowlists evaluate to arrays.

Changes:

  • Adds JSON-aware expression marshalling.
  • Applies it to both safe-output configurations.
  • Adds regression tests and recompiles the workflow.
Show a summary per file
File Description
pkg/workflow/safe_outputs_handler_registry.go Uses JSON-slice emission for allowlists.
pkg/workflow/safe_outputs_config_runtime.go Uses expression-aware runtime marshalling.
pkg/workflow/safe_outputs_config_generation.go Uses expression-aware config generation.
pkg/workflow/compiler_safe_outputs_builder.go Adds JSON-expression handling.
pkg/workflow/approve_workflow_run_test.go Tests array substitution.
pkg/workflow/approve_workflow_run_integration_test.go Tests compiled handler configuration.
.github/workflows/pr-sous-chef.lock.yml Regenerates the workflow with unquoted allowlist emission.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Balanced

return b
}
if len(value) == 1 && isExpression(value[0]) {
b.config[key] = templatableJSONExpression(value[0])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5cf8bcc: AddTemplatableJSONSlice now wraps the expression with ${{ toJSON(...) }} before splicing it in, so an empty-string result serializes to "" (still valid JSON) instead of an empty token. Added a regression test covering the empty-result case.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 18, 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 Aug 18, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Lean already. Ship. Reviewed all non-generated diff (Go source + tests) for over-engineering per ponytail-review skill. The templatableJSONExpression marker/placeholder mechanism, templatableJSONExpressions visitor, and AddTemplatableJSONSlice builder are directly load-bearing for the fix (avoiding double-JSON-encoding an already-JSON expression) - no dead flexibility, no reinvented stdlib, no single-caller abstraction to inline, nothing shrinkable. Test additions are proportionate regression coverage.

Generated by Ponytail Reviewer for #53812

@github-actions

github-actions Bot commented Aug 18, 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

Comment Memory

reviewed_at: 2026-08-18T00:00:00Z
review_event: COMMENT
top_themes:
  - approve_workflow_run JSON-expression fix looks correct
  - regression coverage added for handler/runtime config rendering
files_reviewed:
  - .github/workflows/pr-sous-chef.lock.yml
  - pkg/workflow/approve_workflow_run_integration_test.go
  - pkg/workflow/approve_workflow_run_test.go
  - pkg/workflow/compiler_safe_outputs_builder.go
  - pkg/workflow/safe_outputs_config_generation.go
  - pkg/workflow/safe_outputs_config_runtime.go
  - pkg/workflow/safe_outputs_handler_registry.go
comment_count: 0

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 8.8 AIC · ⌖ 12.7 AIC · ⊞ 7K ·
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

I didn't find a blocking issue in the changed lines.

Why this passes review

The risky part of this change was the new raw-JSON expression path for allowed_pull_requests. The implementation updates both handler-manager and runtime config generation to share the same marshaling path, and the new tests cover both the literal-list case and expression substitution parsing back into valid JSON.

I did not find a correctness regression in the changed code after checking the diff and the added regression coverage.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 8.8 AIC · ⌖ 12.7 AIC · ⊞ 7K
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.

The fix correctly removes the wrapping double-quotes around the ${{ needs.approval_allowlist.outputs.eligible_pull_request_numbers }} expression in the GH_AW_SAFE_OUTPUTS_CONFIG env var. Previously, the JSON array was injected as a string value; now it is injected as a raw JSON array, which is what allowed_pull_requests expects. No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 19.2 AIC · ⌖ 8.77 AIC · ⊞ 5.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.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — commenting on two correctness concerns.

📋 Key Themes & Highlights

Key Themes

  • Magic-string collision risk: The fixed templatableJSONExpressionPrefix can theoretically appear inside a user-supplied expression, causing bytes.ReplaceAll to corrupt unrelated config values. Low probability but worth hardening.
  • Untested multi-element path: AddTemplatableJSONSlice silently falls through to []string when len(value) > 1. No test pins this behaviour.
  • Array shape coverage: Tests substitute string arrays (["123","456"]) but not numeric arrays ([123,456]), which are a plausible runtime shape.

Positive Highlights

  • ✅ Clean design: templatableJSONExpression with a custom MarshalJSON keeps the type system honest and the post-processing explicit.
  • ✅ Consistent wiring: both safe_outputs_config_generation.go and safe_outputs_config_runtime.go updated together — no split-brain risk.
  • ✅ Good regression test (TestApproveWorkflowRunAllowedPullRequestsExpressionEmitsJSONArray) covers the original corrupted-payload scenario end-to-end.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 48.5 AIC · ⌖ 10.3 AIC · ⊞ 7.8K
Comment /matt to run again

@@ -11,6 +14,14 @@ type handlerConfigBuilder struct {
config map[string]any

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] Collision risk with fixed placeholder prefix: __GH_AW_TEMPLATABLE_JSON_EXPRESSION__: is a static string embedded verbatim in the marshalled JSON before the find-and-replace pass. If a user expression ever contained this prefix, bytes.ReplaceAll would corrupt other values in the config payload.

💡 Safer approach: per-marshal index-based markers

Generate per-call placeholders indexed by position to make collisions impossible:

func marshalSafeOutputsConfig(config map[string]any) ([]byte, error) {
    expressions := templatableJSONExpressions(config)
    configJSON, err := json.Marshal(config)
    if err != nil {
        return nil, err
    }
    for i, expr := range expressions {
        placeholder, _ := json.Marshal(fmt.Sprintf("%s%d:", templatableJSONExpressionPrefix, i))
        configJSON = bytes.ReplaceAll(configJSON, placeholder, []byte(expr))
    }
    return configJSON, nil
}

And make MarshalJSON on templatableJSONExpression include the index (or use a UUID nonce). This prevents a user-supplied expression like ${{ env.__GH_AW_TEMPLATABLE_JSON_EXPRESSION__:something }} from poisoning replacements.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4851485: replaced the static sentinel-only placeholder with a per-instance placeholder that embeds a unique numeric id assigned via an atomic counter when the expression is created. This ties uniqueness to the expression instance rather than to marshal traversal order, so a user-supplied value containing the sentinel text can no longer collide with an internal placeholder.

b.config[key] = templatableJSONExpression(value[0])
return b
}
b.config[key] = 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.

[/tdd] The multi-element case in AddTemplatableJSONSlice — where len(value) > 1 and some elements are expressions — is untested and silently falls through to b.config[key] = value (a []string). This means mixed literal+expression slices would still be marshalled as a JSON array of quoted strings, not as raw JSON.

💡 Add a test and clarify the intent

Either document that multi-element expression slices are unsupported (and add a test that confirms the current []string path), or add handling:

func TestApproveWorkflowRunAllowedPullRequestsMixedLiterals(t *testing.T) {
    // two literal PR numbers — should stay as a JSON array of strings
    config := &ApproveWorkflowRunConfig{AllowedPullRequests: []string{"123", "456"}}
    handlerConfig := handlerRegistry["approve_workflow_run"](&SafeOutputsConfig{ApproveWorkflowRun: config})
    configJSON, err := marshalSafeOutputsConfig(map[string]any{"approve_workflow_run": handlerConfig})
    require.NoError(t, err)
    assert.Contains(t, string(configJSON), `"allowed_pull_requests":["123","456"]`)
}

Without this test the behaviour of the multi-element path is undocumented and could regress silently.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented the intent and added TestApproveWorkflowRunAllowedPullRequestsMixedLiteralsAndExpression in 4851485, confirming a multi-element slice stays a plain JSON array of strings rather than being treated as a templatable expression.


runtimeConfig := strings.ReplaceAll(string(configJSON), expression, `["123","456"]`)
var config map[string]map[string]any
require.NoError(t, json.Unmarshal([]byte(runtimeConfig), &config))

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 integration test substitutes the expression with ["123","456"] (strings), but the real runtime value from a GitHub Actions output would be an array of numbers (integers) when PR numbers are returned numerically. It would be worth verifying both ["123","456"] (strings) and [123,456] (numbers) parse without error, since the handler consuming allowed_pull_requests may type-assert the values.

💡 Extend the test
// also test numeric JSON array (the more common runtime shape)
runtimeConfigNumeric := strings.ReplaceAll(string(configJSON), expression, `[123,456]`)
var numericConfig map[string]map[string]any
require.NoError(t, json.Unmarshal([]byte(runtimeConfigNumeric), &numericConfig))
assert.Equal(t, []any{float64(123), float64(456)}, numericConfig["approve_workflow_run"]["allowed_pull_requests"])

This ensures the fix works for whichever array shape the upstream expression produces.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a numeric-array subtest in 4851485 (TestApproveWorkflowRunAllowedPullRequestsExpressionEmitsJSONArray/numeric_array) verifying [123,456] parses to []any{float64(123), float64(456)}, alongside the existing string-array case.

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/53812-templatable-json-expression-safe-outputs.md — review and complete it before merging.

🔒 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 — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you considered
  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-53812: Two-Pass Serialization for Templatable JSON Array Expressions

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

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

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 numbered by PR number (e.g., 53812-templatable-json-expression-safe-outputs.md for PR #53812).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 101.4 AIC · ⌖ 22.1 AIC · ⊞ 9.1K ·
Comment /review to run again

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…ceholders

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

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Addressed all four open review threads in 4851485: wrapped the expression in toJSON(...) for valid-JSON empty results, replaced the static placeholder with a per-instance unique id to eliminate collision risk, and added regression tests for the mixed-literal fallback, numeric arrays, and the empty-string edge case. Local go build, go vet, and make test-unit all pass; pr-sous-chef.lock.yml was recompiled.

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

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: bug · Risk: high
  • Score: 60/100 (impact 30, urgency 20, quality 10)
  • Recommended action: fast_track

Fixes safe-output config payload corruption from unescaped JSON-array expressions. Prior automated approval was dismissed — needs a fresh human review pass given the high-risk safe-output surface area.

Generated by 🔧 PR Triage Agent · auto · 58.5 AIC · ⌖ 2.15 AIC · ⊞ 8.3K ·

@pelikhan
pelikhan merged commit 75bc7df into main Aug 19, 2026
@pelikhan
pelikhan deleted the copilot/aw-failures-fix-malformed-json-emission branch August 19, 2026 00:50
@github-actions github-actions Bot mentioned this pull request Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[aw-failures] Fix malformed JSON emission for approve_workflow_run.allowed_pull_requests in safe-outputs config builder

3 participants