Skip to content

feat: add target, target-repo, and allowed-repos to resolve-pull-request-review-thread#18891

Merged
pelikhan merged 3 commits intomainfrom
copilot/add-target-config-to-resolve-thread
Feb 28, 2026
Merged

feat: add target, target-repo, and allowed-repos to resolve-pull-request-review-thread#18891
pelikhan merged 3 commits intomainfrom
copilot/add-target-config-to-resolve-thread

Conversation

Copy link
Contributor

Copilot AI commented Feb 28, 2026

  • Add SafeOutputTargetConfig to ResolvePullRequestReviewThreadConfig in pkg/workflow/resolve_pr_review_thread.go
  • Update parseResolvePullRequestReviewThreadConfig to parse target, target-repo, and allowed-repos
  • Update compiler_safe_outputs_config.go to include target, target-repo, allowed_repos in config generation
  • Update actions/setup/js/resolve_pr_review_thread.cjs to support cross-repo thread resolution with resolveTargetRepoConfig
  • Address review feedback:
    • Remove dead isInvalid branch (ParseTargetConfig always returns false) — use _, _ pattern
    • Fix %d format verb used on *string Max field — use templatableIntValue(config.Max)
    • Fix hasExplicitTargetConfig to check raw config keys (config["target-repo"], config.allowed_repos) instead of always-truthy defaultTargetRepo
    • Use validateTargetRepo from repo_helpers.cjs for wildcard-aware repo validation (supports *, org/*, etc.)
    • Fix JSDoc return type: repoNameWithOwner: string|null
    • Fail closed when threadRepo is null in cross-repo mode
    • Add wildcard/pattern allowlist tests (*, org/*, non-matching pattern)
    • Add fail-closed test for missing threadRepo in cross-repo mode
Original prompt

This section details on the original issue you should resolve

<issue_title>feat: add target config to resolve-pull-request-review-thread</issue_title>
<issue_description>## Request

Add target, target-repo, and allowed-repos support to resolve-pull-request-review-thread, matching other safe-outputs like add-comment and create-pull-request.

Context

SafeOutputTargetConfig was present in the initial implementation (53cad0d) but intentionally removed in cleanup (9b134b9) before #15668 merged, scoping resolution to the triggering PR only.

This limits use cases where an agent needs to resolve review threads on a PR in a different repository (e.g., cross-repo workflows).

Current behavior

resolve-pull-request-review-thread only accepts max. The JS handler validates threads belong to the triggering PR.

Desired behavior

Support target, target-repo, and allowed-repos fields consistent with other safe-outputs, allowing cross-repo thread resolution.</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add target config to resolve pull request review thread feat: add target, target-repo, and allowed-repos to resolve-pull-request-review-thread Feb 28, 2026
@pelikhan pelikhan marked this pull request as ready for review February 28, 2026 19:56
Copilot AI review requested due to automatic review settings February 28, 2026 19:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds target, target-repo, and allowed-repos support to resolve-pull-request-review-thread so workflows can resolve review threads outside the triggering PR/repo (consistent with other safe-outputs).

Changes:

  • Extend Go safe-output config parsing to include SafeOutputTargetConfig for resolve-pull-request-review-thread.
  • Forward target, target-repo, and allowed_repos into the generated JS handler config.
  • Update JS handler + tests to fetch repository.nameWithOwner via GraphQL and validate cross-repo targeting.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
pkg/workflow/resolve_pr_review_thread.go Embed/parse target config for resolve thread safe-output config.
pkg/workflow/compiler_safe_outputs_config.go Add target, target-repo, allowed_repos to handler config builder for this safe output.
actions/setup/js/resolve_pr_review_thread.cjs Add repo-aware thread lookup and cross-repo validation logic.
actions/setup/js/resolve_pr_review_thread.test.cjs Extend mocks + add cross-repo test cases.

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

Comment on lines 29 to 33
// Parse target config (target, target-repo, allowed-repos) with validation
targetConfig, isInvalid := ParseTargetConfig(configMap)
if isInvalid {
return nil // Invalid configuration (e.g., wildcard target-repo), return nil to cause validation error
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

ParseTargetConfig currently always returns isInvalid=false (and allows target-repo: "*" per its doc), so this isInvalid branch is dead code and the comment about wildcard being invalid is misleading. Either remove the check here or switch to a parser that can actually return a validation error for disallowed values.

Suggested change
// Parse target config (target, target-repo, allowed-repos) with validation
targetConfig, isInvalid := ParseTargetConfig(configMap)
if isInvalid {
return nil // Invalid configuration (e.g., wildcard target-repo), return nil to cause validation error
}
// Parse target config (target, target-repo, allowed-repos)
targetConfig := ParseTargetConfig(configMap)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c93026 — removed the isInvalid check and used _, _ pattern consistent with mark_pull_request_as_ready_for_review.go.

Comment on lines 159 to 214
// When a target-repo or allowed-repos is configured, validate the thread's repository.
// Otherwise, fall back to the legacy behavior of scoping to the triggering PR only.
const hasTargetRepoConfig = defaultTargetRepo || allowedRepos.size > 0;

if (hasTargetRepoConfig) {
// Cross-repo mode: validate thread repo against configured repos
if (threadRepo) {
const normalizedThreadRepo = threadRepo.toLowerCase();
const isDefaultRepo = normalizedDefaultTargetRepo && normalizedThreadRepo === normalizedDefaultTargetRepo;
const isAllowedRepo = isDefaultRepo || normalizedAllowedRepos.has(normalizedThreadRepo);
if (!isAllowedRepo) {
core.warning(`Thread ${threadId} belongs to repo ${threadRepo}, which is not in the allowed repos`);
return {
success: false,
error: `Thread belongs to repo '${threadRepo}', but only threads in allowed repositories can be resolved. Allowed: ${defaultTargetRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
};
}
}

// Determine target PR number based on target config
if (resolveTarget === "triggering") {
if (!triggeringPRNumber) {
core.warning("Cannot resolve review thread: not running in a pull request context");
return {
success: false,
error: "Cannot resolve review threads outside of a pull request context",
};
}
if (threadPRNumber !== triggeringPRNumber) {
core.warning(`Thread ${threadId} belongs to PR #${threadPRNumber}, not triggering PR #${triggeringPRNumber}`);
return {
success: false,
error: `Thread belongs to PR #${threadPRNumber}, but only threads on the triggering PR #${triggeringPRNumber} can be resolved`,
};
}
} else if (resolveTarget !== "*") {
// Explicit PR number target
const targetPRNumber = parseInt(resolveTarget, 10);
if (Number.isNaN(targetPRNumber) || targetPRNumber <= 0) {
core.warning(`Invalid target PR number: '${resolveTarget}'`);
return {
success: false,
error: `Invalid target: '${resolveTarget}' - must be 'triggering', '*', or a positive integer`,
};
}
if (threadPRNumber !== targetPRNumber) {
core.warning(`Thread ${threadId} belongs to PR #${threadPRNumber}, not target PR #${targetPRNumber}`);
return {
success: false,
error: `Thread belongs to PR #${threadPRNumber}, but target is PR #${targetPRNumber}`,
};
}
}
// resolveTarget === "*": any PR in allowed repos — no further PR number check needed
} else {
// Default (legacy) mode: scope to triggering PR only
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

hasTargetRepoConfig is always truthy because defaultTargetRepo is always a non-empty string (it falls back to context.repo). That makes the else ("legacy mode") branch unreachable and the comment about only validating when configured inaccurate. Consider either removing the dead branch, or basing the condition on the presence of config keys (e.g. config["target-repo"] / config.allowed_repos) if you truly need a legacy path.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c93026 — replaced defaultTargetRepo || allowedRepos.size > 0 with hasExplicitTargetConfig, which checks config["target-repo"] and config.allowed_repos?.length > 0 directly on the raw config to detect user intent rather than the always-truthy resolved value.

Comment on lines 163 to 176
if (hasTargetRepoConfig) {
// Cross-repo mode: validate thread repo against configured repos
if (threadRepo) {
const normalizedThreadRepo = threadRepo.toLowerCase();
const isDefaultRepo = normalizedDefaultTargetRepo && normalizedThreadRepo === normalizedDefaultTargetRepo;
const isAllowedRepo = isDefaultRepo || normalizedAllowedRepos.has(normalizedThreadRepo);
if (!isAllowedRepo) {
core.warning(`Thread ${threadId} belongs to repo ${threadRepo}, which is not in the allowed repos`);
return {
success: false,
error: `Thread belongs to repo '${threadRepo}', but only threads in allowed repositories can be resolved. Allowed: ${defaultTargetRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`,
};
}
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The repo allowlist check here only supports exact matches (Set.has) and will not honor the wildcard patterns (*, org/*, */repo, etc.) supported elsewhere via repo_helpers.validateRepo/isRepoAllowed. This breaks existing allowed_repos semantics and makes configs like allowed_repos: ["*"] ineffective. Reuse validateTargetRepo/validateRepo from repo_helpers.cjs for the threadRepo validation (and fail closed if threadRepo is unexpectedly missing).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c93026 — replaced the custom exact-match logic with validateTargetRepo(threadRepo, defaultTargetRepo, allowedRepos) from repo_helpers.cjs, which supports wildcards (*, org/*, etc.). Also added fail-closed behavior when threadRepo is null.

Comment on lines 20 to 52
@@ -37,7 +41,15 @@ async function getThreadPullRequestNumber(github, threadId) {

const result = await github.graphql(query, { threadId });

return result?.node?.pullRequest?.number ?? null;
const pullRequest = result?.node?.pullRequest;
if (!pullRequest) {
return null;
}

return {
prNumber: pullRequest.number,
repoNameWithOwner: pullRequest.repository?.nameWithOwner ?? null,
};
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The JSDoc says repoNameWithOwner is a string, but the function can return null (pullRequest.repository?.nameWithOwner ?? null). Update the return type to string|null (and/or enforce non-null if you expect this field to always be present) to keep the docs consistent with runtime behavior.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c93026 — updated the JSDoc return type to {prNumber: number, repoNameWithOwner: string|null}|null.

Comment on lines +325 to +390
describe("resolve_pr_review_thread - cross-repo support", () => {
const originalPayload = mockContext.payload;

beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});

afterEach(() => {
global.context.payload = originalPayload;
});

it("should allow resolving a thread in target-repo when configured", async () => {
mockGraphqlForThread(10, "other-owner/other-repo");

const { main } = require("./resolve_pr_review_thread.cjs");
const freshHandler = await main({
max: 10,
"target-repo": "other-owner/other-repo",
target: "*",
});

const message = {
type: "resolve_pull_request_review_thread",
thread_id: "PRRT_kwDOCrossRepo",
};

const result = await freshHandler(message, {});

expect(result.success).toBe(true);
expect(result.thread_id).toBe("PRRT_kwDOCrossRepo");
expect(result.is_resolved).toBe(true);
});

it("should reject a thread whose repo is not in allowed-repos", async () => {
mockGraphqlForThread(10, "other-owner/other-repo");

const { main } = require("./resolve_pr_review_thread.cjs");
const freshHandler = await main({
max: 10,
"target-repo": "allowed-owner/allowed-repo",
target: "*",
});

const message = {
type: "resolve_pull_request_review_thread",
thread_id: "PRRT_kwDOCrossRepo",
};

const result = await freshHandler(message, {});

expect(result.success).toBe(false);
expect(result.error).toContain("other-owner/other-repo");
expect(result.error).toContain("allowed");
});

it("should allow cross-repo thread in allowed_repos list", async () => {
mockGraphqlForThread(10, "extra-owner/extra-repo");

const { main } = require("./resolve_pr_review_thread.cjs");
const freshHandler = await main({
max: 10,
"target-repo": "allowed-owner/allowed-repo",
allowed_repos: ["extra-owner/extra-repo"],
target: "*",
});
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

Cross-repo tests cover exact repo matches, but there are no assertions for the wildcard/pattern allowlist behavior that other handlers support (e.g. allowed_repos: ["*"] or allowed_repos: ["other-owner/*"]). Adding at least one such test would prevent regressions and would have caught the current exact-match-only validation logic.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c93026 — added tests for allowed_repos: ["*"] wildcard, allowed_repos: ["org/*"] pattern (match and non-match), and the fail-closed case when threadRepo is null in cross-repo mode.

}
config.SafeOutputTargetConfig = targetConfig

resolvePRReviewThreadLog.Printf("Parsed resolve-pull-request-review-thread config: max=%d, target_repo=%s", config.Max, config.TargetRepoSlug)
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

config.Max is a *string (templatable int), but this log uses %d, which will print %!d(*string=...) at runtime. Use %v/%s with a nil-safe dereference (or log templatableIntValue(config.Max) if you specifically want the numeric value).

Suggested change
resolvePRReviewThreadLog.Printf("Parsed resolve-pull-request-review-thread config: max=%d, target_repo=%s", config.Max, config.TargetRepoSlug)
maxVal := ""
if config.Max != nil {
maxVal = *config.Max
}
resolvePRReviewThreadLog.Printf("Parsed resolve-pull-request-review-thread config: max=%s, target_repo=%s", maxVal, config.TargetRepoSlug)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9c93026 — replaced %d with templatableIntValue(config.Max) which extracts the integer value from the *string field.

@pelikhan
Copy link
Contributor

…rget config

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

Copilot AI commented Feb 28, 2026

@copilot fix https://github.com/github/gh-aw/actions/runs/22527301901/job/65262649972

The CI failure in that run (TestAgentVersionInAwInfo, TestGenerateCreateAwInfoWithStaged) is pre-existing and unrelated to these changes — confirmed by checking out the base commit and reproducing the same failures. The review comments above have been addressed in 9c93026.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GOMOD GOMODCACHE tcfg env g/logger/exampleGOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGO111MODULE (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GOMOD GOMODCACHE tcfg env 23a0315c8ed0011dGOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGO111MODULE (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha -unreachable=false /tmp/go-build387703305/b193/vet.cfg 703305/b319/vet.cfg (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha se 703305/b347/vet.cfg /opt/hostedtoolcache/go/1.25.0/x64/pkg/tool/linux_amd64/compile (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha HEAD (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/a70c5eada06553e3510ac27f2c3bda9d3705bccb
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/a70c5eada06553e3510ac27f2c3bda9d3705bccb --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD erignore go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha (http block)
  • https://api.github.com/repos/githubnext/agentics/git/ref/tags/
    • Triggering command: /usr/bin/gh gh api /repos/githubnext/agentics/git/ref/tags/# --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE sh (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan
Copy link
Contributor

@pelikhan pelikhan merged commit 19e14da into main Feb 28, 2026
75 of 78 checks passed
@pelikhan pelikhan deleted the copilot/add-target-config-to-resolve-thread branch February 28, 2026 20:28
Copilot stopped work on behalf of pelikhan due to an error February 28, 2026 20:28
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.

feat: add target config to resolve-pull-request-review-thread

3 participants