Skip to content

Add PR-targeting support to create_check_run (including target: "*" flows)#38237

Open
Copilot wants to merge 5 commits into
mainfrom
copilot/add-pr-targeting-behavior
Open

Add PR-targeting support to create_check_run (including target: "*" flows)#38237
Copilot wants to merge 5 commits into
mainfrom
copilot/add-pr-targeting-behavior

Conversation

Copilot AI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

create_check_run previously attached checks using event SHA heuristics and could not target arbitrary pull requests the way create-pull-request-adjacent tools can. This change adds explicit PR targeting semantics so check runs can be attached to the intended PR head SHA in wildcard and explicit-target workflows.

  • Config surface: add PR target support

    • safe-outputs.create-check-run now accepts target (triggering, *, or explicit PR number) in compiler config.
    • Handler config emission now includes target for create_check_run.
    • Non-string target values in YAML frontmatter now produce a warning log instead of being silently ignored.
  • Runtime behavior: resolve PR first, then attach check run

    • create_check_run now resolves target PR via shared target resolution logic when target is configured.
    • For resolved PR targets, it fetches the current PR head SHA via the API (rather than the event payload) to ensure the SHA is always up-to-date even if the PR was force-pushed between the triggering event and handler execution.
    • Hard target resolution failures now emit core.error (red annotation); expected soft skips emit core.info.
    • In staged mode with a target configured, the pulls.get API call is skipped entirely; the staged preview includes the resolved PR number.
    • Preserves existing fallback behavior (PR payload head SHA → GITHUB_SHAcontext.sha) when no target is configured.
    • Staged-mode returns consolidated into a single code path (both target and non-target flows share one if (isStaged) block after SHA resolution).
  • Permissions: least-privilege pull-requests: read when targeting

    • When create-check-run has a target configured, the compiled workflow now requests pull-requests: read in addition to contents: read + checks: write, so the pulls.get call does not 403 in least-privilege environments.
  • Tool contract: per-call PR selection in wildcard mode

    • create_check_run tool schema documents pull_request_number and aliases (pr_number, pr, pull_number) for target: "*" usage.
    • All alias fields now carry x-synonyms entries for camelCase discovery; pull_request_number's x-synonyms lists the actual alias property names the runtime checks.
    • Description clarifies how create-check-run target routing behaves in PR-targeted workflows.
  • Specification: create_check_run added to Safe Outputs MCP Gateway Specification (v1.23.0)

    • Added a full W3C-style Section 7.3 type definition for create_check_run in docs/src/content/docs/specs/safe-outputs-specification.md.
    • Covers MCP tool schema, operational semantics (SHA resolution order, staged-mode behavior, error-handling policy), configuration parameters table, dual permission profile (with/without target), and example frontmatter + agent message.
  • Coverage updates

    • Added JS coverage for wildcard, explicit, and triggering PR target resolution paths.
    • Added JS coverage for pulls.get API error recovery (including assertion that core.error is called) and staged-mode-with-target no-API-call behavior.
    • Added compiler-level coverage to assert target is emitted for create_check_run handler config, with a sentinel to prevent false-positive test passes.
# workflow frontmatter
safe-outputs:
  create-check-run:
    target: "*"
{"type":"create_check_run","pull_request_number":876,"conclusion":"failure","title":"3 issues found","summary":"See findings"}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add PR-targeting support to create_check_run Add PR-targeting support to create_check_run (including target: "*" flows) Jun 9, 2026
Copilot AI requested a review from pelikhan June 9, 2026 23:59
@pelikhan pelikhan marked this pull request as ready for review June 10, 2026 01:04
Copilot AI review requested due to automatic review settings June 10, 2026 01:04
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ failed during design decision gate check.

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel completed test quality analysis.

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

Adds explicit pull request targeting to the create_check_run safe-output so checks can be attached to the intended PR head SHA (including target: "*" per-call routing), instead of relying on event SHA heuristics.

Changes:

  • Emit target in compiled handler config for create_check_run and parse it from workflow frontmatter.
  • Extend the create_check_run tool schema to accept pull_request_number plus aliases for wildcard targeting.
  • Update the JS handler to resolve the PR first and use pull_request.head.sha when target is configured; add JS + compiler-level tests.
