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}
+
+
+
---