diff --git a/.github/workflows/design-decision-gate.lock.yml b/.github/workflows/design-decision-gate.lock.yml index e8ab3b7f111..6206dfd8428 100644 --- a/.github/workflows/design-decision-gate.lock.yml +++ b/.github/workflows/design-decision-gate.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"ef27c358a28fe7291a5fa2df62f8e26637515d08c7ef75403b06bb842fbc50b7","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f239717c54cbcb18f7bf4c8a00a9e2d9a6e46305cd9d8b9560e381be34487bfd","strict":true,"agent_id":"claude"} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.25"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.25"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.25"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.25"},{"image":"ghcr.io/github/github-mcp-server:v1.0.0"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]} # ___ _ _ # / _ \ | | (_) @@ -203,23 +203,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_8cc7a327df544c85_EOF' + cat << 'GH_AW_PROMPT_7b71667809e4e0bc_EOF' - GH_AW_PROMPT_8cc7a327df544c85_EOF + GH_AW_PROMPT_7b71667809e4e0bc_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_8cc7a327df544c85_EOF' + cat << 'GH_AW_PROMPT_7b71667809e4e0bc_EOF' Tools: add_comment(max:2), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_8cc7a327df544c85_EOF + GH_AW_PROMPT_7b71667809e4e0bc_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_8cc7a327df544c85_EOF' + cat << 'GH_AW_PROMPT_7b71667809e4e0bc_EOF' - GH_AW_PROMPT_8cc7a327df544c85_EOF + GH_AW_PROMPT_7b71667809e4e0bc_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_8cc7a327df544c85_EOF' + cat << 'GH_AW_PROMPT_7b71667809e4e0bc_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -248,14 +248,14 @@ jobs: {{/if}} - GH_AW_PROMPT_8cc7a327df544c85_EOF + GH_AW_PROMPT_7b71667809e4e0bc_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_8cc7a327df544c85_EOF' + cat << 'GH_AW_PROMPT_7b71667809e4e0bc_EOF' {{#runtime-import .github/agents/adr-writer.agent.md}} {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/design-decision-gate.md}} - GH_AW_PROMPT_8cc7a327df544c85_EOF + GH_AW_PROMPT_7b71667809e4e0bc_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -455,9 +455,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_0eaad8afda325a3f_EOF' - {"add_comment":{"hide_older_comments":true,"max":2},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_to_pull_request_branch":{"allowed_files":["docs/adr/**"],"commit_title_suffix":" [design-decision-gate]","if_no_changes":"warn","max_patch_size":1024,"patch_format":"bundle","protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","CLAUDE.md","AGENTS.md"],"protected_path_prefixes":[".github/",".agents/",".claude/"]},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_0eaad8afda325a3f_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a69af6efb55b93f1_EOF' + {"add_comment":{"hide_older_comments":true,"max":2},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_to_pull_request_branch":{"allowed_files":["docs/adr/**"],"commit_title_suffix":" [design-decision-gate]","if_no_changes":"warn","ignore_missing_branch_failure":true,"max_patch_size":1024,"patch_format":"bundle","protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","CLAUDE.md","AGENTS.md"],"protected_path_prefixes":[".github/",".agents/",".claude/"]},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_a69af6efb55b93f1_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -664,7 +664,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.25' GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_15f4afa06208d904_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_58ce56625cad4b25_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -704,7 +704,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_15f4afa06208d904_EOF + GH_AW_MCP_CONFIG_58ce56625cad4b25_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1465,7 +1465,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"docs/adr/**\"],\"commit_title_suffix\":\" [design-decision-gate]\",\"if_no_changes\":\"warn\",\"max_patch_size\":1024,\"patch_format\":\"bundle\",\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"CLAUDE.md\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"]},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"docs/adr/**\"],\"commit_title_suffix\":\" [design-decision-gate]\",\"if_no_changes\":\"warn\",\"ignore_missing_branch_failure\":true,\"max_patch_size\":1024,\"patch_format\":\"bundle\",\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"CLAUDE.md\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\",\".claude/\"]},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/design-decision-gate.md b/.github/workflows/design-decision-gate.md index 57d0d41d0fe..74c6abcf10d 100644 --- a/.github/workflows/design-decision-gate.md +++ b/.github/workflows/design-decision-gate.md @@ -22,6 +22,7 @@ safe-outputs: allowed-files: - docs/adr/** patch-format: bundle + ignore-missing-branch-failure: true commit-title-suffix: " [design-decision-gate]" noop: messages: diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index d4076e0a3a1..7869cf329f7 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -24,6 +24,25 @@ const { getGitAuthEnv } = require("./git_helpers.cjs"); /** @type {string} Safe output type handled by this module */ const HANDLER_TYPE = "push_to_pull_request_branch"; +const MISSING_BRANCH_ERROR_TEMPLATE = branchName => `Branch ${branchName} no longer exists on origin (it may have been deleted), can't push to it.`; +const MISSING_REMOTE_REF_PATTERNS = [ + "couldn't find remote ref", + "could not find remote ref", + "remote ref does not exist", + "did not match any file(s) known to git", + "unknown revision or path not in the working tree", + "fatal: couldn't find remote ref", + "exit code 128", +]; + +/** + * @param {unknown} value + * @returns {boolean} + */ +function looksLikeMissingRemoteBranchError(value) { + const text = String(value ?? "").toLowerCase(); + return MISSING_REMOTE_REF_PATTERNS.some(pattern => text.includes(pattern)); +} /** * Main handler factory for push_to_pull_request_branch @@ -36,6 +55,7 @@ async function main(config = {}) { const titlePrefix = config.title_prefix || ""; const envLabels = config.labels ? (Array.isArray(config.labels) ? config.labels : config.labels.split(",")).map(label => String(label).trim()).filter(label => label) : []; const ifNoChanges = config.if_no_changes || "warn"; + const ignoreMissingBranchFailure = config.ignore_missing_branch_failure === true; const commitTitleSuffix = config.commit_title_suffix || ""; const maxSizeKb = config.max_patch_size ? parseInt(String(config.max_patch_size), 10) : 1024; const maxCount = config.max || 0; // 0 means no limit @@ -70,6 +90,7 @@ async function main(config = {}) { core.info(`Required labels: ${envLabels.join(", ")}`); } core.info(`If no changes: ${ifNoChanges}`); + core.info(`Ignore missing branch failure: ${ignoreMissingBranchFailure}`); if (commitTitleSuffix) { core.info(`Commit title suffix: ${commitTitleSuffix}`); } @@ -464,9 +485,18 @@ async function main(config = {}) { }); if (lsRemoteResult.exitCode === 2) { + const missingBranchError = MISSING_BRANCH_ERROR_TEMPLATE(branchName); + if (ignoreMissingBranchFailure) { + core.warning(`${missingBranchError} Skipping as configured by ignore-missing-branch-failure.`); + return { + success: false, + error: missingBranchError, + skipped: true, + }; + } return { success: false, - error: `Branch ${branchName} no longer exists on origin (it may have been deleted), can't push to it.`, + error: missingBranchError, }; } @@ -488,6 +518,12 @@ async function main(config = {}) { env: { ...process.env, ...gitAuthEnv }, }); } catch (fetchError) { + const fetchErrorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError); + if (ignoreMissingBranchFailure && looksLikeMissingRemoteBranchError(fetchErrorMessage)) { + const missingBranchError = MISSING_BRANCH_ERROR_TEMPLATE(branchName); + core.warning(`${missingBranchError} Skipping as configured by ignore-missing-branch-failure.`); + return { success: false, error: missingBranchError, skipped: true }; + } return { success: false, error: `Failed to fetch branch ${branchName}: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}` }; } @@ -495,6 +531,11 @@ async function main(config = {}) { try { await exec.exec(`git rev-parse --verify origin/${branchName}`); } catch (verifyError) { + const missingBranchError = MISSING_BRANCH_ERROR_TEMPLATE(branchName); + if (ignoreMissingBranchFailure) { + core.warning(`${missingBranchError} Skipping as configured by ignore-missing-branch-failure.`); + return { success: false, error: missingBranchError, skipped: true }; + } return { success: false, error: `Branch ${branchName} does not exist on origin, can't push to it: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}` }; } diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs index c8c9b047fbb..4fdc66c4d4d 100644 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ b/actions/setup/js/push_to_pull_request_branch.test.cjs @@ -645,6 +645,22 @@ index 0000000..abc1234 expect(mockExec.exec).not.toHaveBeenCalled(); }); + it("should skip deleted branch failure when ignore_missing_branch_failure is enabled", async () => { + const patchPath = createPatchFile(); + + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 2, stdout: "", stderr: "fatal: couldn't find remote ref feature-branch" }); + + const module = await loadModule(); + const handler = await module.main({ ignore_missing_branch_failure: true }); + const result = await handler({ patch_path: patchPath }, {}); + + expect(result.success).toBe(false); + expect(result.skipped).toBe(true); + expect(result.error).toContain("no longer exists on origin"); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("ignore-missing-branch-failure")); + expect(mockExec.exec).not.toHaveBeenCalled(); + }); + it("should fail with diagnostic error when branch existence check fails for other reasons", async () => { const patchPath = createPatchFile(); @@ -689,6 +705,23 @@ index 0000000..abc1234 expect(result.error).toContain("does not exist on origin"); }); + it("should skip rev-parse missing branch failure when ignore_missing_branch_failure is enabled", async () => { + const patchPath = createPatchFile(); + + // git fetch succeeds, but git rev-parse fails + mockExec.exec.mockResolvedValueOnce(0); // fetch + mockExec.exec.mockRejectedValueOnce(new Error("fatal: Needed a single revision")); + + const module = await loadModule(); + const handler = await module.main({ ignore_missing_branch_failure: true }); + const result = await handler({ patch_path: patchPath }, {}); + + expect(result.success).toBe(false); + expect(result.skipped).toBe(true); + expect(result.error).toContain("no longer exists on origin"); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("ignore-missing-branch-failure")); + }); + it("should handle git checkout failure", async () => { const patchPath = createPatchFile(); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 84f5cace55d..23994195e59 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -7074,6 +7074,11 @@ "enum": ["warn", "error", "ignore"], "description": "Behavior when no changes to push: 'warn' (default - log warning but succeed), 'error' (fail the action), or 'ignore' (silent success)" }, + "ignore-missing-branch-failure": { + "type": "boolean", + "description": "When true, treat deleted/missing pull request branch errors as a skipped push instead of a hard failure. Useful when the PR branch may be deleted before safe outputs run.", + "default": false + }, "commit-title-suffix": { "type": "string", "description": "Optional suffix to append to generated commit titles (e.g., ' [skip ci]' to prevent triggering CI on the commit)" diff --git a/pkg/workflow/compiler_safe_outputs_handlers.go b/pkg/workflow/compiler_safe_outputs_handlers.go index d2b0f0d6d56..c42bb7abc61 100644 --- a/pkg/workflow/compiler_safe_outputs_handlers.go +++ b/pkg/workflow/compiler_safe_outputs_handlers.go @@ -411,6 +411,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("title_prefix", c.TitlePrefix). AddStringSlice("labels", c.Labels). AddIfNotEmpty("if_no_changes", c.IfNoChanges). + AddIfTrue("ignore_missing_branch_failure", c.IgnoreMissingBranchFailure). AddIfNotEmpty("commit_title_suffix", c.CommitTitleSuffix). AddDefault("max_patch_size", maxPatchSize). AddIfNotEmpty("target-repo", c.TargetRepoSlug). diff --git a/pkg/workflow/push_to_pull_request_branch.go b/pkg/workflow/push_to_pull_request_branch.go index c603100b11f..e3a31c1538d 100644 --- a/pkg/workflow/push_to_pull_request_branch.go +++ b/pkg/workflow/push_to_pull_request_branch.go @@ -16,6 +16,7 @@ type PushToPullRequestBranchConfig struct { TitlePrefix string `yaml:"title-prefix,omitempty"` // Required title prefix for pull request validation Labels []string `yaml:"labels,omitempty"` // Required labels for pull request validation IfNoChanges string `yaml:"if-no-changes,omitempty"` // Behavior when no changes to push: "warn", "error", or "ignore" (default: "warn") + IgnoreMissingBranchFailure bool `yaml:"ignore-missing-branch-failure,omitempty"` // When true, missing/deleted target branches are treated as skipped instead of hard failures. CommitTitleSuffix string `yaml:"commit-title-suffix,omitempty"` // Optional suffix to append to generated commit titles GithubTokenForExtraEmptyCommit string `yaml:"github-token-for-extra-empty-commit,omitempty"` // Token used to push an empty commit to trigger CI events. Use a PAT or "app" for GitHub App auth. TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository push to pull request branch @@ -113,6 +114,13 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any) } } + // Parse ignore-missing-branch-failure (optional, defaults to false) + if ignoreMissingBranchFailure, exists := configMap["ignore-missing-branch-failure"]; exists { + if ignoreMissingBranchFailureBool, ok := ignoreMissingBranchFailure.(bool); ok { + pushToBranchConfig.IgnoreMissingBranchFailure = ignoreMissingBranchFailureBool + } + } + // Parse title-prefix using shared helper pushToBranchConfig.TitlePrefix = parseTitlePrefixFromConfig(configMap) diff --git a/pkg/workflow/push_to_pull_request_branch_test.go b/pkg/workflow/push_to_pull_request_branch_test.go index 48210217546..d42c6ef7397 100644 --- a/pkg/workflow/push_to_pull_request_branch_test.go +++ b/pkg/workflow/push_to_pull_request_branch_test.go @@ -93,6 +93,43 @@ Please make changes and push them to the feature branch. } } +func TestPushToPullRequestBranchIgnoreMissingBranchFailureConfig(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + + testMarkdown := `--- +on: + pull_request: + types: [opened, synchronize] +safe-outputs: + push-to-pull-request-branch: + ignore-missing-branch-failure: true +--- + +# Test Push to PR Branch Missing Branch Ignore +` + + mdFile := filepath.Join(tmpDir, "test-push-to-pull-request-branch-ignore-missing.md") + if err := os.WriteFile(mdFile, []byte(testMarkdown), 0644); err != nil { + t.Fatalf("Failed to write test markdown file: %v", err) + } + + compiler := NewCompiler() + if err := compiler.CompileWorkflow(mdFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(mdFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + if !strings.Contains(lockContentStr, `"ignore_missing_branch_failure":true`) && !strings.Contains(lockContentStr, `"ignore_missing_branch_failure": true`) { + t.Errorf("Generated workflow should contain ignore_missing_branch_failure in handler config JSON") + } +} + func TestPushToPullRequestBranchWithTargetAsterisk(t *testing.T) { // Create a temporary directory for the test tmpDir := testutil.TempDir(t, "test-*")