Show a summary per file
File Description
pkg/workflow/safe_outputs_handler_registry.go Emits target into handler config for create_check_run.
pkg/workflow/js/safe_outputs_tools.json Documents pull_request_number + aliases for wildcard PR targeting.
pkg/workflow/create_check_run.go Adds Target to CreateCheckRunConfig and parses it from frontmatter.
pkg/workflow/compiler_safe_outputs_config_test.go Adds compiler coverage asserting target is emitted for create_check_run.
actions/setup/js/safe_outputs_tools.json Mirrors tool-schema updates for the setup action bundle.
actions/setup/js/create_check_run.test.cjs Adds runtime tests for wildcard + explicit PR target resolution paths.
actions/setup/js/create_check_run.cjs Implements PR resolution + PR head SHA attachment when target is configured.

Copilot's findings

Tip

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

  • Files reviewed: 7/7 changed files
  • Comments generated: 3

Comment thread actions/setup/js/create_check_run.cjs Outdated
Comment on lines +139 to +143
githubClient.rest.pulls.get({
owner,
repo,
pull_number: pullRequestNumber,
}),

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 the first commit. Added NewPermissionsContentsReadChecksWritePRRead() to permissions_factory.go (contents:read + checks:write + pull-requests:read) and updated the create-check-run PermissionBuilder in safe_output_handlers.go to use it when safeOutputs.CreateCheckRun.Target != "".

Comment on lines +40 to +46
// Parse target (optional PR targeting mode)
if target, exists := configMap["target"]; exists {
if targetStr, ok := target.(string); ok {
checkRunConfig.Target = targetStr
createCheckRunLog.Printf("Using check run target: %s", targetStr)
}
}

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. Added an else branch that calls createCheckRunLog.Printf("Warning: create-check-run target value %v is not a string and will be ignored", target) in create_check_run.go when the type assertion fails.

Comment on lines +1488 to +1506
for _, step := range steps {
if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") {
parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ")
if len(parts) == 2 {
jsonStr := strings.TrimSpace(parts[1])
jsonStr = strings.Trim(jsonStr, "\"")
jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"")

var config map[string]map[string]any
err := json.Unmarshal([]byte(jsonStr), &config)
require.NoError(t, err)

checkRunConfig, ok := config["create_check_run"]
require.True(t, ok)

target, ok := checkRunConfig["target"]
require.True(t, ok)
assert.Equal(t, "*", target)
}

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. Added foundHandlerConfig := false before the loop, set it to true inside the matching branch, and added require.True(t, foundHandlerConfig, "Expected GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG in generated steps") after the loop — matching the pattern already used by TestHandlerConfigUpdatePullRequestUpdateBranch.

@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, /zoom-out, and /grill-with-docs — no blocking correctness issues, but four coverage gaps and one schema inconsistency are worth addressing.

�� Key Themes & Highlights

Key Themes

  • Test coverage gaps: The pulls.get catch block and the target: "triggering" code path are both exercised by production code but have no tests. The Go compiler test uses a conditional loop pattern that can pass vacuously if the emitted YAML changes shape.
  • target: "triggering" makes a redundant API call: functionally equivalent to no-target, but routes through resolveTarget + pulls.get instead of reading context.payload.pull_request.head.sha directly — introducing extra latency and a new failure mode without an obvious benefit.
  • Schema alias consistency: pr_number, pr, pull_number lack x-synonyms entries that similar aliases carry elsewhere in the schema.

Positive Highlights

  • ✅ Clean reuse of the shared resolveTarget helper — new behavior is consistent with how other PR-targeting handlers work
  • ✅ Good error handling structure: target resolution failures, missing SHA, and API errors each return distinct, descriptive error objects
  • ✅ Three meaningful new JS tests covering wildcard success, wildcard missing-number error, and explicit numeric target
  • ✅ End-to-end wiring (Go struct → YAML parser → handler registry → JS runtime) is complete and tightly scoped
  • additionalProperties: false preserved in schema — new fields are additive and safe

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 382.9 AIC · ⌖ 13.8 AIC

