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
17 changes: 10 additions & 7 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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,
});
}
Expand Down
53 changes: 53 additions & 0 deletions actions/setup/js/messages_core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
5 changes: 3 additions & 2 deletions actions/setup/js/push_to_pull_request_branch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

/**
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 8 additions & 3 deletions actions/setup/md/manifest_protection_create_pr_fallback.md
Original file line number Diff line number Diff line change
Expand Up @@ -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})**

<details>
<summary>🔒 Protected files</summary>

{files}

</details>

To route changes like this to a review issue instead of blocking, configure `protected-files: fallback-to-issue` in your workflow configuration.

{footer}
13 changes: 10 additions & 3 deletions actions/setup/md/manifest_protection_push_failed_fallback.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary>🔒 Protected files</summary>

{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.

</details>

<details>
<summary><b>📋 Create the pull request manually</b></summary>
Expand Down
9 changes: 8 additions & 1 deletion actions/setup/md/manifest_protection_push_to_pr_fallback.md
Original file line number Diff line number Diff line change
@@ -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.

<details>
<summary>🔒 Protected files</summary>

{files}

</details>

---

<details>
Expand Down
Loading