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
19 changes: 12 additions & 7 deletions actions/setup/js/run_operation_update_upgrade.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,14 @@ async function main() {

const isUpgrade = operation === "upgrade";

// Run gh aw update or gh aw upgrade (--no-compile: do not touch lock files)
const fullCmd = [bin, ...prefixArgs, operation, "--no-compile"].join(" ");
// Run gh aw update or gh aw upgrade without extra flags so all files are
// updated (codemods, action pins, lock files, etc.). Changed files under
// .github/workflows/ are detected afterwards but excluded from staging so
// the GitHub Actions actor – which is not permitted to commit workflow
// files – does not attempt to include them in the pull request.
const fullCmd = [bin, ...prefixArgs, operation].join(" ");
core.info(`Running: ${fullCmd}`);
const exitCode = await exec.exec(bin, [...prefixArgs, operation, "--no-compile"]);
const exitCode = await exec.exec(bin, [...prefixArgs, operation]);
if (exitCode !== 0) {
throw new Error(`Command '${fullCmd}' failed with exit code ${exitCode}`);
}
Expand All @@ -93,11 +97,12 @@ async function main() {
return;
}

// Exclude .github/workflows/*.yml files: they cannot be modified by the
// GitHub Actions bot and including them would cause the PR checks to fail.
// Exclude ALL .github/workflows/ files: the GitHub Actions actor is not
// permitted to commit any changes to workflow files (neither compiled .yml
// files nor source .md files). Including them would cause PR checks to fail.
const filesToStage = changedFiles.filter(file => {
const lower = file.toLowerCase();
return !(lower.startsWith(".github/workflows/") && (lower.endsWith(".yml") || lower.endsWith(".yaml")));
return !lower.startsWith(".github/workflows/");
});

if (filesToStage.length === 0) {
Expand Down Expand Up @@ -184,7 +189,7 @@ async function main() {
const operationLabel = isUpgrade ? "Upgrade" : "Update";
const prBody = `## Agentic Workflows ${operationLabel}

The \`gh aw ${operation} --no-compile\` command was run automatically and produced the following changes:
The \`gh aw ${operation}\` command was run automatically and produced the following changes:

${fileList}

Expand Down
83 changes: 68 additions & 15 deletions actions/setup/js/run_operation_update_upgrade.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe("run_operation_update_upgrade", () => {
await main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No changes detected"));
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update", "--no-compile"]);
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update"]);
});

it("finishes without PR when only workflow yml files changed", async () => {
Expand All @@ -203,6 +203,24 @@ describe("run_operation_update_upgrade", () => {
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No non-workflow files changed"));
expect(mockExec.exec).not.toHaveBeenCalledWith("git", expect.arrayContaining(["add"]));
});

it("finishes without PR when only workflow md files changed", async () => {
process.env.GH_AW_OPERATION = "update";
process.env.GH_AW_CMD_PREFIX = "gh aw";
process.env.GH_TOKEN = "test-token";

mockExec.getExecOutput = vi.fn().mockResolvedValueOnce({
stdout: " M .github/workflows/my-workflow.md\n",
stderr: "",
exitCode: 0,
});

const { main } = await import("./run_operation_update_upgrade.cjs");
await main();

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No non-workflow files changed"));
expect(mockExec.exec).not.toHaveBeenCalledWith("git", expect.arrayContaining(["add"]));
});
});

describe("main - creates PR when files changed", () => {
Expand All @@ -212,15 +230,15 @@ describe("run_operation_update_upgrade", () => {
process.env.GH_TOKEN = "test-token";

const getExecOutputMock = vi.fn();
// git status
// git status - only non-workflow file changed
getExecOutputMock.mockResolvedValueOnce({
stdout: " M .github/workflows/my-workflow.md\n",
stdout: " M .github/aw/actions-lock.json\n",
stderr: "",
exitCode: 0,
});
// git diff --cached --name-only
getExecOutputMock.mockResolvedValueOnce({
stdout: ".github/workflows/my-workflow.md\n",
stdout: ".github/aw/actions-lock.json\n",
stderr: "",
exitCode: 0,
});
Expand All @@ -235,19 +253,54 @@ describe("run_operation_update_upgrade", () => {
const { main } = await import("./run_operation_update_upgrade.cjs");
await main();

// Verify gh aw update --no-compile was run
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update", "--no-compile"]);
// Verify gh aw update was run
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "update"]);
// Verify branch was created
expect(mockExec.exec).toHaveBeenCalledWith("git", expect.arrayContaining(["checkout", "-b", expect.stringContaining("aw/update-")]));
// Verify file was staged
expect(mockExec.exec).toHaveBeenCalledWith("git", ["add", "--", ".github/workflows/my-workflow.md"]);
expect(mockExec.exec).toHaveBeenCalledWith("git", ["add", "--", ".github/aw/actions-lock.json"]);
// Verify commit was made
expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: update agentic workflows"]);
// Verify PR title
expect(getExecOutputMock).toHaveBeenCalledWith("gh", expect.arrayContaining(["pr", "create", "--title", "[aw] Updates available", "--label", "agentic-workflows"]), expect.anything());
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Created PR"));
});

it("creates PR with only non-workflow files when both workflow and non-workflow files changed", async () => {
process.env.GH_AW_OPERATION = "update";
process.env.GH_AW_CMD_PREFIX = "gh aw";
process.env.GH_TOKEN = "test-token";

// Both a workflow .md and a non-workflow file changed
const getExecOutputMock = vi.fn();
getExecOutputMock.mockResolvedValueOnce({
stdout: " M .github/workflows/my-workflow.md\n M .github/aw/actions-lock.json\n",
stderr: "",
exitCode: 0,
});
// git diff --cached --name-only (only non-workflow file staged)
getExecOutputMock.mockResolvedValueOnce({
stdout: ".github/aw/actions-lock.json\n",
stderr: "",
exitCode: 0,
});
// gh pr create
getExecOutputMock.mockResolvedValueOnce({
stdout: "https://github.com/testowner/testrepo/pull/5\n",
stderr: "",
exitCode: 0,
});
mockExec.getExecOutput = getExecOutputMock;

const { main } = await import("./run_operation_update_upgrade.cjs");
await main();

// Workflow .md must NOT be staged
expect(mockExec.exec).not.toHaveBeenCalledWith("git", ["add", "--", ".github/workflows/my-workflow.md"]);
// Non-workflow file should be staged
expect(mockExec.exec).toHaveBeenCalledWith("git", ["add", "--", ".github/aw/actions-lock.json"]);
});

it("creates PR for upgrade operation with correct title", async () => {
process.env.GH_AW_OPERATION = "upgrade";
process.env.GH_AW_CMD_PREFIX = "gh aw";
Expand Down Expand Up @@ -277,8 +330,8 @@ describe("run_operation_update_upgrade", () => {
const { main } = await import("./run_operation_update_upgrade.cjs");
await main();

// Verify gh aw upgrade --no-compile was run
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "upgrade", "--no-compile"]);
// Verify gh aw upgrade was run
expect(mockExec.exec).toHaveBeenCalledWith("gh", ["aw", "upgrade"]);
// Verify correct commit message
expect(mockExec.exec).toHaveBeenCalledWith("git", ["commit", "-m", "chore: upgrade agentic workflows"]);
// Verify PR title is "[aw] Upgrade available"
Expand All @@ -294,16 +347,16 @@ describe("run_operation_update_upgrade", () => {

const getExecOutputMock = vi.fn();
getExecOutputMock
.mockResolvedValueOnce({ stdout: " M .github/workflows/my-workflow.md\n", stderr: "", exitCode: 0 })
.mockResolvedValueOnce({ stdout: ".github/workflows/my-workflow.md\n", stderr: "", exitCode: 0 })
.mockResolvedValueOnce({ stdout: " M .github/aw/actions-lock.json\n", stderr: "", exitCode: 0 })
.mockResolvedValueOnce({ stdout: ".github/aw/actions-lock.json\n", stderr: "", exitCode: 0 })
.mockResolvedValueOnce({ stdout: "https://github.com/testowner/testrepo/pull/3\n", stderr: "", exitCode: 0 });
mockExec.getExecOutput = getExecOutputMock;

const { main } = await import("./run_operation_update_upgrade.cjs");
await main();

// Verify binary is ./gh-aw (no prefix args) with --no-compile
expect(mockExec.exec).toHaveBeenCalledWith("./gh-aw", ["update", "--no-compile"]);
// Verify binary is ./gh-aw (no prefix args)
expect(mockExec.exec).toHaveBeenCalledWith("./gh-aw", ["update"]);
});
});

Expand Down Expand Up @@ -338,7 +391,7 @@ describe("run_operation_update_upgrade", () => {
const getExecOutputMock = vi.fn();
getExecOutputMock
.mockResolvedValueOnce({
stdout: " M .github/workflows/my-workflow.md\n?? .github/aw/actions-lock.json\n",
stdout: " M .github/agents/agentic-workflows.agent.md\n?? .github/aw/actions-lock.json\n",
stderr: "",
exitCode: 0,
})
Expand All @@ -348,7 +401,7 @@ describe("run_operation_update_upgrade", () => {

// git add fails for the first file, succeeds for others
mockExec.exec = vi.fn().mockImplementation(async (cmd, args) => {
if (cmd === "git" && args[0] === "add" && args[2] === ".github/workflows/my-workflow.md") {
if (cmd === "git" && args[0] === "add" && args[2] === ".github/agents/agentic-workflows.agent.md") {
throw new Error("git add failed");
}
return 0;
Expand Down
Loading