Comment thread actions/setup/js/create_check_run.cjs Outdated
return { success: false, error: msg };
}
core.info(`Using PR #${pullRequestNumber} head SHA ${headSha} (target=${checkRunTarget})`);
} catch (error) {

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 pulls.get catch block has no test coverage — if the GitHub API returns a 404 (non-existent PR) or a 403 (auth failure), this error path silently goes untested.

💡 Suggested test case
it("returns error when pulls.get throws", async () => {
  mockGithub.rest.pulls = {
    get: async () => { throw new Error("Not Found"); },
  };
  const { main } = require("./create_check_run.cjs");
  const handler = await main({ max: 10, target: "*" });
  const result = await handler(
    { type: "create_check_run", pull_request_number: 42, conclusion: "success", title: "T", summary: "S" },
    {}
  );
  expect(result.success).toBe(false);
  expect(result.error).toContain("Failed to resolve pull request");
});

This path is the primary recovery surface for transient network errors and stale PR references.

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 "returns error when pulls.get throws (e.g. 404 or 403)" test in the target resolution describe block. It stubs mockGithub.rest.pulls.get to throw new Error("Not Found") and asserts result.success === false with result.error containing "Failed to resolve pull request".

expect(result.success).toBe(true);
expect(capturedParams.head_sha).toBe("target-pr-sha-7");
});
});

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] target: "triggering" is a documented valid value but has no test — its code path (resolveTarget → pulls.get in a PR event context) is distinct from both the no-target fallback and the wildcard/explicit paths.

💡 Suggested test case
it("resolves triggering PR context when target is \"triggering\"", async () => {
  mockContext.eventName = "pull_request";
  mockContext.payload = { pull_request: { number: 99 } };
  mockGithub.rest.pulls = {
    get: async ({ pull_number }) => ({
      data: { number: pull_number, head: { sha: "triggering-pr-sha" } },
    }),
  };
  let capturedParams;
  mockGithub.rest.checks.create = makeChecksCreate(p => { capturedParams = p; });

  const { main } = require("./create_check_run.cjs");
  const handler = await main({ max: 10, target: "triggering" });
  const result = await handler(
    { type: "create_check_run", conclusion: "success", title: "T", summary: "S" },
    {}
  );
  expect(result.success).toBe(true);
  expect(capturedParams.head_sha).toBe("triggering-pr-sha");
});

Without this test the target: "triggering" branch in create_check_run.cjs is invisible to the test suite.

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 "resolves triggering PR context when target is 'triggering'" test. It sets mockContext.eventName = "pull_request" with payload.pull_request.number = 99, stubs pulls.get to return head.sha = "api-sha-99", and asserts that capturedParams.head_sha equals the API-fetched SHA (not the payload SHA), confirming the refreshed-SHA intent documented in the code comment.

