Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions actions/setup/js/git_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function execGitSync(args, options = {}) {
timeout: defaultTimeoutMs,
killSignal: "SIGKILL",
...spawnOptions,
stdio: spawnOptions.stdio === "inherit" ? "pipe" : spawnOptions.stdio,
env: safeEnv,
});

Expand Down
6 changes: 6 additions & 0 deletions actions/setup/js/git_helpers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ describe("git_helpers.cjs", () => {
expect(result).toContain("git version");
});

it("captures inherited git output for Actions logging", async () => {
const { execGitSync } = await import("./git_helpers.cjs");

expect(execGitSync(["--version"], { stdio: "inherit" })).toContain("git version");
});

it("should not call core.error when suppressLogs is true", async () => {
const { execGitSync } = await import("./git_helpers.cjs");

Expand Down
13 changes: 7 additions & 6 deletions actions/setup/js/push_experiment_state.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -560,21 +560,22 @@ async function main() {
// Refresh baseRef and fetch the updated remote history so that
// pushSignedCommits can resolve the new baseRef in git rev-list.
try {
const { stdout: lsOut } = await exec.getExecOutput("git", ["ls-remote", "origin", `refs/heads/${branchName}`], { cwd: workspaceDir });
const gitEnv = { ...process.env, ...getGitAuthEnv(ghToken) };
const { stdout: lsOut } = await exec.getExecOutput("git", ["ls-remote", "origin", `refs/heads/${branchName}`], { cwd: workspaceDir, env: gitEnv, silent: true });
const remoteHead = lsOut.trim().split(/\s+/)[0] || "";
if (remoteHead && remoteHead !== currentBaseRef) {
currentBaseRef = remoteHead;
core.info(`Refreshed baseRef for retry: ${currentBaseRef}`);
// Fetch the updated branch history into the local repo so pushSignedCommits
// can resolve currentBaseRef in `git rev-list baseRef..HEAD`.
try {
execGitSync(["fetch", "origin", `refs/heads/${branchName}`], { stdio: "pipe", cwd: workspaceDir, suppressLogs: true });
execGitSync(["fetch", "origin", `refs/heads/${branchName}`], { stdio: "pipe", cwd: workspaceDir, env: gitEnv, suppressLogs: true });
currentBaseRef = remoteHead;
core.info(`Refreshed baseRef for retry: ${currentBaseRef}`);
} catch (fetchErr) {
core.info(`Fetch of branch "${branchName}" on retry failed (non-fatal): ${getErrorMessage(fetchErr)}`);
}
}
} catch {
// ls-remote failed — ignored, keep existing baseRef.
} catch (remoteErr) {
core.warning(`Could not refresh baseRef for branch "${branchName}" on retry; keeping existing baseRef: ${getErrorMessage(remoteErr)}`);
}
} else {
core.setFailed(`Failed to push ${stateLabel} after ${MAX_RETRIES + 1} attempts: ${errMsg}`);
Expand Down
48 changes: 48 additions & 0 deletions actions/setup/js/push_experiment_state.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import os from "os";
import path from "path";
import fs from "fs";
import vm from "node:vm";
import { createRequire } from "node:module";

// Globals required by push_experiment_state.cjs and its dependencies
const mockCore = {
Expand Down Expand Up @@ -128,6 +130,52 @@ describe("push_experiment_state", () => {
statSpy.mockRestore();
});

it.each(["none", "fetch", "ls-remote"])("authenticates retry operations and advances the base only after a successful fetch (failure: %s)", async failure => {
const baseRef = "a".repeat(40);
const remoteHead = "b".repeat(40);
const gitEnv = { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader", GIT_CONFIG_VALUE_0: "masked-auth-header" };
const execGitSync = vi.fn(args => {
if (args[0] === "status") return "M state.json";
if (args[0] === "rev-parse") return baseRef;
if (failure === "fetch" && args[0] === "fetch" && args[1] === "origin") throw new Error("temporary fetch failure");
return "";
});
const pushSignedCommits = vi.fn().mockRejectedValueOnce(new Error("non-fast-forward")).mockResolvedValue(undefined);
const exec = { getExecOutput: vi.fn().mockResolvedValue({ stdout: `${remoteHead}\trefs/heads/evals/test` }) };
if (failure === "ls-remote") exec.getExecOutput.mockRejectedValue(new Error("temporary ls-remote failure"));
const require = createRequire(import.meta.url);
const module = { exports: {} };
const workspaceDir = path.join(tmpDir, "workspace");
fs.mkdirSync(workspaceDir);
fs.writeFileSync(path.join(tmpDir, "state.json"), "{}");
const mocks = {
"./git_helpers.cjs": { execGitSync, withGitRetry: async fn => fn() },
"./git_auth_helpers.cjs": { getGitAuthEnv: () => gitEnv },
"./push_signed_commits.cjs": { pushSignedCommits },
};
vm.runInNewContext(fs.readFileSync(path.join(import.meta.dirname, "push_experiment_state.cjs"), "utf8"), {
require: id => mocks[id] || require(id),
module,
core: mockCore,
exec,
context: mockContext,
github: {},
setTimeout: fn => fn(),
process: { env: { GH_AW_STATE_BRANCH: "evals/test", GH_AW_STATE_DIR: tmpDir, GITHUB_WORKSPACE: workspaceDir, GH_TOKEN: "test-token" } },
});
await module.exports.main();
expect(exec.getExecOutput).toHaveBeenCalledWith("git", ["ls-remote", "origin", "refs/heads/evals/test"], { cwd: workspaceDir, env: expect.objectContaining(gitEnv), silent: true });
if (failure === "ls-remote") {
expect(execGitSync).not.toHaveBeenCalledWith(["fetch", "origin", "refs/heads/evals/test"], expect.anything());
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('Could not refresh baseRef for branch "evals/test" on retry; keeping existing baseRef: temporary ls-remote failure'));
} else {
expect(execGitSync).toHaveBeenCalledWith(["fetch", "origin", "refs/heads/evals/test"], expect.objectContaining({ env: expect.objectContaining(gitEnv) }));
}
expect(pushSignedCommits).toHaveBeenCalledTimes(2);
expect(pushSignedCommits.mock.calls[1][0].baseRef).toBe(failure === "none" ? remoteHead : baseRef);
expect(mockCore.setFailed).not.toHaveBeenCalled();
});

it("merges concurrent experiment state updates without losing same-variant increments", () => {
const baseState = {
counts: { prompt_style: { concise: 1, detailed: 1 } },
Expand Down
11 changes: 0 additions & 11 deletions actions/setup/js/push_repo_memory.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,6 @@ function getCustomValidationFailureDetail({ stdout, stderr }) {
async function setPushRepoMemoryFailure(message) {
const safeMessage = redactFailureSummary(message);
core.info(`Repo-memory push failed: ${safeMessage}`);
const summary = core.summary;
if (summary && typeof summary.addRaw === "function" && typeof summary.write === "function") {
const safeSummaryMessage = safeMessage.replace(/```/g, "``\u200b`");
try {
await summary.addRaw(`### Repo-memory push failed\n\n\`\`\`text\n${safeSummaryMessage}\n\`\`\`\n`).write();
} catch (error) {
if (typeof core.warning === "function") {
core.warning(`Failed to write repo-memory failure summary: ${redactFailureSummary(getErrorMessage(error))}`);
}
}
}
return core.setFailed(safeMessage);
}

Expand Down
16 changes: 7 additions & 9 deletions actions/setup/js/push_repo_memory.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2205,12 +2205,8 @@ describe("push_repo_memory.cjs - signed commit push (pushSignedCommits delegatio
it("should fail deterministic validation errors without retrying", async () => {
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "repo-memory-validation-"));
const setFailed = vi.fn();
const summaryContents = [];
const summary = {
addRaw: vi.fn(content => {
summaryContents.push(content);
return { write: vi.fn().mockResolvedValue(undefined) };
}),
addRaw: vi.fn(),
write: vi.fn(),
};
const previousGitHubToken = process.env.GH_TOKEN;
Expand Down Expand Up @@ -2238,8 +2234,7 @@ describe("push_repo_memory.cjs - signed commit push (pushSignedCommits delegatio
expect(pushed).toBe(false);
expect(setFailed).toHaveBeenCalledWith("Failed to push changes: ERR_VALIDATION: merge commit detected with [REDACTED]");
expect(global.core.info).toHaveBeenCalledWith("Repo-memory push failed: Failed to push changes: ERR_VALIDATION: merge commit detected with [REDACTED]");
expect(summaryContents.join("\n")).toContain("ERR_VALIDATION: merge commit detected with [REDACTED]");
expect(summaryContents.join("\n")).not.toContain("not-a-real-token");
expect(summary.addRaw).not.toHaveBeenCalled();
expect(isDeterministicPushValidationError("ERR_VALIDATION: policy violation")).toBe(true);
} finally {
if (previousGitHubToken === undefined) {
Expand Down Expand Up @@ -2283,7 +2278,7 @@ describe("push_repo_memory.cjs - signed commit push (pushSignedCommits delegatio
}
});

it("retries a compare-and-swap loss by rebasing onto the refreshed head and preserving both JSONL rows", async () => {
it("retries a non-fast-forward push by rebasing onto the refreshed head and preserving both JSONL rows", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "repo-memory-race-"));
const remoteDir = path.join(rootDir, "remote.git");
const seedDir = path.join(rootDir, "seed");
Expand Down Expand Up @@ -2318,13 +2313,14 @@ describe("push_repo_memory.cjs - signed commit push (pushSignedCommits delegatio
const pushSignedCommitsFn = vi.fn(async ({ baseRef: receivedBaseRef, cwd }) => {
pushBases.push(receivedBaseRef);
if (pushBases.length === 1) {
throw new Error("ERR_API: GraphQL createCommitOnBranch expectedHeadOid did not match");
execFileSync("git", ["push", "origin", branchName], { cwd, stdio: "pipe" });
}

const revList = execSync(`git rev-list --parents ${remoteHead}..HEAD`, { cwd, encoding: "utf8" }).trim();
const rows = fs.readFileSync(path.join(cwd, "history.jsonl"), "utf8").trim().split("\n");
expect(revList.split(/\s+/)).toHaveLength(2);
expect(rows).toEqual(['{"id":"base"}', '{"id":"remote"}', '{"id":"local"}']);
execFileSync("git", ["push", "origin", branchName], { cwd, stdio: "pipe" });
});

await pushRepoMemoryChangesWithRetry({
Expand Down Expand Up @@ -2352,6 +2348,8 @@ describe("push_repo_memory.cjs - signed commit push (pushSignedCommits delegatio
expect(pushBases).toEqual([baseRef, remoteHead]);
expect(delays).toEqual([501]);
expect(global.core.setFailed).not.toHaveBeenCalled();
expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("fetch first"));
expect(execFileSync("git", ["--git-dir", remoteDir, "show", `${branchName}:history.jsonl`], { encoding: "utf8" })).toBe('{"id":"base"}\n{"id":"remote"}\n{"id":"local"}\n');
} finally {
randomSpy.mockRestore();
delete global.core;
Expand Down
Loading
Loading