Skip to content

feat: add commit-id config option to pin review attribution to the reviewed commit - #48738

Open
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-pr-review-commit-id-attribution
Open

feat: add commit-id config option to pin review attribution to the reviewed commit#48738
pelikhan with Copilot wants to merge 3 commits into
mainfrom
copilot/fix-pr-review-commit-id-attribution

Conversation

Copilot AI commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Under workflow_run triggers, safe_outputs runs after agent — if a new commit lands during the agent's run, pulls.get() returns the new HEAD sha and the posted review is attributed to a commit the agent never saw, making inline comments appear fabricated or stale.

Changes

Go compiler

  • Added CommitId string field (commit-id in YAML) to SubmitPullRequestReviewConfig and CreatePullRequestReviewCommentsConfig
  • Handler registry emits commit_id via AddIfNotEmpty into the runtime JSON config for both handlers

JS runtime

  • pr_review_buffer.cjs: Extended ReviewContext with optional commitId; submitReview() uses reviewContext.commitId || pullRequest.head.sha as commit_id in pulls.createReview()
  • submit_pr_review.cjs: Reads config.commit_id as pinnedCommitId and threads it through both the payload and fetched-PR paths of setReviewContextOnBuffer
  • create_pr_review_comment.cjs: Same — reads config.commit_id and passes commitId to buffer.setReviewContext()

Usage

safe-outputs:
  submit-pull-request-review:
    commit-id: ${{ needs.eligibility.outputs.head_sha }}
  create-pull-request-review-comment:
    commit-id: ${{ needs.eligibility.outputs.head_sha }}

When commit-id is set, the review is pinned to that SHA. GitHub will automatically mark the review as outdated if HEAD has since moved — which is the correct behavior — rather than attributing a stale review to the new commit.

…reate-pull-request-review-comment

Adds a `commit-id` configuration option to both `submit-pull-request-review`
and `create-pull-request-review-comment` safe output handlers. When set, the
configured SHA is used as the `commit_id` for `pulls.createReview()` instead
of the live PR head SHA fetched during `safe_outputs`.

This prevents attribution drift in workflow_run scenarios where a new commit
may be pushed to the PR during the agent's run. Workflow authors can pin the
review to the exact commit that was checked out and reviewed:

```yaml
safe-outputs:
  submit-pull-request-review:
    commit-id: ${{ needs.eligibility.outputs.head_sha }}
```

Changes:
- Go compiler: add CommitId field to SubmitPullRequestReviewConfig and
  CreatePullRequestReviewCommentsConfig, parse commit-id from YAML config,
  emit commit_id in handler registry JSON
- JS runtime: read config.commit_id in submit_pr_review.cjs and
  create_pr_review_comment.cjs, store as commitId on ReviewContext
- JS buffer: use reviewContext.commitId || pullRequest.head.sha as
  commit_id when calling pulls.createReview()
- Tests: add unit tests for all new behavior in Go and JavaScript

Closes #48661

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix PR review commit ID attribution issue feat: add commit-id config option to pin review attribution to the reviewed commit Jul 28, 2026
Copilot AI requested a review from pelikhan July 28, 2026 21:51
@pelikhan
pelikhan marked this pull request as ready for review July 28, 2026 21:53
Copilot AI review requested due to automatic review settings July 28, 2026 21:53
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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

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 commit pinning so pull request reviews target the commit actually reviewed instead of a newer PR head.

Changes:

  • Adds commit-id compiler configuration and runtime propagation.
  • Uses the pinned SHA when submitting reviews.
  • Adds Go and JavaScript tests for commit selection.
Show a summary per file
File Description
pkg/workflow/submit_pr_review.go Parses review commit configuration.
pkg/workflow/submit_pr_review_footer_test.go Tests compiler configuration handling.
pkg/workflow/safe_outputs_handler_registry.go Emits commit IDs to handlers.
pkg/workflow/create_pr_review_comment.go Parses comment commit configuration.
actions/setup/js/submit_pr_review.test.cjs Tests submit-handler pinning.
actions/setup/js/submit_pr_review.cjs Propagates pinned review commits.
actions/setup/js/pr_review_buffer.test.cjs Tests commit selection during submission.
actions/setup/js/pr_review_buffer.cjs Submits reviews against pinned commits.
actions/setup/js/create_pr_review_comment.test.cjs Tests comment-handler pinning.
actions/setup/js/create_pr_review_comment.cjs Propagates pins into comment buffers.

Review details

Tip

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

  • Files reviewed: 10/10 changed files
  • Comments generated: 6
  • Review effort level: Medium