"description": "Pull request number to attach the check run to when the workflow uses `create-check-run: target: \"*\"` (or equivalent explicit PR targeting). This is the numeric ID from the GitHub URL (e.g., 876 in github.com/owner/repo/pull/876).",
"x-synonyms": ["pullRequestNumber"]
},
"pr_number": {

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.

[/grill-with-docs] The three alias fields (pr_number, pr, pull_number) are missing x-synonyms entries, unlike similar aliases in add_comment and the primary pull_request_number field which has "x-synonyms": ["pullRequestNumber"]. Agents that resolve field names via synonym matching may not recognize camelCase variants of these aliases.

💡 Suggested additions
"pr_number": {
  "type": ["number", "string"],
  "description": "Alias for pull_request_number. Prefer pull_request_number in new calls.",
  "x-synonyms": ["prNumber"]
},
"pr": {
  "type": ["number", "string"],
  "description": "Alias for pull_request_number. Prefer pull_request_number in new calls.",
  "x-synonyms": ["pullRequest", "pull"]
},
"pull_number": {
  "type": ["number", "string"],
  "description": "Alias for pull_request_number. Prefer pull_request_number in new calls.",
  "x-synonyms": ["pullNumber"]
}

The same change applies to pkg/workflow/js/safe_outputs_tools.json (the identical copy).

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 x-synonyms to all three alias fields in both actions/setup/js/safe_outputs_tools.json and pkg/workflow/js/safe_outputs_tools.json: pr_number → ["prNumber"], pr → ["pullRequest", "pull"], pull_number → ["pullNumber"].

var steps []string
compiler.addHandlerManagerConfigEnvVar(&steps, workflowData)

for _, step := range steps {

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 for _, step := range steps loop is conditional — if addHandlerManagerConfigEnvVar generates no step containing GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG, the loop body never executes, all assertions are skipped, and the test passes vacuously. This is the same pattern as TestHandlerConfigClosePullRequestTargetRepo so it is pre-existing, but worth locking down.

💡 Suggested fix

Add a sentinel after the loop to assert the step was actually found:

found := false
for _, step := range steps {
    if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") {
        found = true
        // ... existing assertions ...
    }
}
require.True(t, found, "expected a step containing GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG")

This prevents the test from silently passing if the config env var is accidentally removed from the emitted YAML.

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. Added foundHandlerConfig := false before the loop, set to true inside the matching branch, and require.True(t, foundHandlerConfig, "Expected GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG in generated steps") after the loop.

const headSha = prHeadSha || process.env.GITHUB_SHA || context.sha;
let headSha = "";

if (checkRunTarget) {

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] When target: "triggering" is configured and the context is a pull_request event, this branch calls resolveTarget (returns context.payload.pull_request.number), then makes a pulls.get API call to fetch the head SHA. The else branch below reads the same SHA directly from context.payload.pull_request.head.sha with no API call. This means target: "triggering" is functionally equivalent to no target, but slower and with an extra failure mode.

💡 Two options

Option A — Short-circuit for triggering in PR context:
Inside the checkRunTarget branch, after a successful resolveTarget, check targetResult.contextType === "pull request" and whether the payload already has the SHA; if so, skip the API call and use context.payload.pull_request.head.sha directly.

Option B — Document the semantic difference:
If the API call is intentional (e.g., to get a refreshed SHA in case the PR was force-pushed between trigger and handler), add a brief comment explaining that intent, and add a test that explicitly asserts the API call is made rather than the payload SHA used.

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.

Took Option B. Added a code comment above the pulls.get call explaining the intent: "We intentionally go through the API even when the context payload already carries a SHA (e.g. target: "triggering" on a pull_request event) so that we always use the most recent head in case the PR was force-pushed between the triggering event and when this handler runs." Also added a test that asserts capturedParams.head_sha equals the API-fetched SHA rather than the payload SHA.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

🔶 Test Quality Score: 58/100 — Needs Improvement

Analyzed 4 tests: 3 design, 1 implementation, 1 guideline note (missing assertion messages).

📊 Metrics & Test Classification (4 tests analyzed)
Metric Value
New/modified tests analyzed 4
✅ Design tests (behavioral contracts) 3 (75%)
⚠️ Implementation tests (low value) 1 (25%)
Tests with error/edge cases 1 (25%)
Duplicate test clusters 0
Test inflation detected Yes — Go: 41 test lines vs ~10 production lines (4.1:1 ratio)
🚨 Coding-guideline violations 1 (missing assertion messages in Go)

Test Classification Details

Test File Classification Issues Detected
it("uses pull_request_number when target is '*'") actions/setup/js/create_check_run.test.cjs ✅ Design Happy path only; no API-failure edge case
it("returns error when target is '*' and pull_request_number is missing") actions/setup/js/create_check_run.test.cjs ✅ Design
it("uses explicit target PR number from config") actions/setup/js/create_check_run.test.cjs ✅ Design Happy path only
TestHandlerConfigCreateCheckRunTarget pkg/workflow/compiler_safe_outputs_config_test.go ⚠️ Implementation Parses internal env-var JSON; no error cases; 4 bare assertions lack messages

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration) ✅
  • 🟨 JavaScript (*.test.cjs): 3 tests (vitest)
⚠️ Flagged Tests — Requires Review (1 issue)

⚠️ TestHandlerConfigCreateCheckRunTarget (pkg/workflow/compiler_safe_outputs_config_test.go)

