Skip to content

Add comment support to approve-workflow-run safe output - #54504

Merged
pelikhan merged 1 commit into
mainfrom
copilot/update-safe-output-approve-workflow-run
Aug 21, 2026
Merged

Add comment support to approve-workflow-run safe output#54504
pelikhan merged 1 commit into
mainfrom
copilot/update-safe-output-approve-workflow-run

Conversation

Copilot AI commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

approve-workflow-run approved pending workflow runs silently, leaving no trace on the associated pull request that the run had started, and always requested pull-requests: write regardless of whether write access was actually needed.

Comment on approval

  • After a successful approval, posts a comment on each pull request associated with the run announcing it has started, linking to the run's HTML URL and including the standard generated attribution footer.
  • Comment failures are logged as warnings and never fail the approval itself.

New comment config field

  • Added comment (default true) to disable this behavior:
safe-outputs:
  approve-workflow-run:
    comment: false  # skip posting the PR comment

Least-privilege permissions

  • pull-requests: write is now only requested when comment is enabled (the default).
  • pull-requests: read is used when comment: false, matching prior behavior.

Docs & schema

  • Updated JSON schema, TypeScript config types, and reference docs (safe-outputs-pull-requests.md, safe-outputs-specification.md, glossary.md, frontmatter-full.md) to document the new field and permission model.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan August 21, 2026 11:28
@pelikhan
pelikhan marked this pull request as ready for review August 21, 2026 11:34
Copilot AI balanced review requested due to automatic review settings August 21, 2026 11:34
@github-actions

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

@github-actions

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

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

Copy link
Copy Markdown
Contributor

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

No ADR enforcement needed: PR #54504 does not have the implementation label and has 49 new lines of code in business logic directories (threshold: 100).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Ponytail review of PR #54504 complete. Reviewed the diff (approve_workflow_run.cjs comment posting, config plumbing, docs, schema, tests) against ponytail-review criteria. The new buildApprovalCommentBody/postApprovalComment functions mirror established patterns already used elsewhere (e.g. mark_pull_request_as_ready_for_review.cjs footer+comment composition), the config/permission plumbing is minimal and directly required, and the try/catch is the standard log-warning-dont-fail convention used across safe-output handlers. No dead code, reinvented stdlib, unneeded deps, speculative abstractions, or unnecessary flexibility found. Lean already. Ship.

Generated by Ponytail Reviewer for #54504

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-21T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - duplicate PR comments on retried approvals
  - missing idempotency for new side-effecting notification
files_reviewed:
  - actions/setup/js/approve_workflow_run.cjs
  - actions/setup/js/approve_workflow_run.test.cjs
  - pkg/workflow/approve_workflow_run.go
  - pkg/workflow/safe_output_handlers.go
  - pkg/workflow/safe_outputs_permissions_test.go
comment_count: 1

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 · 6.79 AIC · ⌖ 6.91 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.

Request changes

The new PR-comment side effect is not idempotent: repeated approval attempts for the same run will post duplicate "run started" comments, creating persistent noise on every linked pull request.

Blocking theme
  • The handler always calls issues.createComment after approval and never checks whether that run was already announced.
  • Because approval attempts are retryable across job/process boundaries, the duplicate-comment path is realistic rather than theoretical.
  • This needs a stable marker or update-in-place strategy before the feature is safe to merge.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 6.79 AIC · ⌖ 6.91 AIC · ⊞ 7K
Comment /review to run again

