diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 981db2d394b..6ddede69153 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -25,7 +25,7 @@ const { getBaseBranch } = require("./get_base_branch.cjs"); const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); const { checkFileProtection } = require("./manifest_file_helpers.cjs"); -const { renderTemplateFromFile } = require("./messages_core.cjs"); +const { renderTemplateFromFile, buildProtectedFileList, encodePathSegments } = require("./messages_core.cjs"); const { COPILOT_REVIEWER_BOT, FAQ_CREATE_PR_PERMISSIONS_URL } = require("./constants.cjs"); const { isStagedMode } = require("./safe_output_helpers.cjs"); @@ -977,7 +977,11 @@ ${patchPreview}`; // patch artifact download instructions instead of the compare URL. if (manifestProtectionFallback) { const allFound = manifestProtectionFallback; - const filesFormatted = allFound.map(f => `\`${f}\``).join(", "); + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + // Use head branch (branchName) for links when push succeeded; fall back to baseBranch + // for the push-failed case where the head branch is not yet on the remote. + const branchForLinks = manifestProtectionPushFailedError ? baseBranch : branchName; + const fileList = buildProtectedFileList(allFound, githubServer, repoParts.owner, repoParts.repo, branchForLinks); let fallbackBody; if (manifestProtectionPushFailedError) { @@ -989,7 +993,7 @@ ${patchPreview}`; fallbackBody = renderTemplateFromFile(pushFailedTemplatePath, { main_body: mainBodyContent, footer: footerContent, - files: filesFormatted, + files: fileList, run_id: String(runId), branch_name: branchName, base_branch: baseBranch, @@ -999,15 +1003,14 @@ ${patchPreview}`; }); } else { // Normal case — push succeeded, provide compare URL. - const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; - const encodedBase = baseBranch.split("/").map(encodeURIComponent).join("/"); - const encodedHead = branchName.split("/").map(encodeURIComponent).join("/"); + const encodedBase = encodePathSegments(baseBranch); + const encodedHead = encodePathSegments(branchName); const createPrUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/compare/${encodedBase}...${encodedHead}?expand=1&title=${encodeURIComponent(title)}`; const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/manifest_protection_create_pr_fallback.md`; fallbackBody = renderTemplateFromFile(templatePath, { main_body: mainBodyContent, footer: footerContent, - files: filesFormatted, + files: fileList, create_pr_url: createPrUrl, }); } diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs index 2a29f94aec0..ecd4fa40e58 100644 --- a/actions/setup/js/messages_core.cjs +++ b/actions/setup/js/messages_core.cjs @@ -111,9 +111,62 @@ function toSnakeCase(obj) { ); } +/** + * RFC3986-compliant encoding for individual URI components. + * Starts with encodeURIComponent and then additionally percent-encodes + * characters that are still reserved in RFC3986 (`!`, `'`, `(`, `)`, `*`). + * This prevents these characters from breaking Markdown link parsing. + * @param {string} value + * @returns {string} + */ +function encodeRFC3986URIComponent(value) { + return encodeURIComponent(value).replace(/[!'()*]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")); +} + +/** + * URL-encode each segment of a slash-separated path. + * Preserves the slash separators while encoding special characters in each segment + * using RFC3986-compliant encoding to avoid breaking Markdown link syntax. + * @param {string} path - A slash-separated path (e.g. branch name or file path) + * @returns {string} Path with each segment individually URL-encoded + */ +function encodePathSegments(path) { + return path.split("/").map(encodeRFC3986URIComponent).join("/"); +} + +/** + * Build a markdown list of protected files with clickable GitHub URLs. + * Both the branch name and each file path segment are individually URL-encoded. + * Basename-only entries (no slash) are rendered as code spans to avoid linking + * to a potentially incorrect root-level path. + * @param {string[]} files - Array of file paths or basenames + * @param {string} githubServer - GitHub server URL (e.g. "https://github.com") + * @param {string} owner - Repository owner + * @param {string} repo - Repository name + * @param {string} branch - Branch name (will be URL-encoded internally) + * @returns {string} Markdown list with one entry per line + */ +function buildProtectedFileList(files, githubServer, owner, repo, branch) { + const encodedBranch = encodePathSegments(branch); + return files + .map(f => { + // If the entry looks like a full path (contains a slash), render it as a blob link. + // Otherwise, treat it as a basename-only entry (e.g. from manifest matching) and + // render it as a code span to avoid linking to a potentially incorrect root path. + if (f.includes("/")) { + const encodedPath = encodePathSegments(f); + return `- [${f}](${githubServer}/${owner}/${repo}/blob/${encodedBranch}/${encodedPath})`; + } + return `- \`${f}\``; + }) + .join("\n"); +} + module.exports = { getMessages, renderTemplate, renderTemplateFromFile, toSnakeCase, + encodePathSegments, + buildProtectedFileList, }; diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index c8351960008..02edf8f0244 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -15,7 +15,7 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { checkFileProtection } = require("./manifest_file_helpers.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); -const { renderTemplateFromFile } = require("./messages_core.cjs"); +const { renderTemplateFromFile, buildProtectedFileList } = require("./messages_core.cjs"); const { getGitAuthEnv } = require("./git_helpers.cjs"); /** @@ -397,9 +397,10 @@ async function main(config = {}) { const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; const prUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/pull/${pullNumber}`; const issueTitle = `[gh-aw] Protected Files: ${prTitle || `PR #${pullNumber}`}`; + const fileList = buildProtectedFileList(protectedFilesForFallback, githubServer, repoParts.owner, repoParts.repo, branchName); const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/manifest_protection_push_to_pr_fallback.md`; const issueBody = renderTemplateFromFile(templatePath, { - files: protectedFilesForFallback.map(f => `\`${f}\``).join(", "), + files: fileList, pull_number: pullNumber, pr_url: prUrl, run_url: runUrl, diff --git a/actions/setup/md/manifest_protection_create_pr_fallback.md b/actions/setup/md/manifest_protection_create_pr_fallback.md index ee94f7deef1..e68d6eab368 100644 --- a/actions/setup/md/manifest_protection_create_pr_fallback.md +++ b/actions/setup/md/manifest_protection_create_pr_fallback.md @@ -5,12 +5,17 @@ > [!WARNING] > 🛡️ **Protected Files** > -> This was originally intended as a pull request, but the patch modifies protected files: {files}. -> -> These files may affect project dependencies, CI/CD pipelines, or agent behaviour. **Please review the changes carefully** before creating the pull request. +> This was originally intended as a pull request, but the patch modifies protected files. These files may affect project dependencies, CI/CD pipelines, or agent behaviour. **Please review the changes carefully** before creating the pull request. > > **[Click here to create the pull request once you have reviewed the changes]({create_pr_url})** +
+🔒 Protected files + +{files} + +
+ To route changes like this to a review issue instead of blocking, configure `protected-files: fallback-to-issue` in your workflow configuration. {footer} diff --git a/actions/setup/md/manifest_protection_push_failed_fallback.md b/actions/setup/md/manifest_protection_push_failed_fallback.md index b38890d13f7..75a693f7b1f 100644 --- a/actions/setup/md/manifest_protection_push_failed_fallback.md +++ b/actions/setup/md/manifest_protection_push_failed_fallback.md @@ -5,9 +5,16 @@ > [!WARNING] > 🛡️ **Protected Files — Push Permission Denied** > -> This was originally intended as a pull request, but the patch modifies protected files: {files}. -> -> The push was rejected because GitHub Actions does not have `workflows` permission to push these changes, and is never allowed to make such changes, or other authorization being used does not have this permission. A human must create the pull request manually. +> This was originally intended as a pull request, but the patch modifies protected files. A human must create the pull request manually. + +
+🔒 Protected files + +{files} + +The push was rejected because GitHub Actions does not have `workflows` permission to push these changes, and is never allowed to make such changes, or other authorization being used does not have this permission. + +
📋 Create the pull request manually diff --git a/actions/setup/md/manifest_protection_push_to_pr_fallback.md b/actions/setup/md/manifest_protection_push_to_pr_fallback.md index 567ad7ef57e..a6c64fab09e 100644 --- a/actions/setup/md/manifest_protection_push_to_pr_fallback.md +++ b/actions/setup/md/manifest_protection_push_to_pr_fallback.md @@ -1,12 +1,19 @@ > [!WARNING] > 🛡️ **Protected Files** > -> The push to pull request branch was blocked because the patch modifies protected files: {files}. +> The push to pull request branch was blocked because the patch modifies protected files. > > **Target Pull Request:** [#{pull_number}]({pr_url}) > > **Please review the changes carefully** before pushing them to the pull request branch. These files may affect project dependencies, CI/CD pipelines, or agent behaviour. +
+🔒 Protected files + +{files} + +
+ ---