Classification: Implementation test

Issue: The test calls addHandlerManagerConfigEnvVar, then manually scans the resulting step strings for GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG, splits on : , unescapes JSON, and asserts the target key equals "*". This tests the internal serialization format of an env var rather than the observable runtime outcome (the check run being created against the correct PR head SHA). It would survive a behavioral regression (wrong SHA used at runtime) while breaking on any innocent refactoring of the env var format.

What design invariant does this test enforce? That addHandlerManagerConfigEnvVar encodes the target field in the internal JSON payload — an implementation detail, not a user-visible contract.

What would break if deleted? Only if the JSON key name or serialization format changed. A real behavioral regression (targeting the wrong PR) would slip through.

Missing assertion messages — all four calls violate the codebase convention requiring a descriptive third argument:

// Current (violates guideline)
require.NoError(t, err)
require.True(t, ok)  // ×2
assert.Equal(t, "*", target)

// Corrected
require.NoError(t, err, "handler config JSON should unmarshal cleanly")
require.True(t, ok, "handler config should contain create_check_run key")
require.True(t, ok, "create_check_run config should contain target key")
assert.Equal(t, "*", target, "create_check_run target should match configured value")

Suggested improvements:

  1. Prefer a higher-level assertion: compile the workflow end-to-end and assert the resulting YAML (or the Go struct) rather than manually parsing env var strings.
  2. Add an error case: what happens when Target is an empty string or an invalid value?
  3. Add descriptive messages to all assertions (see above).

Verdict

Check passed. 25% of new tests are implementation tests (threshold: 30%). Score of 58/100 reflects test inflation (Go: 4.1:1) and missing assertion messages — consider the flagged improvements before follow-up PRs.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls or data-format details
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §27246111619

🧪 Test quality analysis by Test Quality Sentinel · 460.8 AIC · ⌖ 19.8 AIC ·

@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: 58/100. Test quality is acceptable — 25% of new tests are implementation tests (threshold: 30%). Note: test inflation (4.1:1 Go ratio) and missing assertion messages reduce the score; see the flagged test in the comment above for suggested improvements.

@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 — one blocking test issue, one incorrect schema annotation.

🔍 Findings summary

🔴 High — TestHandlerConfigCreateCheckRunTarget can silently pass without validating

compiler_safe_outputs_config_test.go — The new test is missing the foundHandlerConfig guard that every analogous test in the same file uses. If addHandlerManagerConfigEnvVar never emits a GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG step, or the step format changes, the loop body is never entered, all assertions are skipped, and the test passes green. This gives completely false confidence that target serialization works.

🟡 Medium — x-synonyms: ["pullRequestNumber"] is wrong

safe_outputs_tools.json — The runtime (resolveTarget) checks pr_number, pr, pull_number — all snake_case. pullRequestNumber (camelCase) is not handled anywhere. The annotation will mislead tools/agents that read x-synonyms to discover valid aliases.

🔵 Low — Staged mode makes a live pulls.get() API call that gets discarded

create_check_run.cjs — When checkRunTarget is set, the PR head SHA is fetched before the isStaged check. In staged mode the SHA is then thrown away without being included in the preview log. Either skip the fetch in staged mode or surface the SHA in the log.

🔎 Code quality review by PR Code Quality Reviewer · ⌖ 13.9 AIC

assert.Equal(t, "*", target)
}
}
}

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 silently passes without asserting anything: if addHandlerManagerConfigEnvVar produces no step matching GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG (or the format changes), the loop body is never entered and the test exits with zero assertions.

💡 Suggested fix

Every equivalent test in this file (e.g., TestHandlerConfigUpdatePullRequestUpdateBranch, TestHandlerConfigClosePullRequestTargetRepo) tracks whether the config step was found and asserts it at the end:

var steps []string
compiler.addHandlerManagerConfigEnvVar(&steps, workflowData)

foundHandlerConfig := false   // ADD
for _, step := range steps {
    if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") {
        foundHandlerConfig = true  // ADD
        parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ")
        if len(parts) == 2 {
            // ... existing JSON parsing ...
        }
    }
}
require.True(t, foundHandlerConfig, "Expected GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG in generated steps")  // ADD