for (const pullRequest of run.pull_requests) {
const pullRequestNumber = parsePositiveInt(pullRequest.number);
if (pullRequestNumber !== undefined) {
await postApprovalComment(githubClient, pullRequestNumber, run.html_url);

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.

This change can spam every linked pull request with a duplicate “run started” comment on retries or repeated approvals, because it unconditionally calls issues.createComment and never checks whether the same run was already announced. That turns a benign re-run path into noisy, user-visible churn.

💡 Why this should be fixed

approve_workflow_run is explicitly retryable (processedCount is only local state, and a new job/process starts from zero), so the same run_id can be approved again after a transient failure or a repeated safe-output invocation. With the new behavior, every successful approval posts another identical PR comment.

That creates persistent review noise on the pull request and is especially bad for workflows associated with multiple PRs because the duplication fans out to each one.

Please make the comment idempotent per (pull_request_number, run_id), for example by searching for an existing marker before posting or by updating an existing bot comment instead of always creating a new one.

const marker = `<!-- gh-aw-approve-workflow-run:${runId} -->`;
const body = `${marker}
${buildApprovalCommentBody(run.html_url, pullRequestNumber)}`;
// list comments and skip/update when marker already exists

@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 /tdd — requesting changes on two correctness issues before merge.

📋 Key Themes & Highlights

Key Themes

  1. Zero-value trap on Comment boolbool zero-value is false, but the semantic default is true. Any ApproveWorkflowRunConfig constructed outside parseApproveWorkflowRunConfig (e.g., in tests or future callers) silently under-requests permissions. The existing Staged field uses *TemplatableBool for precisely this reason — Comment should follow the same pattern. This also affects the permission logic in safe_output_handlers.go which relies on the field value directly.

  2. buildApprovalCommentBody reads env vars directly — bypasses the config object and makes the function untestable without environment mutations. Pass values from config or as parameters instead.

  3. Weak test assertiontoContain("gh-aw-agentic-workflow") tests an internal footer fragment rather than the user-visible content. Prefer asserting the run URL and a meaningful phrase.

Positive Highlights

  • ✅ Comment failures are correctly downgraded to warnings — approval is never blocked
  • pull-requests: write vs read is properly conditioned on the config flag
  • ✅ Spec, schema, TypeScript types, and docs all updated in lock-step
  • ✅ Three focused test cases (default on, explicit off, error resilience)

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

@@ -8,6 +8,7 @@ var approveWorkflowRunLog = logger.New("workflow:approve_workflow_run")
type ApproveWorkflowRunConfig struct {
BaseSafeOutputConfig `yaml:",inline"`

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] Comment bool with omitempty and a semantic default of true is a zero-value trap. The Go zero value for bool is false, so any code that constructs ApproveWorkflowRunConfig{} directly (e.g., in tests or future callers) silently gets comment: false — wrong permissions (pull-requests: read instead of write) with no warning.

💡 Suggestion: use *bool like Staged does

Staged in BaseSafeOutputConfig already uses *TemplatableBool for exactly this reason. Apply the same pattern here:

Comment *bool `yaml:"comment,omitempty"`

// default in parseApproveWorkflowRunConfig:
defaultTrue := true
config.Comment = &defaultTrue

// permission logic:
commentEnabled := safeOutputs.ApproveWorkflowRun.Comment == nil || *safeOutputs.ApproveWorkflowRun.Comment

The test TestApproveWorkflowRunCommentDisabled already constructs ApproveWorkflowRunConfig{} with Comment: false — once you switch to *bool the zero value becomes nil (meaning "default true"), and the test would need to pass comment: &falseVal to express the opt-out correctly.

@copilot please address this.

*/
function buildApprovalCommentBody(runHtmlUrl, pullRequestNumber) {
const workflowName = process.env.GH_AW_WORKFLOW_NAME || "Workflow";
const workflowSource = process.env.GH_AW_WORKFLOW_SOURCE || "";

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] buildApprovalCommentBody reads process.env.GH_AW_WORKFLOW_NAME and GH_AW_WORKFLOW_SOURCE* directly, bypassing any config object or parameter injection. This makes the function hard to unit-test in isolation and breaks the pattern used by other handlers that receive these values through the handler config map.

💡 Suggestion

Pass the env-derived values through config (they're already set on the config object for other handlers), or at minimum accept them as parameters so the unit test for buildApprovalCommentBody can control them without environment mutations:

function buildApprovalCommentBody(runHtmlUrl, pullRequestNumber, { workflowName, workflowSource, workflowSourceURL }) { ... }

@copilot please address this.

expect(call.body).toContain(pendingPullRequestRun.html_url);
expect(call.body).toContain("gh-aw-agentic-workflow");
});

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 for "posts a comment by default" asserts toContain("gh-aw-agentic-workflow") — this is testing a fragment of the attribution footer rather than the semantically meaningful parts of the comment (the run URL, approval verb, PR number). If the footer text changes, the test breaks without any real regression; if the comment omits the run URL entirely the test still passes.

💡 Suggestion

Assert the parts that matter for correctness and user-visible value:

expect(call.body).toContain(pendingPullRequestRun.html_url); // run link is present ✅ (already)
expect(call.body).toMatch(/approved|started/i);              // action communicated
// remove the footer-fragment assertion or make it more specific

@copilot please address this.

// pull request associated with the approved run; otherwise read is sufficient.
pullRequestsLevel := PermissionRead
if safeOutputs.ApproveWorkflowRun != nil && safeOutputs.ApproveWorkflowRun.Comment {
pullRequestsLevel = PermissionWrite

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] The permission logic in safe_output_handlers.go checks safeOutputs.ApproveWorkflowRun.Comment (the bool field, default zero = false) rather than testing whether the config is nil before dereferencing:

if safeOutputs.ApproveWorkflowRun != nil && safeOutputs.ApproveWorkflowRun.Comment {

When Comment defaults to true but is stored as bool (zero = false), and a config struct is constructed without calling parseApproveWorkflowRunConfig, this condition evaluates to false even though the user intent is "default on". The permissions would then be pull-requests: read instead of write, silently under-requesting the scope needed to post the comment at runtime.

This is a direct consequence of the bool vs *bool issue noted on approve_workflow_run.go:9 — fixing that one will fix this too.

@copilot please address this.

@github-actions github-actions Bot mentioned this pull request Aug 21, 2026

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 configurable PR notifications after workflow-run approval and adjusts permissions accordingly.

Changes:

  • Posts attributed run-started comments by default.
  • Adds comment: false and least-privilege permissions.
  • Updates tests, schema, types, and documentation.
Show a summary per file
File Description
pkg/workflow/safe_outputs_permissions_test.go Tests conditional PR permissions.
pkg/workflow/safe_outputs_handler_registry.go Emits comment configuration.
pkg/workflow/safe_output_handlers.go Selects read or write permission.
pkg/workflow/approve_workflow_run.go Parses the new option.
pkg/workflow/approve_workflow_run_test.go Tests parsing and defaults.
pkg/parser/schemas/main_workflow_schema.json Defines the schema field.
docs/src/content/docs/specs/safe-outputs-specification.md Updates behavioral specification.
docs/src/content/docs/reference/safe-outputs-pull-requests.md Documents usage and permissions.
docs/src/content/docs/reference/glossary.md Updates glossary behavior.
docs/src/content/docs/reference/frontmatter-full.md Adds frontmatter reference.
actions/setup/js/types/safe-outputs-config.d.ts Adds the TypeScript option.
actions/setup/js/approve_workflow_run.test.cjs Tests comment behavior.
actions/setup/js/approve_workflow_run.cjs Posts approval comments.

Review details

  • Files reviewed: 12/13 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +139 to +140
const message = sanitizeContent(getRunStartedMessage({ workflowName, runUrl: runHtmlUrl, eventType: "pull request" }));
const footer = generateFooterWithMessages(workflowName, runUrl, workflowSource, workflowSourceURL, undefined, pullRequestNumber, undefined, undefined);
Comment on lines +131 to +137
pullRequestsLevel := PermissionRead
if safeOutputs.ApproveWorkflowRun != nil && safeOutputs.ApproveWorkflowRun.Comment {
pullRequestsLevel = PermissionWrite
}
return NewPermissionsFromMap(map[PermissionScope]PermissionLevel{
PermissionActions: PermissionWrite,
PermissionPullRequests: PermissionRead,
PermissionPullRequests: pullRequestsLevel,
Comment on lines 3189 to +3190
- `actions: write` - Workflow-run approval
- `pull-requests: read` - Listing associated pull request files
- `pull-requests: write` - Posting the run-started comment (when `comment` is enabled, the default); `pull-requests: read` is sufficient when `comment: false`

@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 implementation is clean and correct. Comment building uses the right URL in each position (approved run URL in the message, current agent run URL in the attribution footer), staged mode is unaffected, errors are downgraded to warnings and never fail the approval, and permission escalation is properly conditioned on the comment flag. Tests cover default-on, opt-out, and error-resilience cases.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 33.1 AIC · ⌖ 11.6 AIC · ⊞ 5.7K

@pelikhan
pelikhan merged commit 3aa28cf into main Aug 21, 2026
115 of 132 checks passed
@pelikhan
pelikhan deleted the copilot/update-safe-output-approve-workflow-run branch August 21, 2026 12:02
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.4

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