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
12 changes: 12 additions & 0 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const { generateWorkflowIdMarker } = require("./generate_footer.cjs");
const { parseBoolTemplatable } = require("./templatable.cjs");
const { generateFooterWithMessages } = require("./messages_footer.cjs");
const { normalizeBranchName } = require("./normalize_branch_name.cjs");
const { pushExtraEmptyCommit } = require("./extra_empty_commit.cjs");

/**
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
Expand Down Expand Up @@ -839,6 +840,17 @@ ${patchPreview}`;
)
.write();

// Push an extra empty commit if a token is configured.
// This works around the GITHUB_TOKEN limitation where pushes don't trigger CI events.
const ciTriggerResult = await pushExtraEmptyCommit({
branchName,
repoOwner: repoParts.owner,
repoName: repoParts.repo,
});
if (ciTriggerResult.success && !ciTriggerResult.skipped) {
core.info("Extra empty commit pushed - CI checks should start shortly");
}

// Return success with PR details
return {
success: true,
Expand Down
119 changes: 119 additions & 0 deletions actions/setup/js/extra_empty_commit.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// @ts-check
/// <reference types="@actions/github-script" />

/**
* @fileoverview Extra Empty Commit Helper
*
* Pushes an empty commit to a branch using a different token to trigger CI events.
* This works around the GitHub Actions limitation where events created with
* GITHUB_TOKEN do not trigger other workflow runs.
*
* The token comes from `github-token-for-extra-empty-commit` in safe-outputs config
* (passed as GH_AW_EXTRA_EMPTY_COMMIT_TOKEN env var), or `app` for GitHub App authentication.
*/

/**
* Push an empty commit to a branch using a dedicated token.
* This commit is pushed with different authentication so that push/PR events
* are triggered for CI checks to run.
*
* @param {Object} options - Options for the extra empty commit
* @param {string} options.branchName - The branch to push the empty commit to
* @param {string} options.repoOwner - Repository owner
* @param {string} options.repoName - Repository name
* @param {string} [options.commitMessage] - Custom commit message (default: "ci: trigger CI checks")
* @returns {Promise<{success: boolean, skipped?: boolean, error?: string}>}
*/
async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMessage }) {
const token = process.env.GH_AW_EXTRA_EMPTY_COMMIT_TOKEN;

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

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

try {
// Cycle prevention: count empty commits in the last 60 commits on this branch.
// If 30 or more are empty, skip pushing to avoid infinite trigger loops.
const MAX_EMPTY_COMMITS = 30;
const COMMITS_TO_CHECK = 60;
let emptyCommitCount = 0;

try {
let logOutput = "";
// List last N commits: for each, output "COMMIT:<hash>" then changed file names.
// Empty commits will have no files listed after the hash line.
await exec.exec("git", ["log", `--max-count=${COMMITS_TO_CHECK}`, "--format=COMMIT:%H", "--name-only", "HEAD"], {
listeners: {
stdout: data => {
logOutput += data.toString();
},
},
silent: true,
});
// Split by COMMIT: markers; each chunk starts with the hash, followed by filenames
const chunks = logOutput.split("COMMIT:").filter(c => c.trim());
for (const chunk of chunks) {
const lines = chunk.split("\n").filter(l => l.trim());
// First line is the hash, remaining lines are changed files
if (lines.length <= 1) {
emptyCommitCount++;
}
}
} catch {
// If we can't check, default to allowing the push
emptyCommitCount = 0;
}

if (emptyCommitCount >= MAX_EMPTY_COMMITS) {
core.warning(`Cycle prevention: found ${emptyCommitCount} empty commits in the last ${COMMITS_TO_CHECK} commits on ${branchName}. ` + `Skipping extra empty commit to avoid potential infinite loop.`);
return { success: true, skipped: true };
}

core.info(`Cycle check passed: ${emptyCommitCount} empty commit(s) in last ${COMMITS_TO_CHECK} (limit: ${MAX_EMPTY_COMMITS})`);

// Configure git remote with the token for authentication
const remoteUrl = `https://x-access-token:${token}@github.com/${repoOwner}/${repoName}.git`;

// Add a temporary remote with the token
try {
await exec.exec("git", ["remote", "remove", "ci-trigger"]);
} catch {
// Remote doesn't exist yet, that's fine
}
await exec.exec("git", ["remote", "add", "ci-trigger", remoteUrl]);

// Create and push an empty commit
const message = commitMessage || "ci: trigger CI checks";
await exec.exec("git", ["commit", "--allow-empty", "-m", message]);
await exec.exec("git", ["push", "ci-trigger", branchName]);

core.info(`Extra empty commit pushed to ${branchName} successfully`);

// Clean up the temporary remote
try {
await exec.exec("git", ["remote", "remove", "ci-trigger"]);
} catch {
// Non-fatal cleanup error
}

return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
core.warning(`Failed to push extra empty commit: ${errorMessage}`);

// Clean up the temporary remote on failure
try {
await exec.exec("git", ["remote", "remove", "ci-trigger"]);
} catch {
// Non-fatal cleanup error
}

// Extra empty commit failure is not fatal - the main push already succeeded
return { success: false, error: errorMessage };
}
}

module.exports = { pushExtraEmptyCommit };
Loading
Loading