Without this guard, a future regression that stops serializing target into the env var would go completely undetected because require.True(t, ok) and assert.Equal(t, "*", target) inside the if len(parts) == 2 block are simply never reached.

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. Added foundHandlerConfig := false sentinel before the loop, set to true inside the matching branch, and require.True(t, foundHandlerConfig, "Expected GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG in generated steps") after the loop.

"pull_request_number": {
"type": ["number", "string"],
"description": "Pull request number to attach the check run to when the workflow uses `create-check-run: target: \"*\"` (or equivalent explicit PR targeting). This is the numeric ID from the GitHub URL (e.g., 876 in github.com/owner/repo/pull/876).",
"x-synonyms": ["pullRequestNumber"]

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.

x-synonyms annotation points to a field the runtime never reads: pullRequestNumber (camelCase) is listed as the synonym, but resolveTarget in safe_output_helpers.cjs only checks item.pull_request_number || item.pr_number || item.pr || item.pull_number — all snake_case. No camelCase alias is handled anywhere.

💡 Suggested fix

Either:

  1. Fix the annotation to list the actual aliases that resolveTarget supports:
"x-synonyms": ["pr_number", "pr", "pull_number"]
  1. Or add camelCase handling in resolveTarget:
const pullRequestNumberField =
  item.pull_request_number ||
  item.pullRequestNumber ||   // ADD
  item.pr_number ||
  item.pr ||
  item.pull_number;

The same change applies to pkg/workflow/js/safe_outputs_tools.json which is an identical copy.

As-is, any tool that reads x-synonyms to discover equivalent aliases will advertise pullRequestNumber as valid, but passing it will silently be ignored (filtered by additionalProperties: false before reaching the runtime).

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 pull_request_number's x-synonyms to list the actual alias property names the runtime checks: ["pr_number", "pr", "pull_number"]. Also added x-synonyms for each alias field. Applied to both actions/setup/js/safe_outputs_tools.json and pkg/workflow/js/safe_outputs_tools.json.

Comment thread actions/setup/js/create_check_run.cjs Outdated
try {
const { data: pullRequest } = await withRetry(
() =>
githubClient.rest.pulls.get({

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.

Live API call executes even in staged mode: githubClient.rest.pulls.get() runs before the isStaged check at line 182. In staged mode, the PR head SHA is fetched, used for nothing (the staged preview log does not include it), then discarded.

💡 Suggested fix

Either guard the API call behind !isStaged, or include the resolved SHA in the staged preview output so the call is justified:

// Option A: skip the API call in staged mode
if (isStaged) {
  logStagedPreviewInfo(`Would create check run "${checkRunName}" targeting PR #${pullRequestNumber} with conclusion=${conclusion}, title="${resolvedTitle}"`);
  processedCount++;
  return { success: true, staged: true, ... };
}
// Only fetch the SHA when we actually need it
const { data: pullRequest } = await withRetry(...);
// Option B: include the SHA in the staged preview (already fetched, make it useful)
logStagedPreviewInfo(
  `Would create check run "${checkRunName}" on PR #${pullRequestNumber} @ ${headSha} with conclusion=${conclusion}, title="${resolvedTitle}"`
);

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 with Option A. The staged-mode early return now happens inside the checkRunTarget branch before the pulls.get call, so no live API call is made in staged mode. The staged preview message includes the PR number for context. Added a test "skips pulls.get API call and returns staged preview when target is set and staged mode is active" that asserts pullsGetCalled === false.

Comment thread actions/setup/js/create_check_run.cjs Outdated
supportsIssue: false,
});
if (!targetResult.success) {
core.warning(targetResult.error);

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.

core.warning used for hard failures (shouldFail === true) instead of core.error: when target resolution fails with a genuine error — invalid PR number, target: "*" with no pull_request_number in the message — the log shows a yellow warning instead of a red error annotation, even though the workflow will ultimately fail. Compare assign_to_agent.cjs which correctly branches on shouldFail.

💡 Suggested fix

Mirror the pattern from assign_to_agent.cjs:

if (!targetResult.success) {
  if (targetResult.shouldFail) {
    core.error(targetResult.error);   // hard failure → red annotation
  } else {
    core.info(targetResult.error);    // expected soft skip → informational
  }
  return {
    success: false,
    error: targetResult.error,
    skipped: !targetResult.shouldFail,
  };
}

As written, a misconfigured target (e.g., target: "abc" — not a number) logs a warning, the outer processor later emits a core.error and core.setFailed, and the reader has to hunt for why the step failed after seeing only a warning about it.

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. Now uses core.error when targetResult.shouldFail is true (hard failure → red annotation) and core.info when it is false (expected soft skip → informational), mirroring the assign_to_agent.cjs pattern. The skipped field is now !targetResult.shouldFail.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI and others added 2 commits June 10, 2026 01:34
- Add NewPermissionsContentsReadChecksWritePRRead for target-configured check runs
- Conditionally emit pull-requests:read when create-check-run has a target set
- Warn when target config value is non-string rather than silently ignoring
- Fix TestHandlerConfigCreateCheckRunTarget false-positive by tracking and asserting foundHandlerConfig

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…synonyms, and tests

- Use core.error/core.info instead of core.warning for target resolution failures
- Skip pulls.get API call in staged mode; include PR number in staged preview message
- Add comment explaining why target: "triggering" always goes via API (refreshed SHA)
- Add tests: pulls.get throw, target: "triggering", staged mode skips API call
- Fix x-synonyms: pull_request_number lists alias property names; add synonyms for all aliases
- Apply x-synonyms fix to both safe_outputs_tools.json copies

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

Copy link
Copy Markdown
Collaborator

@copilot do deep review of the changes, fix security. YAGNI/SOLID/DRY

…turns, strengthen test

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

Copilot AI commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

@copilot do deep review of the changes, fix security. YAGNI/SOLID/DRY

Done in commit 55c55e0. Three concrete issues found and fixed:

Security — missing core.error() in pulls.get catch (annotation visibility)
The catch block returned { success: false } without calling core.error(), so 403/404 failures never produced a red annotation in the Actions UI. Added core.error(msg) before the return, consistent with every other hard-failure path. The pulls.get error test now also asserts core.error was called (not just that result.error is set).

DRY — two duplicate staged-mode return blocks
There was one if (isStaged) { return staged; } inside the if (checkRunTarget) branch and another identical one after the SHA-resolution block. Both returned the same { success, staged, previewInfo } shape. Consolidated to a single block after SHA resolution by hoisting resolvedPrNumber to the outer scope and guarding the pulls.get call with if (!isStaged) instead of an early return.

Log accuracy — misleading "Creating check run" log in staged mode
In the non-target staged path, core.info("Creating check run … on …@sha …") fired before the staged guard and then immediately returned a staged preview—never actually creating anything. Moving the staged check before that log line means it only fires when a check run is genuinely about to be created.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot update safe outputs specification using w3c spec writer

…ification v1.23.0

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

Copilot AI commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update safe outputs specification using w3c spec writer

Done in commit 4b53f0e. Added create_check_run to docs/src/content/docs/specs/safe-outputs-specification.md as a full Section 7.3 type definition (v1.23.0), covering:

  • MCP tool schema (conclusion, title, summary, text, pull_request_number with aliases)
  • Operational semantics — SHA resolution order, staged-mode behavior, error-handling policy
  • Configuration parameters table (name, target, max, staged, output.title, output.summary)
  • Dual permission profile: contents:read + checks:write without target; adds pull-requests:read when target is set
  • Example frontmatter and example agent message
  • Changelog entry for 1.23.0

@github-actions

Copy link
Copy Markdown
Contributor

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · 104.6 AIC · ⌖ 1.48 AIC · ⊞ 17.3K ·

@github-actions

Copy link
Copy Markdown
Contributor

@copilot add the missing test guard and fix the x-synonyms annotation, then request another review.

Generated by 👨‍🍳 PR Sous Chef · 104.6 AIC · ⌖ 1.48 AIC · ⊞ 17.3K ·

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