Comment on lines +116 to +119
// Parse optional commit-id override
if commitId, exists := configMap["commit-id"]; exists {
if commitIdStr, ok := commitId.(string); ok && commitIdStr != "" {
config.CommitId = commitIdStr
Comment on lines +56 to +59
// Parse optional commit-id override
if commitId, exists := configMap["commit-id"]; exists {
if commitIdStr, ok := commitId.(string); ok && commitIdStr != "" {
prReviewCommentsConfig.CommitId = commitIdStr
Comment on lines +302 to +305
// Use the pinned commit ID when configured, falling back to the live PR head SHA.
// A pinned commitId ensures the review is attributed to the commit the agent actually
// reviewed, preventing attribution drift when new commits are pushed during the run.
const resolvedCommitId = commitId || pullRequest.head.sha;
Comment on lines 474 to +477
owner: repoParts.owner,
repo: repoParts.repo,
pull_number: pullRequestNumber,
commit_id: pullRequest.head.sha,
commit_id: resolvedCommitId,
Comment on lines +47 to +50
// Optional pinned commit SHA. When set, overrides the live PR head SHA so that the
// review is attributed to the commit the agent actually reviewed, not whatever head
// the PR has at post time (which may differ under workflow_run triggers).
const pinnedCommitId = typeof config.commit_id === "string" && config.commit_id.trim() ? config.commit_id.trim() : null;
Comment on lines +46 to +49
// Optional pinned commit SHA. When set, overrides the live PR head SHA so that the
// review is attributed to the commit the agent actually reviewed, not whatever head
// the PR has at post time (which may differ under workflow_run triggers).
const pinnedCommitId = typeof config.commit_id === "string" && config.commit_id.trim() ? config.commit_id.trim() : null;

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

Review: feat: add commit-id config option

The implementation is well-structured and the test coverage is thorough. One blocking issue found:

Go naming convention violationCommitId should be CommitID (Go requires initialisms to be fully uppercased per the Go wiki). This affects:

  • CommitId field in submit_pr_review.go and create_pr_review_comment.go
  • Local variables commitId / commitIdStr in both Go files
  • Test assertions using CommitId in submit_pr_review_footer_test.go

Everything else — the JS runtime changes, the fallback logic, the test cases — looks correct and well thought-out.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 30.6 AIC · ⌖ 4.67 AIC · ⊞ 5K

Footer *string `yaml:"footer,omitempty"` // Controls when to show footer in PR review body: "always" (default), "none", or "if-body" (only when review has body text)
AllowedEvents []string `yaml:"allowed-events,omitempty"` // Optional list of allowed review event types: APPROVE, COMMENT, REQUEST_CHANGES. If omitted, all event types are allowed.
SupersedeOlderReviews bool `yaml:"supersede-older-reviews,omitempty"` // When true, dismisses older same-workflow REQUEST_CHANGES reviews after a replacement review is posted.
CommitId string `yaml:"commit-id,omitempty"` // Optional commit SHA to use as the reviewed commit. Pins the review to the commit the agent actually reviewed, preventing attribution drift when new commits are pushed during the run.

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.

Go naming convention: CommitIdCommitID

Go naming conventions require initialisms (ID, URL, HTTP, SHA) be fully uppercased. CommitId should be CommitID. The same applies to the identical field in create_pr_review_comment.go and local variables commitId/commitIdStr.

This will be flagged by golint/staticcheck.

// current
CommitId string `yaml:"commit-id,omitempty"`

// correct
CommitID string `yaml:"commit-id,omitempty"`

@copilot please address this.

@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: Request changes — the commit-id pinning applies a single global value to every PR in multi-PR/registry mode, mislabeling attribution instead of fixing it.

💡 Themes and rationale

The core idea (pin commit_id to the SHA the agent actually reviewed) is sound and well-tested for the single-PR case. However, both submit_pr_review.cjs and create_pr_review_comment.cjs support multi-PR registry mode (target: "*"), and in that mode the same workflow-level commit-id string is spread into every PR's buffer context regardless of which PR is being processed. Since commit-id is intended to come from a single upstream job output tied to one specific PR/run, applying it uniformly across multiple PRs in one workflow invocation will silently attribute reviews/comments to the wrong commit for every PR except the one the value was actually computed for — this is worse than the bug the PR is fixing, and it is untested (the only registry-mode test with commit_id uses a single PR).

Additionally, there's no validation that commit-id is a plausible SHA or belongs to the target PR's history before being sent to the GitHub API, so a stale/misconfigured upstream value fails opaquely at request time rather than with a clear config error.

Recommend: gate commit-id pinning to single-PR-target configurations (warn/skip in registry multi-PR mode), or support a per-PR mapping, plus add multi-PR regression tests before merge.

🔎 Code quality review by PR Code Quality Reviewer · sonnet50 · 52.3 AIC · ⌖ 7.26 AIC · ⊞ 7.4K
Comment /review to run again

Comments that could not be inline-anchored

actions/setup/js/submit_pr_review.cjs:287

A single workflow-level commit-id is pinned identically onto every PR resolved via target: &quot;*&quot;, silently attributing the wrong commit to all but one PR when multiple PRs are reviewed in the same run.

<details><summary>💡 Details and fix</summary>

pinnedCommitId is computed once from config.commit_id (a single string) and spread unconditionally into every buffer's review context in both setReviewContextOnBuffer call sites (payload PR and fetched PR), regardless of which PR is being p…

actions/setup/js/create_pr_review_comment.cjs:25

Same global-pinning bug as submit_pr_review.cjs: one commit-id is applied to every PR comment buffer in multi-PR/registry mode, mislabeling comments on all but one PR.

<details><summary>💡 Details and fix</summary>

This file's test suite (see describe(&quot;create_pr_review_comment.cjs — registry mode (multiple reviews)&quot;)) confirms multi-PR usage is supported, yet pinnedCommitId is derived once from top-level config.commit_id and unconditionally spread into every buffer's review context …

pkg/workflow/submit_pr_review.go:430

No validation that commit-id is a well-formed SHA or actually reachable on the target PR before it flows into pulls.createReview().

<details><summary>💡 Details and fix</summary>

The parser accepts any non-empty string via configMap[&quot;commit-id&quot;] with only a non-empty check, no format validation (e.g. 40/7-char hex) and no ancestry check against the PR branch. Since this value is expected to come from expressions like ${{ needs.eligibility.outputs.head_sha }}, a misconfigured or stale …

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 69/100 — Acceptable

Analyzed 17 test(s): 17 design, 0 implementation, 0 violation(s).

📊 Metrics (17 tests)
Metric Value
Analyzed 17 total (Go: 8, JS: 9)
✅ Design 17 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5 (29%)
Duplicate clusters 0
Inflation Go: 9.7:1, JS: 5.9:1 (both >2:1 threshold)
🚨 Violations 0

Test Breakdown

Test File Classification Coverage
TestParseCommitIdConfig[1-4] submit_pr_review_footer_test.go design_test Config parsing (both handlers), empty defaults
TestParseCommitIdConfig[5-7] submit_pr_review_footer_test.go design_test JSON handler config emission, conditional logic
TestParseCommitIdConfig[8] submit_pr_review_footer_test.go design_test GitHub expression support
Registry + direct payload tests submit_pr_review.test.cjs design_test Multi-buffer coordination, PR payload path
API fetch test submit_pr_review.test.cjs design_test workflow_run event path, API coordination
Backwards compat tests (2x) submit_pr_review.test.cjs design_test No config, empty string edge cases
Comment handler tests (2x) create_pr_review_comment.test.cjs design_test Pinned config, defaults
Buffer tests (2x) pr_review_buffer.test.cjs design_test Pinned vs live SHA, fallback behavior
⚠️ Inflation Analysis

Go Test File:

  • New lines: 174 (test) vs ~18 (production parsing code)
  • Ratio: 9.7:1 (threshold: 2:1) — flagged
  • Justification: JSON serialization validation requires verbose setup/parse/assert pattern

JavaScript Test Files:

  • New lines: 234 total (test) vs ~40 (production coordination code)
  • Ratio: 5.9:1 (threshold: 2:1) — flagged
  • Justification: Integration tests with mock setup, context verification

Verdict: Inflation flagged but justified. The tests validate critical design invariants:

  • Configuration flow from YAML → handler context
  • Pinned vs live SHA separation at submission layer
  • Backwards compatibility (no config = undefined)
  • Multiple event paths (pull_request, workflow_run)

No redundant or duplicated test patterns detected.

Design Invariants Covered

✅ Config parsing (submit-pull-request-review, create-pull-request-review-comment)
✅ Conditional emission (present when set, omitted when empty)
✅ Backwards compatibility (undefined when not configured)
✅ Dynamic values (GitHub expressions)
✅ Registry coordination (multiple buffers)
✅ Event path variants (pull_request payload, workflow_run API fetch)
✅ Commit pinning vs live HEAD SHA separation
✅ Handler config serialization (JSON environment variable)
✅ API submission uses pinned SHA (not live HEAD)
✅ Fallback to current HEAD when unpinned

Verdict

APPROVED. 0% implementation tests (threshold: 30%). No hard violations. All 17 new tests are design tests covering comprehensive feature matrix and edge cases.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 26.2 AIC · ⌖ 9.8 AIC · ⊞ 8.1K ·
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.

✅ Test Quality Sentinel: 69/100. All 17 new tests are design tests (0% implementation, threshold: 30%). No hard violations. Inflation flagged but justified by JSON config validation necessity.

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/48738-pin-review-attribution-to-reviewed-commit.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 could not 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-48738: Pin PR Review Attribution to the Reviewed Commit

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., 48738-pin-review-attribution-to-reviewed-commit.md for this PR).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 71.7 AIC · ⌖ 10.1 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /codebase-design and /grill-with-docs — requesting changes on a naming violation and a docs gap.

📋 Key Themes & Highlights

Key Themes

  • Go naming (CommitIdCommitID): Both submit_pr_review.go and create_pr_review_comment.go use CommitId where Go conventions require CommitID. This will trigger golint/revive findings.
  • Duplicated parsing logic: The commit-id extraction block is copy-pasted verbatim into two separate parse functions — a shared helper would prevent drift.
  • Missing reference docs: commit-id is a new user-facing YAML option but isn't documented in the spec or glossary alongside the other options (allowed-events, supersede-older-reviews).

Positive Highlights

  • ✅ Excellent test coverage — 408 lines of new tests across all code paths (payload PR, fetched PR, registry mode, empty config, whitespace-only config)
  • setReviewContext is correctly write-once, so there's no risk of a later handler call overwriting a previously pinned commit ID
  • AddIfNotEmpty correctly gates emission so empty commit_id is never sent to the JS runtime
  • ✅ Fallback to pullRequest.head.sha is clean and well-commented
  • ✅ The PR description is clear and the workflow_run race condition is well-explained

Warning

Firewall blocked 1 domain

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

  • proxy.golang.org

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

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 82.5 AIC · ⌖ 7.02 AIC · ⊞ 6.7K
Comment /matt to run again

Footer *string `yaml:"footer,omitempty"` // Controls when to show footer in PR review body: "always" (default), "none", or "if-body" (only when review has body text)
AllowedEvents []string `yaml:"allowed-events,omitempty"` // Optional list of allowed review event types: APPROVE, COMMENT, REQUEST_CHANGES. If omitted, all event types are allowed.
SupersedeOlderReviews bool `yaml:"supersede-older-reviews,omitempty"` // When true, dismisses older same-workflow REQUEST_CHANGES reviews after a replacement review is posted.
CommitId string `yaml:"commit-id,omitempty"` // Optional commit SHA to use as the reviewed commit. Pins the review to the commit the agent actually reviewed, preventing attribution drift when new commits are pushed during the run.

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] CommitId violates Go naming conventions — initialisms must be fully uppercase. The Go team's [naming guide]((go.dev/redacted) and golint/revive both flag Id as incorrect; it should be CommitID.

💡 Suggested fix

Rename in both structs and all references:

// submit_pr_review.go and create_pr_review_comment.go
CommitID string `yaml:"commit-id,omitempty"`

Also update safe_outputs_handler_registry.go (c.CommitIdc.CommitID) and local variables in the two parseXxx functions (commitIdcommitID). Short mechanical rename but needed to stay lint-clean.

@copilot please address this.

}

// Parse optional commit-id override
if commitId, exists := configMap["commit-id"]; exists {

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 commit-id parsing block is copy-pasted verbatim in both parseSubmitPullRequestReviewConfig and parsePullRequestReviewCommentsConfig. Duplicated parsing logic is a maintenance hazard — a future change (e.g., validation, normalisation) would need to be applied in two places.

💡 Suggested refactor

Extract a small helper:

// parseOptionalCommitID extracts and validates the commit-id config field.
func parseOptionalCommitID(configMap map[string]any, log *log.Logger) string {
    raw, exists := configMap["commit-id"]
    if !exists {
        return ""
    }
    s, ok := raw.(string)
    if !ok || s == "" {
        return ""
    }
    log.Printf("Commit ID override: %s", s)
    return s
}

Then both parsers become a single call site:

config.CommitID = parseOptionalCommitID(configMap, submitPRReviewLog)

@copilot please address this.

// Optional pinned commit SHA. When set, overrides the live PR head SHA so that the
// review is attributed to the commit the agent actually reviewed, not whatever head
// the PR has at post time (which may differ under workflow_run triggers).
const pinnedCommitId = typeof config.commit_id === "string" && config.commit_id.trim() ? config.commit_id.trim() : null;

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 commit-id option is user-facing YAML configuration but has no entry in docs/src/content/docs/specs/safe-outputs-specification.md (where allowed-events, supersede-older-reviews, etc. are documented, see line 1648). Without docs, users discovering this feature must read the source or the PR description.

💡 What to add

In the spec, under submit-pull-request-review fields and create-pull-request-review-comment fields, add:

- `commit-id` *(optional)* — SHA of the commit the agent actually reviewed. When set, the review is pinned to this commit instead of the live PR head SHA at post time. Useful under `workflow_run` triggers where a new commit may land between the agent run and the review post. GitHub will mark the review as outdated if HEAD has since moved — which is the correct behavior.

Also consider adding a glossary entry alongside supersede-older-reviews (line 406 of glossary.md).

@copilot please address this.

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.

PR review commit_id is the head at post time, not the commit the agent reviewed

3 participants