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
2 changes: 0 additions & 2 deletions .github/workflows/code-scanning-fixer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/codex-github-remote-mcp-test.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/commit-changes-analyzer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/example-permissions-warning.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/firewall.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/notion-issue-summary.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/python-data-charts.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/release.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/repo-audit-analyzer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/research.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/technical-doc-writer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/test-create-pr-error-handling.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/test-dispatcher.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/test-project-url-default.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/test-workflow.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions .github/workflows/video-analyzer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions actions/setup/js/checkout_pr_branch.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("checkout_pr_branch.cjs", () => {
setOutput: vi.fn(),
startGroup: vi.fn(),
endGroup: vi.fn(),
exportVariable: vi.fn(),
summary: {
addRaw: vi.fn().mockReturnThis(),
write: vi.fn().mockResolvedValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,13 @@ describe("create_discussion category normalization", () => {
});

afterEach(() => {
// Restore environment
process.env = originalEnv;
// Restore environment by mutating process.env in place
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key];
}
}
Object.assign(process.env, originalEnv);
vi.clearAllMocks();
});

Expand Down
9 changes: 7 additions & 2 deletions actions/setup/js/create_discussion_fallback.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,13 @@ describe("create_discussion fallback with close_older_discussions", () => {
});

afterEach(() => {
// Restore environment
process.env = originalEnv;
// Restore environment by mutating process.env in place
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key];
}
}
Object.assign(process.env, originalEnv);
vi.clearAllMocks();
});

Expand Down
9 changes: 7 additions & 2 deletions actions/setup/js/create_discussion_labels.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,13 @@ describe("create_discussion with labels", () => {
});

afterEach(() => {
// Restore environment
process.env = originalEnv;
// Restore environment by mutating process.env in place
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key];
}
}
Object.assign(process.env, originalEnv);

// Clear mocks
vi.clearAllMocks();
Expand Down
10 changes: 7 additions & 3 deletions actions/setup/js/create_issue.test.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @ts-check
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createRequire } from "module";

Expand Down Expand Up @@ -81,8 +80,13 @@ describe("create_issue", () => {
});

afterEach(() => {
// Restore original environment
process.env = originalEnv;
// Restore environment by mutating process.env in place
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key];
}
}
Object.assign(process.env, originalEnv);
vi.clearAllMocks();
});

Expand Down
38 changes: 34 additions & 4 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,10 @@ async function main(config = {}) {
core.info(`Created new branch from base: ${branchName}`);

// Apply the patch using git CLI (skip if empty)
// Track number of new commits pushed so we can restrict the extra empty commit
// to branches with exactly one new commit (security: prevents use of CI trigger
// token on multi-commit branches where workflow files may have been modified).
let newCommitCount = 0;
if (!isEmpty) {
core.info("Applying patch...");

Expand All @@ -565,8 +569,10 @@ async function main(config = {}) {
}

// Patches are created with git format-patch, so use git am to apply them
// Use --3way to handle cross-repo patches where the patch base may differ from target repo
// This allows git to resolve create-vs-modify mismatches when a file exists in target but not source
try {
await exec.exec(`git am ${patchFilePath}`);
await exec.exec(`git am --3way ${patchFilePath}`);
core.info("Patch applied successfully");
} catch (patchError) {
core.error(`Failed to apply patch: ${patchError instanceof Error ? patchError.message : String(patchError)}`);
Expand Down Expand Up @@ -616,6 +622,17 @@ async function main(config = {}) {

await exec.exec(`git push origin ${branchName}`);
core.info("Changes pushed to branch");

// Count new commits on PR branch relative to base, used to restrict
// the extra empty CI-trigger commit to exactly 1 new commit.
try {
const { stdout: countStr } = await exec.getExecOutput("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`]);
newCommitCount = parseInt(countStr.trim(), 10);
core.info(`${newCommitCount} new commit(s) on branch relative to origin/${baseBranch}`);
} catch {
// Non-fatal - newCommitCount stays 0, extra empty commit will be skipped
core.info("Could not count new commits - extra empty commit will be skipped");
}
} catch (pushError) {
// Push failed - create fallback issue instead of PR (if fallback is enabled)
core.error(`Git push failed: ${pushError instanceof Error ? pushError.message : String(pushError)}`);
Expand Down Expand Up @@ -664,8 +681,8 @@ To apply the patch locally:
gh run download ${runId} -n agent-artifacts -D /tmp/agent-artifacts-${runId}

# The patch file will be at agent-artifacts/tmp/gh-aw/${patchFileName} after download
# Apply the patch
git am /tmp/agent-artifacts-${runId}/${patchFileName}
# Apply the patch (--3way handles cross-repo patches where files may already exist)
git am --3way /tmp/agent-artifacts-${runId}/${patchFileName}
\`\`\`
${patchPreview}`;

Expand Down Expand Up @@ -750,6 +767,16 @@ ${patchPreview}`;

await exec.exec(`git push origin ${branchName}`);
core.info("Empty branch pushed successfully");

// Count new commits (will be 1 from the Initialize commit)
try {
const { stdout: countStr } = await exec.getExecOutput("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`]);
newCommitCount = parseInt(countStr.trim(), 10);
core.info(`${newCommitCount} new commit(s) on branch relative to origin/${baseBranch}`);
} catch {
// Non-fatal - newCommitCount stays 0, extra empty commit will be skipped
core.info("Could not count new commits - extra empty commit will be skipped");
}
} catch (pushError) {
const error = `Failed to push empty branch: ${pushError instanceof Error ? pushError.message : String(pushError)}`;
core.error(error);
Expand Down Expand Up @@ -840,12 +867,15 @@ ${patchPreview}`;
)
.write();

// Push an extra empty commit if a token is configured.
// Push an extra empty commit if a token is configured and exactly 1 new commit was pushed.
// This works around the GITHUB_TOKEN limitation where pushes don't trigger CI events.
// Restricting to exactly 1 new commit prevents the CI trigger token being used on
// multi-commit branches where workflow files may have been iteratively modified.
const ciTriggerResult = await pushExtraEmptyCommit({
branchName,
repoOwner: repoParts.owner,
repoName: repoParts.repo,
newCommitCount,
});
if (ciTriggerResult.success && !ciTriggerResult.skipped) {
core.info("Extra empty commit pushed - CI checks should start shortly");
Expand Down
10 changes: 9 additions & 1 deletion actions/setup/js/extra_empty_commit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,24 @@
* @param {string} options.repoOwner - Repository owner
* @param {string} options.repoName - Repository name
* @param {string} [options.commitMessage] - Custom commit message (default: "ci: trigger CI checks")
* @param {number} [options.newCommitCount] - Number of new commits being pushed. Only pushes the
* empty commit when exactly 1 new commit was pushed, preventing accidental workflow-file
* modifications on multi-commit branches and reducing loop risk.
* @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>}
*/
async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage }) {
async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage, newCommitCount }) {
const token = process.env.GH_AW_CI_TRIGGER_TOKEN;

if (!token || !token.trim()) {
core.info("No extra empty commit token configured - skipping");
return { success: true, skipped: true };
}

if (newCommitCount !== undefined && newCommitCount !== 1) {
core.info(`Skipping extra empty commit: ${newCommitCount} new commit(s) pushed (only triggers for exactly 1 commit)`);
return { success: true, skipped: true };
}

core.info("Extra empty commit token detected - pushing empty commit to trigger CI events");

try {
Expand Down
Loading
Loading