Repository navigation
Fix #37835: always derive push_to_pull_request_branch from PR head ref #37863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4221013
1902dc3
e5027b4
fff68b7
034582e
f433551
40babd1
9cbc6d2
28e526a
c5c83f0
3318eec
7678ec4
8b6c445
09e27ab
07b5447
5eb0c65
baad3b9
7876fe5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
| // Tests intentionally use the simplest safe-output tool (`create_issue`) so we | ||
| // do not need to set up a git working tree for patch sidecars. | ||
|
|
||
| import { describe, it, expect, beforeAll } from "vitest"; | ||
| import { describe, it, expect, beforeAll, vi } from "vitest"; | ||
| import { spawnSync } from "child_process"; | ||
| import { createRequire } from "module"; | ||
| import fs from "fs"; | ||
|
|
@@ -379,12 +379,13 @@ describe("apply_samples.cjs preStagePatch (create_pull_request / push_to_pull_re | |
| }) | ||
| ); | ||
|
|
||
| // Stub global fetch to simulate the GitHub REST API response. | ||
| const realFetch = global.fetch; | ||
| global.fetch = async url => { | ||
| expect(String(url)).toContain("/repos/owner/repo/pulls/42"); | ||
| return { ok: true, status: 200, json: async () => ({ head: { ref: headRef } }) }; | ||
| }; | ||
| // Spy on global fetch so the call-URL assertion happens on the test side | ||
| // (an assertion failure inside a global-mutating stub is hard to attribute). | ||
| const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ | ||
| ok: true, | ||
| status: 200, | ||
| json: async () => ({ head: { ref: headRef } }), | ||
| }); | ||
|
|
||
| const prevBase = process.env.GH_AW_CUSTOM_BASE_BRANCH; | ||
| const prevEvent = process.env.GITHUB_EVENT_PATH; | ||
|
|
@@ -400,8 +401,9 @@ describe("apply_samples.cjs preStagePatch (create_pull_request / push_to_pull_re | |
| }; | ||
| await preStagePatch(entry, 0, workspace); | ||
| expect(git(["rev-parse", "--abbrev-ref", "HEAD"], workspace).trim()).toBe(headRef); | ||
| expect(fetchSpy).toHaveBeenCalledWith(expect.stringContaining("/repos/owner/repo/pulls/42"), expect.anything()); | ||
| } finally { | ||
| global.fetch = realFetch; | ||
| fetchSpy.mockRestore(); | ||
| if (prevBase === undefined) delete process.env.GH_AW_CUSTOM_BASE_BRANCH; | ||
| else process.env.GH_AW_CUSTOM_BASE_BRANCH = prevBase; | ||
| if (prevEvent === undefined) delete process.env.GITHUB_EVENT_PATH; | ||
|
|
@@ -429,6 +431,45 @@ describe("apply_samples.cjs preStagePatch (create_pull_request / push_to_pull_re | |
| } | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Resolution path 3 — an explicit 💡 Suggested testAdd a test that sets it("derives branch from explicit pull_request_number on the sample when no event payload exists", async () => {
const workspace = makeTempDir("gh-aw-prestage-push-argpr-");
initRepo(workspace, "main");
const headRef = "feat/explicit-pr-number";
const realFetch = global.fetch;
global.fetch = async () => ({ ok: true, status: 200, json: async () => ({ head: { ref: headRef } }) });
const prevEvent = process.env.GITHUB_EVENT_PATH;
const prevRepo = process.env.GITHUB_REPOSITORY;
const prevBase = process.env.GH_AW_CUSTOM_BASE_BRANCH;
delete process.env.GITHUB_EVENT_PATH; // no event payload — force path 3
process.env.GITHUB_REPOSITORY = "owner/repo";
process.env.GH_AW_CUSTOM_BASE_BRANCH = "main";
try {
const entry = {
tool: "push_to_pull_request_branch",
arguments: { message: "Push update", pull_request_number: 99 },
sidecars: { patch: newFileDiff("arg-pr.txt", "via arg pr\n") },
};
await preStagePatch(entry, 0, workspace);
expect(git(["rev-parse", "--abbrev-ref", "HEAD"], workspace).trim()).toBe(headRef);
} finally {
global.fetch = realFetch;
// restore env
}
}); |
||
|
|
||
| it("derives push_to_pull_request_branch branch from explicit arguments.pull_request_number when no event payload exists", async () => { | ||
| // Resolution path 3: no GITHUB_EVENT_PATH, but the sample entry carries an | ||
| // explicit `arguments.pull_request_number`. The driver should hit the PR | ||
| // API directly rather than fail fast. | ||
| const workspace = makeTempDir("gh-aw-prestage-push-argpr-"); | ||
| initRepo(workspace, "main"); | ||
|
|
||
| const headRef = "feat/explicit-pr-number"; | ||
| const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({ | ||
| ok: true, | ||
| status: 200, | ||
| json: async () => ({ head: { ref: headRef } }), | ||
| }); | ||
|
|
||
| const prevBase = process.env.GH_AW_CUSTOM_BASE_BRANCH; | ||
| const prevEvent = process.env.GITHUB_EVENT_PATH; | ||
| const prevRepo = process.env.GITHUB_REPOSITORY; | ||
| delete process.env.GITHUB_EVENT_PATH; // force path 3 | ||
| process.env.GH_AW_CUSTOM_BASE_BRANCH = "main"; | ||
| process.env.GITHUB_REPOSITORY = "owner/repo"; | ||
| try { | ||
| const entry = { | ||
| tool: "push_to_pull_request_branch", | ||
| arguments: { message: "Push update", pull_request_number: 99 }, | ||
| sidecars: { patch: newFileDiff("arg-pr.txt", "via arg pr\n") }, | ||
| }; | ||
| await preStagePatch(entry, 0, workspace); | ||
| expect(git(["rev-parse", "--abbrev-ref", "HEAD"], workspace).trim()).toBe(headRef); | ||
| expect(fetchSpy).toHaveBeenCalledWith(expect.stringContaining("/repos/owner/repo/pulls/99"), expect.anything()); | ||
| } finally { | ||
| fetchSpy.mockRestore(); | ||
| if (prevBase === undefined) delete process.env.GH_AW_CUSTOM_BASE_BRANCH; | ||
| else process.env.GH_AW_CUSTOM_BASE_BRANCH = prevBase; | ||
| if (prevEvent !== undefined) process.env.GITHUB_EVENT_PATH = prevEvent; | ||
| if (prevRepo === undefined) delete process.env.GITHUB_REPOSITORY; | ||
| else process.env.GITHUB_REPOSITORY = prevRepo; | ||
| } | ||
| }); | ||
|
|
||
| it("is a no-op when the sample tool isn't in the patch-sidecar set", async () => { | ||
| // We assert this at the driver level (PATCH_SIDECAR_TOOLS gate in main()), | ||
| // but preStagePatch itself should also be a no-op when called with an | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,8 +88,10 @@ func collectSampleRepoTokens(configs []*CheckoutConfig) map[string]string { | |
| for i, entry := range cm.ordered { | ||
| repo := entry.key.repository | ||
| if repo == "" { | ||
| // Empty repository targets `github.repository` at runtime. Emit a | ||
| // substituted key so the runtime JSON has the actual slug. | ||
| // The repo slug is unknown at compile time; emit a GitHub Actions | ||
| // expression. The Actions runner expands ${{ github.repository }} to | ||
| // "owner/repo" before apply_samples.cjs reads GH_AW_REPO_TOKENS, so | ||
| // the runtime key matches the slug the driver looks up. | ||
| repo = "${{ github.repository }}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/zoom-out] The comment "Emit a substituted key so the runtime JSON has the actual slug" may mislead readers: the Go code does not substitute anything. The literal string // The repo slug is unknown at compile time; emit a GitHub Actions expression.
// The Actions runner expands ${{ github.repository }} → "owner/repo" before
// apply_samples.cjs reads GH_AW_REPO_TOKENS, so the key matches correctly.
repo = "${{ github.repository }}"This is a doc-only nit, but the coupling to GitHub Actions expression evaluation is a non-obvious implicit contract worth calling out explicitly. |
||
| } | ||
| var token string | ||
|
|
@@ -116,32 +118,16 @@ func collectSampleRepoTokens(configs []*CheckoutConfig) map[string]string { | |
| return tokens | ||
| } | ||
|
|
||
| // marshalStringMapDeterministic returns a compact JSON object encoding of m | ||
| // with keys sorted lexicographically so the emitted YAML is byte-stable | ||
| // across runs. Returns nil for empty/nil maps so callers can skip emission. | ||
| func marshalStringMapDeterministic(m map[string]string) []byte { | ||
| // marshalRepoTokens returns a compact JSON object encoding of m, or nil for | ||
| // empty/nil maps so callers can skip emission. encoding/json sorts string- | ||
| // keyed map entries lexicographically, so the output is byte-stable across | ||
| // runs without an explicit sort step. | ||
| func marshalRepoTokens(m map[string]string) []byte { | ||
| if len(m) == 0 { | ||
| return nil | ||
| } | ||
| keys := make([]string, 0, len(m)) | ||
| for k := range m { | ||
| keys = append(keys, k) | ||
| } | ||
| sort.Strings(keys) | ||
| var buf strings.Builder | ||
| buf.WriteString("{") | ||
| for i, k := range keys { | ||
| if i > 0 { | ||
| buf.WriteString(",") | ||
| } | ||
| kEnc, _ := json.Marshal(k) //nolint:jsonmarshalignoredeerror // marshaling a string cannot fail | ||
| vEnc, _ := json.Marshal(m[k]) //nolint:jsonmarshalignoredeerror // marshaling a string cannot fail | ||
| buf.Write(kEnc) | ||
| buf.WriteString(":") | ||
| buf.Write(vEnc) | ||
| } | ||
| buf.WriteString("}") | ||
| return []byte(buf.String()) | ||
| out, _ := json.Marshal(m) //nolint:jsonmarshalignoredeerror // marshaling a string map cannot fail | ||
| return out | ||
| } | ||
|
|
||
| // generateSamplesReplayStep emits the YAML that replaces the agentic | ||
|
|
@@ -177,7 +163,7 @@ func (c *Compiler) generateSamplesReplayStep(yaml *strings.Builder, data *Workfl | |
| // require a non-default token (cross-repo `checkout:` entries with their | ||
| // own `github-token:` or `github-app:`). | ||
| repoTokens := collectSampleRepoTokens(data.CheckoutConfigs) | ||
| repoTokensPayload := marshalStringMapDeterministic(repoTokens) | ||
| repoTokensPayload := marshalRepoTokens(repoTokens) | ||
|
|
||
| yaml.WriteString(" - name: Replay safe-outputs samples (deterministic)\n") | ||
| yaml.WriteString(" id: agentic_execution\n") | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.