From 873dd6c28e2ccf37b6d359e2a4a51ab4450c5a60 Mon Sep 17 00:00:00 2001 From: Federico Grandi Date: Fri, 14 Aug 2026 21:10:39 +0200 Subject: [PATCH 1/2] fix: block scheme:: remote helpers and restrict git transports Harden fetch/pull/push argument handling against ext:: (and other scheme::) remote-helper URLs, and allowlist safe transports by default, with allow_unsafe_git_protocols as an explicit opt-out. Co-authored-by: Cursor --- README.md | 11 ++++++++ action.yml | 5 ++++ lib/index.js | 2 +- src/io.ts | 8 ++++++ src/main.ts | 56 ++++++++++++++++++++++++------------- src/util.ts | 31 +++++++++++++++++++- test/integration/helpers.ts | 2 ++ test/util.test.ts | 42 ++++++++++++++++++++++++++++ 8 files changed, 135 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a1f09fb9..36b6e83b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,11 @@ Add a step like this to your workflow: # Default: false dry_run: true + # If true, allow custom git transports / scheme:: remote-helper URLs in argument inputs. + # Keep false unless you need a custom remote helper and fully trust those inputs. + # Default: false + allow_unsafe_git_protocols: false + # Arguments for the git fetch command. If set to false, the action won't fetch the repo. # For more info as to why fetching is usually recommended, please see the "Performance on large repos" FAQ. # Default: --tags --force @@ -111,10 +116,16 @@ Multiple options let you provide the `git` arguments that you want the action to What does this mean for you? It means that strings that contain a lot of nested quotes may be parsed incorrectly, and that specific ways of declaring arguments may not be supported by these libraries. If you're having issues with your argument strings you can check whether they're being parsed correctly either by [enabling debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) for your workflow runs or by testing it directly with `string-argv` ([RunKit demo](https://npm.runkit.com/string-argv)): if each argument and option is parsed correctly you'll see an array where every string is an option or value. Remote-helper overrides (`--upload-pack`, `--receive-pack`, `--exec`, and abbreviations of those) are rejected: they can make git run an arbitrary Git transport program during fetch/pull/push. +Remote-helper URL forms (`ext::…` and other `scheme::` tokens) are rejected for the same reason. +Git child processes are also limited to the `https`, `http`, `ssh`, `file`, and `git` transports (`GIT_ALLOW_PROTOCOL`) unless you set [`allow_unsafe_git_protocols`](#allow_unsafe_git_protocols) to `true` (only for trusted custom remotes/helpers). Message-from-file flags (`-F`, `--file`, abbreviations such as `--fi`, and short-option clusters that include `F` such as `-aF`) are rejected: they can embed arbitrary runner filesystem contents into a tag or commit message and, with a push, into the repository history. Unmatched `'` / `"` quotes are also rejected: `string-argv` can otherwise split on an odd quote and turn part of a value into extra flags (for example a branch name like `fix'--force` becoming `fix` plus `--force`). Do not interpolate untrusted data (for example values from `github.event.*`, `github.head_ref`, or repository content that contributors can edit) into `fetch`, `pull`, `push`, `tag`, `tag_push`, or `commit` without sanitizing them first. When the branch name is dynamic, prefer the default `push: true` with [`new_branch`](#creating-a-new-branch) instead of embedding the ref in a custom `push` string. +### Allow unsafe git protocols + +Set `allow_unsafe_git_protocols: true` only if you need a custom remote helper or a transport outside the default allowlist (`https`, `http`, `ssh`, `file`, `git`). This disables both the `GIT_ALLOW_PROTOCOL` restriction and the rejection of `scheme::` tokens in git argument inputs. It does **not** re-enable blocked options such as `--upload-pack` or `-F`/`--file`. Treat this like a break-glass setting: only enable it with fully trusted, non-interpolated argument strings. + ### Adding files The action adds files using a regular `git add` command, so you can put every kind of argument in the `add` option. For example, if you want to force-add a file: `./path/to/file.txt --force`. diff --git a/action.yml b/action.yml index b6d29711..8ffd3508 100644 --- a/action.yml +++ b/action.yml @@ -68,6 +68,11 @@ inputs: description: Arguments for the git push --tags command (any additional argument will be added after --tags) required: false + allow_unsafe_git_protocols: + description: If true, disables the transport protocol allowlist (https/http/ssh/file/git) and allows scheme:: remote-helper URLs in git argument inputs. Keep false unless you need a custom remote helper and fully trust those inputs. + required: false + default: 'false' + # Input not required from the user github_token: description: The token used to make requests to the GitHub API. It's NOT used to make commits and should not be changed. diff --git a/lib/index.js b/lib/index.js index 0fe78108..7bc1dcf4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,4 +7,4 @@ var uo=Symbol("NOT_RESOLVED");var go=Symbol("MERGE_KEY");function defineScalarTa /* v8 ignore next -- @preserve */ ()=>new ArrayBuffer(0))}}function isJSONResponse(e){return e.type==="application/json"||e.type==="application/scim+json"}function toErrorMessage(e){if(typeof e==="string"){return e}if(e instanceof ArrayBuffer){return"Unknown error"}if("message"in e){const t="documentation_url"in e?` - ${e.documentation_url}`:"";return Array.isArray(e.errors)?`${e.message}: ${e.errors.map(e=>JSON.stringify(e)).join(", ")}${t}`:`${e.message}${t}`}return`Unknown error: ${JSON.stringify(e)}`}function dist_bundle_withDefaults(e,t){const r=e.defaults(t);const newApi=function(e,t){const s=r.merge(e,t);if(!s.request||!s.request.hook){return fetchWrapper(r.parse(s))}const request2=(e,t)=>fetchWrapper(r.parse(r.merge(e,t)));Object.assign(request2,{endpoint:r,defaults:dist_bundle_withDefaults.bind(null,r)});return s.request.hook(request2,s)};return Object.assign(newApi,{endpoint:r,defaults:dist_bundle_withDefaults.bind(null,r)})}var jA=dist_bundle_withDefaults(JA,qA); /* v8 ignore next -- @preserve */ -/* v8 ignore else -- @preserve */var zA="0.0.0-development";function _buildMessageForResponseErrors(e){return`Request failed due to following response errors:\n`+e.errors.map(e=>` - ${e.message}`).join("\n")}var KA=class extends Error{constructor(e,t,r){super(_buildMessageForResponseErrors(r));this.request=e;this.headers=t;this.response=r;this.errors=r.errors;this.data=r.data;if(Error.captureStackTrace){Error.captureStackTrace(this,this.constructor)}}name="GraphqlResponseError";errors;data};var ZA=["method","baseUrl","url","headers","request","query","mediaType","operationName"];var $A=["query","method","url"];var XA=/\/api\/v3\/?$/;function graphql(e,t,r){if(r){if(typeof t==="string"&&"query"in r){return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`))}for(const e in r){if(!$A.includes(e))continue;return Promise.reject(new Error(`[@octokit/graphql] "${e}" cannot be used as variable name`))}}const s=typeof t==="string"?Object.assign({query:t},r):t;const n=Object.keys(s).reduce((e,t)=>{if(ZA.includes(t)){e[t]=s[t];return e}if(!e.variables){e.variables={}}e.variables[t]=s[t];return e},{});const o=s.baseUrl||e.endpoint.DEFAULTS.baseUrl;if(XA.test(o)){n.url=o.replace(XA,"/api/graphql")}return e(n).then(e=>{if(e.data.errors){const t={};for(const r of Object.keys(e.headers)){t[r]=e.headers[r]}throw new KA(n,t,e.data)}return e.data.data})}function graphql_dist_bundle_withDefaults(e,t){const r=e.defaults(t);const newApi=(e,t)=>graphql(r,e,t);return Object.assign(newApi,{defaults:graphql_dist_bundle_withDefaults.bind(null,r),endpoint:r.endpoint})}var ea=graphql_dist_bundle_withDefaults(jA,{headers:{"user-agent":`octokit-graphql.js/${zA} ${getUserAgent()}`},method:"POST",url:"/graphql"});function withCustomRequest(e){return graphql_dist_bundle_withDefaults(e,{method:"POST",url:"/graphql"})}var ta="(?:[a-zA-Z0-9_-]+)";var ra="\\.";var sa=new RegExp(`^${ta}${ra}${ta}${ra}${ta}$`);var na=sa.test.bind(sa);async function auth(e){const t=na(e);const r=e.startsWith("v1.")||e.startsWith("ghs_");const s=e.startsWith("ghu_");const n=t?"app":r?"installation":s?"user-to-server":"oauth";return{type:"token",token:e,tokenType:n}}function withAuthorizationPrefix(e){if(e.split(/\./).length===3){return`bearer ${e}`}return`token ${e}`}async function hook(e,t,r,s){const n=t.endpoint.merge(r,s);n.headers.authorization=withAuthorizationPrefix(e);return t(n)}var oa=function createTokenAuth2(e){if(!e){throw new Error("[@octokit/auth-token] No token passed to createTokenAuth")}if(typeof e!=="string"){throw new Error("[@octokit/auth-token] Token passed to createTokenAuth is not a string")}e=e.replace(/^(token|bearer) +/i,"");return Object.assign(auth.bind(null,e),{hook:hook.bind(null,e)})};const ia="7.0.6";const dist_src_noop=()=>{};const Aa=console.warn.bind(console);const aa=console.error.bind(console);function dist_src_createLogger(e={}){if(typeof e.debug!=="function"){e.debug=dist_src_noop}if(typeof e.info!=="function"){e.info=dist_src_noop}if(typeof e.warn!=="function"){e.warn=Aa}if(typeof e.error!=="function"){e.error=aa}return e}const ca=`octokit-core.js/${ia} ${getUserAgent()}`;class Octokit{static VERSION=ia;static defaults(e){const t=class extends(this){constructor(...t){const r=t[0]||{};if(typeof e==="function"){super(e(r));return}super(Object.assign({},e,r,r.userAgent&&e.userAgent?{userAgent:`${r.userAgent} ${e.userAgent}`}:null))}};return t}static plugins=[];static plugin(...e){const t=this.plugins;const r=class extends(this){static plugins=t.concat(e.filter(e=>!t.includes(e)))};return r}constructor(e={}){const t=new OA.Collection;const r={baseUrl:jA.endpoint.DEFAULTS.baseUrl,headers:{},request:Object.assign({},e.request,{hook:t.bind(null,"request")}),mediaType:{previews:[],format:""}};r.headers["user-agent"]=e.userAgent?`${e.userAgent} ${ca}`:ca;if(e.baseUrl){r.baseUrl=e.baseUrl}if(e.previews){r.mediaType.previews=e.previews}if(e.timeZone){r.headers["time-zone"]=e.timeZone}this.request=jA.defaults(r);this.graphql=withCustomRequest(this.request).defaults(r);this.log=dist_src_createLogger(e.log);this.hook=t;if(!e.authStrategy){if(!e.auth){this.auth=async()=>({type:"unauthenticated"})}else{const r=oa(e.auth);t.wrap("request",r.hook);this.auth=r}}else{const{authStrategy:r,...s}=e;const n=r(Object.assign({request:this.request,log:this.log,octokit:this,octokitOptions:s},e.auth));t.wrap("request",n.hook);this.auth=n}const s=this.constructor;for(let t=0;t({async next(){if(!l)return{done:true};try{const e=await n({method:o,url:l,headers:a});const t=normalizePaginatedListResponse(e);l=((t.headers.link||"").match(/<([^<>]+)>;\s*rel="next"/)||[])[1];if(!l&&"total_commits"in t.data){const e=new URL(t.url);const r=e.searchParams;const s=parseInt(r.get("page")||"1",10);const n=parseInt(r.get("per_page")||"250",10);if(s*n{if(n.done){return t}let o=false;function done(){o=true}t=t.concat(s?s(n.value,done):n.value.data);if(o){return t}return gather(e,t,r,s)})}var Ea=Object.assign(paginate,{iterator:iterator});var fa=null&&["GET /advisories","GET /app/hook/deliveries","GET /app/installation-requests","GET /app/installations","GET /assignments/{assignment_id}/accepted_assignments","GET /classrooms","GET /classrooms/{classroom_id}/assignments","GET /enterprises/{enterprise}/code-security/configurations","GET /enterprises/{enterprise}/code-security/configurations/{configuration_id}/repositories","GET /enterprises/{enterprise}/dependabot/alerts","GET /enterprises/{enterprise}/teams","GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships","GET /enterprises/{enterprise}/teams/{enterprise-team}/organizations","GET /events","GET /gists","GET /gists/public","GET /gists/starred","GET /gists/{gist_id}/comments","GET /gists/{gist_id}/commits","GET /gists/{gist_id}/forks","GET /installation/repositories","GET /issues","GET /licenses","GET /marketplace_listing/plans","GET /marketplace_listing/plans/{plan_id}/accounts","GET /marketplace_listing/stubbed/plans","GET /marketplace_listing/stubbed/plans/{plan_id}/accounts","GET /networks/{owner}/{repo}/events","GET /notifications","GET /organizations","GET /organizations/{org}/dependabot/repository-access","GET /orgs/{org}/actions/cache/usage-by-repository","GET /orgs/{org}/actions/hosted-runners","GET /orgs/{org}/actions/permissions/repositories","GET /orgs/{org}/actions/permissions/self-hosted-runners/repositories","GET /orgs/{org}/actions/runner-groups","GET /orgs/{org}/actions/runner-groups/{runner_group_id}/hosted-runners","GET /orgs/{org}/actions/runner-groups/{runner_group_id}/repositories","GET /orgs/{org}/actions/runner-groups/{runner_group_id}/runners","GET /orgs/{org}/actions/runners","GET /orgs/{org}/actions/secrets","GET /orgs/{org}/actions/secrets/{secret_name}/repositories","GET /orgs/{org}/actions/variables","GET /orgs/{org}/actions/variables/{name}/repositories","GET /orgs/{org}/attestations/repositories","GET /orgs/{org}/attestations/{subject_digest}","GET /orgs/{org}/blocks","GET /orgs/{org}/campaigns","GET /orgs/{org}/code-scanning/alerts","GET /orgs/{org}/code-security/configurations","GET /orgs/{org}/code-security/configurations/{configuration_id}/repositories","GET /orgs/{org}/codespaces","GET /orgs/{org}/codespaces/secrets","GET /orgs/{org}/codespaces/secrets/{secret_name}/repositories","GET /orgs/{org}/copilot/billing/seats","GET /orgs/{org}/copilot/metrics","GET /orgs/{org}/dependabot/alerts","GET /orgs/{org}/dependabot/secrets","GET /orgs/{org}/dependabot/secrets/{secret_name}/repositories","GET /orgs/{org}/events","GET /orgs/{org}/failed_invitations","GET /orgs/{org}/hooks","GET /orgs/{org}/hooks/{hook_id}/deliveries","GET /orgs/{org}/insights/api/route-stats/{actor_type}/{actor_id}","GET /orgs/{org}/insights/api/subject-stats","GET /orgs/{org}/insights/api/user-stats/{user_id}","GET /orgs/{org}/installations","GET /orgs/{org}/invitations","GET /orgs/{org}/invitations/{invitation_id}/teams","GET /orgs/{org}/issues","GET /orgs/{org}/members","GET /orgs/{org}/members/{username}/codespaces","GET /orgs/{org}/migrations","GET /orgs/{org}/migrations/{migration_id}/repositories","GET /orgs/{org}/organization-roles/{role_id}/teams","GET /orgs/{org}/organization-roles/{role_id}/users","GET /orgs/{org}/outside_collaborators","GET /orgs/{org}/packages","GET /orgs/{org}/packages/{package_type}/{package_name}/versions","GET /orgs/{org}/personal-access-token-requests","GET /orgs/{org}/personal-access-token-requests/{pat_request_id}/repositories","GET /orgs/{org}/personal-access-tokens","GET /orgs/{org}/personal-access-tokens/{pat_id}/repositories","GET /orgs/{org}/private-registries","GET /orgs/{org}/projects","GET /orgs/{org}/projectsV2","GET /orgs/{org}/projectsV2/{project_number}/fields","GET /orgs/{org}/projectsV2/{project_number}/items","GET /orgs/{org}/properties/values","GET /orgs/{org}/public_members","GET /orgs/{org}/repos","GET /orgs/{org}/rulesets","GET /orgs/{org}/rulesets/rule-suites","GET /orgs/{org}/rulesets/{ruleset_id}/history","GET /orgs/{org}/secret-scanning/alerts","GET /orgs/{org}/security-advisories","GET /orgs/{org}/settings/immutable-releases/repositories","GET /orgs/{org}/settings/network-configurations","GET /orgs/{org}/team/{team_slug}/copilot/metrics","GET /orgs/{org}/teams","GET /orgs/{org}/teams/{team_slug}/discussions","GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments","GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments/{comment_number}/reactions","GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/reactions","GET /orgs/{org}/teams/{team_slug}/invitations","GET /orgs/{org}/teams/{team_slug}/members","GET /orgs/{org}/teams/{team_slug}/projects","GET /orgs/{org}/teams/{team_slug}/repos","GET /orgs/{org}/teams/{team_slug}/teams","GET /projects/{project_id}/collaborators","GET /repos/{owner}/{repo}/actions/artifacts","GET /repos/{owner}/{repo}/actions/caches","GET /repos/{owner}/{repo}/actions/organization-secrets","GET /repos/{owner}/{repo}/actions/organization-variables","GET /repos/{owner}/{repo}/actions/runners","GET /repos/{owner}/{repo}/actions/runs","GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts","GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs","GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs","GET /repos/{owner}/{repo}/actions/secrets","GET /repos/{owner}/{repo}/actions/variables","GET /repos/{owner}/{repo}/actions/workflows","GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs","GET /repos/{owner}/{repo}/activity","GET /repos/{owner}/{repo}/assignees","GET /repos/{owner}/{repo}/attestations/{subject_digest}","GET /repos/{owner}/{repo}/branches","GET /repos/{owner}/{repo}/check-runs/{check_run_id}/annotations","GET /repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs","GET /repos/{owner}/{repo}/code-scanning/alerts","GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}/instances","GET /repos/{owner}/{repo}/code-scanning/analyses","GET /repos/{owner}/{repo}/codespaces","GET /repos/{owner}/{repo}/codespaces/devcontainers","GET /repos/{owner}/{repo}/codespaces/secrets","GET /repos/{owner}/{repo}/collaborators","GET /repos/{owner}/{repo}/comments","GET /repos/{owner}/{repo}/comments/{comment_id}/reactions","GET /repos/{owner}/{repo}/commits","GET /repos/{owner}/{repo}/commits/{commit_sha}/comments","GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls","GET /repos/{owner}/{repo}/commits/{ref}/check-runs","GET /repos/{owner}/{repo}/commits/{ref}/check-suites","GET /repos/{owner}/{repo}/commits/{ref}/status","GET /repos/{owner}/{repo}/commits/{ref}/statuses","GET /repos/{owner}/{repo}/compare/{basehead}","GET /repos/{owner}/{repo}/compare/{base}...{head}","GET /repos/{owner}/{repo}/contributors","GET /repos/{owner}/{repo}/dependabot/alerts","GET /repos/{owner}/{repo}/dependabot/secrets","GET /repos/{owner}/{repo}/deployments","GET /repos/{owner}/{repo}/deployments/{deployment_id}/statuses","GET /repos/{owner}/{repo}/environments","GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies","GET /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules/apps","GET /repos/{owner}/{repo}/environments/{environment_name}/secrets","GET /repos/{owner}/{repo}/environments/{environment_name}/variables","GET /repos/{owner}/{repo}/events","GET /repos/{owner}/{repo}/forks","GET /repos/{owner}/{repo}/hooks","GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries","GET /repos/{owner}/{repo}/invitations","GET /repos/{owner}/{repo}/issues","GET /repos/{owner}/{repo}/issues/comments","GET /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions","GET /repos/{owner}/{repo}/issues/events","GET /repos/{owner}/{repo}/issues/{issue_number}/comments","GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by","GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocking","GET /repos/{owner}/{repo}/issues/{issue_number}/events","GET /repos/{owner}/{repo}/issues/{issue_number}/labels","GET /repos/{owner}/{repo}/issues/{issue_number}/reactions","GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues","GET /repos/{owner}/{repo}/issues/{issue_number}/timeline","GET /repos/{owner}/{repo}/keys","GET /repos/{owner}/{repo}/labels","GET /repos/{owner}/{repo}/milestones","GET /repos/{owner}/{repo}/milestones/{milestone_number}/labels","GET /repos/{owner}/{repo}/notifications","GET /repos/{owner}/{repo}/pages/builds","GET /repos/{owner}/{repo}/projects","GET /repos/{owner}/{repo}/pulls","GET /repos/{owner}/{repo}/pulls/comments","GET /repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions","GET /repos/{owner}/{repo}/pulls/{pull_number}/comments","GET /repos/{owner}/{repo}/pulls/{pull_number}/commits","GET /repos/{owner}/{repo}/pulls/{pull_number}/files","GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews","GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments","GET /repos/{owner}/{repo}/releases","GET /repos/{owner}/{repo}/releases/{release_id}/assets","GET /repos/{owner}/{repo}/releases/{release_id}/reactions","GET /repos/{owner}/{repo}/rules/branches/{branch}","GET /repos/{owner}/{repo}/rulesets","GET /repos/{owner}/{repo}/rulesets/rule-suites","GET /repos/{owner}/{repo}/rulesets/{ruleset_id}/history","GET /repos/{owner}/{repo}/secret-scanning/alerts","GET /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number}/locations","GET /repos/{owner}/{repo}/security-advisories","GET /repos/{owner}/{repo}/stargazers","GET /repos/{owner}/{repo}/subscribers","GET /repos/{owner}/{repo}/tags","GET /repos/{owner}/{repo}/teams","GET /repos/{owner}/{repo}/topics","GET /repositories","GET /search/code","GET /search/commits","GET /search/issues","GET /search/labels","GET /search/repositories","GET /search/topics","GET /search/users","GET /teams/{team_id}/discussions","GET /teams/{team_id}/discussions/{discussion_number}/comments","GET /teams/{team_id}/discussions/{discussion_number}/comments/{comment_number}/reactions","GET /teams/{team_id}/discussions/{discussion_number}/reactions","GET /teams/{team_id}/invitations","GET /teams/{team_id}/members","GET /teams/{team_id}/projects","GET /teams/{team_id}/repos","GET /teams/{team_id}/teams","GET /user/blocks","GET /user/codespaces","GET /user/codespaces/secrets","GET /user/emails","GET /user/followers","GET /user/following","GET /user/gpg_keys","GET /user/installations","GET /user/installations/{installation_id}/repositories","GET /user/issues","GET /user/keys","GET /user/marketplace_purchases","GET /user/marketplace_purchases/stubbed","GET /user/memberships/orgs","GET /user/migrations","GET /user/migrations/{migration_id}/repositories","GET /user/orgs","GET /user/packages","GET /user/packages/{package_type}/{package_name}/versions","GET /user/public_emails","GET /user/repos","GET /user/repository_invitations","GET /user/social_accounts","GET /user/ssh_signing_keys","GET /user/starred","GET /user/subscriptions","GET /user/teams","GET /users","GET /users/{username}/attestations/{subject_digest}","GET /users/{username}/events","GET /users/{username}/events/orgs/{org}","GET /users/{username}/events/public","GET /users/{username}/followers","GET /users/{username}/following","GET /users/{username}/gists","GET /users/{username}/gpg_keys","GET /users/{username}/keys","GET /users/{username}/orgs","GET /users/{username}/packages","GET /users/{username}/projects","GET /users/{username}/projectsV2","GET /users/{username}/projectsV2/{project_number}/fields","GET /users/{username}/projectsV2/{project_number}/items","GET /users/{username}/received_events","GET /users/{username}/received_events/public","GET /users/{username}/repos","GET /users/{username}/social_accounts","GET /users/{username}/ssh_signing_keys","GET /users/{username}/starred","GET /users/{username}/subscriptions"];function isPaginatingEndpoint(e){if(typeof e==="string"){return fa.includes(e)}else{return false}}function paginateRest(e){return{paginate:Object.assign(paginate.bind(null,e),{iterator:iterator.bind(null,e)})}}paginateRest.VERSION=pa;const Ca=new Context;const Qa=getApiBaseUrl();const Ia={baseUrl:Qa,request:{agent:getProxyAgent(Qa),fetch:getProxyFetch(Qa)}};const Ba=Octokit.plugin(restEndpointMethods,paginateRest).defaults(Ia);function getOctokitOptions(e,t){const r=Object.assign({},t||{});const s=getAuthString(e,r);if(s){r.auth=s}const n=getUserAgentWithOrchestrationId(r.userAgent);if(n){r.userAgent=n}return r}const ma=new Context;function getOctokit(e,t,...r){const s=Ba.plugin(...r);return new s(getOctokitOptions(e,t))}function getOctokitClient(){const e=io_getInput("github_token");if(!e){throw new Error("github_token is required")}return getOctokit(e)}async function getUserInfo(e){if(!e)return undefined;const t=getOctokitClient();const r=await t.rest.users.getByUsername({username:e});core_debug(`Fetched github actor from the API: ${JSON.stringify(r?.data,null,2)}`);return{name:r?.data?.name,email:r?.data?.email}}const wa=/[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu;function neutralizeLogString(e){return e.replace(wa,e=>{const t=e.codePointAt(0).toString(16).padStart(4,"0");return`\\u${t}`})}const ya="[Circular]";function neutralizeForLog(e,t=new WeakSet){if(typeof e==="string")return neutralizeLogString(e);if(typeof e==="number"||typeof e==="boolean"||e===null||e===undefined){return e}if(e instanceof Error){const t=new Error(neutralizeLogString(e.message));t.name=neutralizeLogString(e.name);if(e.stack){t.stack=neutralizeLogString(e.stack)}return t}if(Array.isArray(e)){if(t.has(e))return ya;t.add(e);return e.map(e=>neutralizeForLog(e,t))}if(typeof e==="object"){if(t.has(e))return ya;t.add(e);const r={};for(const[s,n]of Object.entries(e)){r[neutralizeLogString(s)]=neutralizeForLog(n,t)}return r}return e}function log(e,t){if(t)console.log(neutralizeForLog(t));if(e){const t=neutralizeForLog(e);if(typeof t==="string"||t instanceof Error){error(t)}else{error(String(t))}}}const ba=["user.name","user.email","author.name","author.email","committer.name","committer.email"];function pickGitIdentityConfig(e){const t={};for(const r of ba){if(Object.prototype.hasOwnProperty.call(e,r)){t[r]=e[r]}}return t}function assertValidBranchName(e){if(!e||!e.trim()){throw new Error("The new_branch value is empty.")}if(e.startsWith("-")){throw new Error(`The new_branch value '${e}' cannot start with '-' (it would be interpreted as a git option).`)}for(const t of e){const r=t.codePointAt(0);if(r<=31||r===127||r>=128&&r<=159||/\s/u.test(t)){throw new Error(`The new_branch value '${e}' contains whitespace or control characters.`)}}try{(0,ue.execFileSync)("git",["check-ref-format","--branch",e],{stdio:"ignore"})}catch{throw new Error(`The new_branch value '${e}' is not a valid git branch name.`)}}const ka=[{canonical:"upload-pack",minPrefix:"upl"},{canonical:"receive-pack",minPrefix:"rece"},{canonical:"exec",minPrefix:"e"}];const Ta=[{canonical:"file",minPrefix:"fi"}];const Ra=[{canonical:"message",minPrefix:"mes"},{canonical:"local-user",minPrefix:"local-"},{canonical:"cleanup",minPrefix:"cleanup"},{canonical:"file",minPrefix:"fi"},{canonical:"upload-pack",minPrefix:"upl"},{canonical:"receive-pack",minPrefix:"rece"},{canonical:"exec",minPrefix:"e"}];const Da=new Set(["m","u","F"]);function getLongOptionName(e){if(!e.startsWith("--")||e==="--")return undefined;const t=e.slice(2);const r=t.indexOf("=");return(r===-1?t:t.slice(0,r)).toLowerCase()}function longOptionHasInlineValue(e){if(!e.startsWith("--")||e==="--")return false;return e.slice(2).includes("=")}function matchesLongOptionPrefix(e,t){const r=getLongOptionName(e);if(!r)return false;return t.some(({canonical:e,minPrefix:t})=>r.length>=t.length&&e.startsWith(r))}function isDangerousRemoteHelperOption(e){return matchesLongOptionPrefix(e,ka)}function isDangerousMessageFileShortOption(e){if(!e.startsWith("-")||e.startsWith("--"))return false;const t=e.slice(1);for(let e=0;e` - ${e}`).join("\n");const r=e.map(e=>` git rm --cached -- ${e}`).join("\n");throw new Error(`Refusing to commit unexpected gitlink(s) (embedded git repository staged as mode 160000):\n${t}\n`+"Git records a nested .git directory as a gitlink, not as its files. "+`Remove the nested .git directory, or unstage the path(s) with:\n${r}`)}function parseInputArray(e){core_debug(`Parsing input array: ${e}`);try{const t=load(e);if(t&&Array.isArray(t)&&t.every(e=>typeof e==="string")){core_debug(`Input parsed as YAML array of length ${t.length}`);return t}}catch{}core_debug("Input parsed as single string");return[e]}function readJSON(e){let t;try{t=fs.readFileSync(e,{encoding:"utf8"})}catch{throw`Couldn't read file. File path: ${e}`}try{return JSON.parse(t)}catch{throw`Couldn't parse file to JSON. File path: ${e}`}}const Sa={committed:"false",commit_long_sha:undefined,commit_sha:undefined,pushed:"false",tagged:"false",tag_pushed:"false"};Object.entries(Sa).forEach(([e,t])=>setOutput(e,t));function io_getInput(e,t=false){if(t)return getBooleanInput(e);return getInput(e)}function io_setOutput(e,t){core_debug(`Setting output: ${e}=${t}`);Sa[e]=t;setOutput(e,t)}function parsePushAttempts(e){const t=e.trim();if(!/^\+?\d+$/.test(t)){throw new Error(`'${e}' is not a valid value for push_attempts. It must be a positive integer (≥ 1).`)}const r=Number.parseInt(t,10);if(!Number.isSafeInteger(r)||r<1){throw new Error(`'${e}' is not a valid value for push_attempts. It must be a positive integer (≥ 1).`)}return r}function logOutputs(){startGroup("Outputs");for(const e in Sa){info(`${e}: ${Sa[e]}`)}endGroup()}async function checkInputs(){function setInput(e,t){if(t)return process.env[`INPUT_${e.toUpperCase()}`]=t;else return delete process.env[`INPUT_${e.toUpperCase()}`]}function setDefault(e,t){if(!io_getInput(e))setInput(e,t);return io_getInput(e)}if(!io_getInput("add")&&!io_getInput("remove"))throw new Error("Both 'add' and 'remove' are empty, the action has nothing to do.");if(io_getInput("add")){const e=parseInputArray(io_getInput("add"));if(e.length===1)info("Add input parsed as single string, running 1 git add command.");else if(e.length>1)info(`Add input parsed as string array, running ${e.length} git add commands.`);else setFailed("Add input: array length < 1")}if(io_getInput("remove")){const e=parseInputArray(io_getInput("remove")||"");if(e.length===1)info("Remove input parsed as single string, running 1 git rm command.");else if(e.length>1)info(`Remove input parsed as string array, running ${e.length} git rm commands.`);else setFailed("Remove input: array length < 1")}const e=["github_actor","user_info","github_actions"];if(!e.includes(io_getInput("default_author")))throw new Error(`'${io_getInput("default_author")}' is not a valid value for default_author. Valid values: ${e.join(", ")}`);if(io_getInput("dry_run",true))info("> Dry run enabled: no mutating git operations will be performed.");if(io_getInput("fetch")){let e;try{e=io_getInput("fetch",true)}catch{e=io_getInput("fetch")}core_debug(`Current fetch option: '${e}' (parsed as ${typeof e})`)}let t,r;switch(io_getInput("default_author")){case"github_actor":{t=process.env.GITHUB_ACTOR??"";r=`${process.env.GITHUB_ACTOR}@users.noreply.github.com`;break}case"user_info":{if(!io_getInput("author_name")||!io_getInput("author_email")){const e=await getUserInfo(process.env.GITHUB_ACTOR);if(!e?.name)warning("Couldn't fetch author name, filling with github_actor.");if(!e?.email)warning("Couldn't fetch author email, filling with github_actor.");e?.name&&(t=e?.name);e?.email&&(r=e.email);if(t&&r)break}!t&&(t=process.env.GITHUB_ACTOR??"");!r&&(r=`${process.env.GITHUB_ACTOR}@users.noreply.github.com`);break}case"github_actions":{t="github-actions";r="41898282+github-actions[bot]@users.noreply.github.com";break}default:throw new Error("This should not happen, please contact the author of this action. (checkInputs.author)")}setDefault("author_name",t);setDefault("author_email",r);info(`> Using '${io_getInput("author_name")} <${io_getInput("author_email")}>' as author.`);if(io_getInput("committer_name")||io_getInput("committer_email"))info(`> Using custom committer info: ${io_getInput("committer_name")||io_getInput("author_name")+" [from author info]"} <${io_getInput("committer_email")||io_getInput("author_email")+" [from author info]"}>`);setDefault("committer_name",io_getInput("author_name"));setDefault("committer_email",io_getInput("author_email"));core_debug(`Committer: ${io_getInput("committer_name")} <${io_getInput("committer_email")}>`);setDefault("message",`Commit from GitHub Actions (${process.env.GITHUB_WORKFLOW})`);info(`> Using "${io_getInput("message")}" as commit message.`);const s=io_getInput("new_branch");if(s)assertValidBranchName(s);const n=["ignore","exitImmediately","exitAtEnd"];if(!n.includes(io_getInput("pathspec_error_handling")))throw new Error(`"${io_getInput("pathspec_error_handling")}" is not a valid value for the 'pathspec_error_handling' input. Valid values are: ${n.join(", ")}`);if(io_getInput("pull")==="NO-PULL")warning("`NO-PULL` is a legacy option for the `pull` input. If you don't want the action to pull the repo, simply remove this input.");if(io_getInput("push")){let e;try{e=io_getInput("push",true)}catch{e=io_getInput("push")}core_debug(`Current push option: '${e}' (parsed as ${typeof e})`)}const o=parsePushAttempts(io_getInput("push_attempts")||"1");core_debug(`Current push_attempts option: ${o}`);if(o>1&&!io_getInput("pull")){warning("push_attempts is greater than 1 but pull is not set. Retries will re-run push only; without pull (e.g. --rebase), concurrent remote updates are unlikely to recover.")}if(!io_getInput("github_token"))warning("No github_token has been detected, the action may fail if it needs to use the API")}const Fa=u.join(process.cwd(),io_getInput("cwd")||"");const va=lo({baseDir:Fa});const Ua=[];info(`Running in ${Fa}`);(async()=>{await checkInputs();const e=io_getInput("dry_run",true);startGroup("Internal logs");info(e?"> Staging files (dry run)...":"> Staging files...");const t=io_getInput("pathspec_error_handling")==="ignore"?"pathspec":"none";let r=false;if(io_getInput("add")){info(e?"> Adding files (dry run)...":"> Adding files...");const s=await add(t,e);if(e)r=r||s.some(e=>typeof e==="string"&&e.trim().length>0)}else info("> No files to add.");if(io_getInput("remove")){info(e?"> Removing files (dry run)...":"> Removing files...");const s=await main_remove(t,e);if(e)r=r||s.some(e=>{if(e===null||e===undefined)return false;const t=typeof e==="string"?e:String(e);return t.trim().length>0})}else info("> No files to remove.");info("> Checking for uncommitted changes in the git working tree...");const s=(await va.diffSummary(["--cached"])).files.length;const n=matchGitArgs(io_getInput("commit")||"").includes("--allow-empty");if(s>0||r||n){info(e?`> Dry run: would proceed (${s} already staged`+`${r?", staging probes reported changes":""}`+`${n?", --allow-empty":""}).`:`> Found ${s} changed files.`);core_debug(`--allow-empty argument detected: ${n}`);if(e){await logDryRunRemainingSteps();endGroup();info("> Dry run completed. No changes were made.");return}await va.addConfig("user.email",io_getInput("author_email"),undefined,log).addConfig("user.name",io_getInput("author_name"),undefined,log).addConfig("author.email",io_getInput("author_email"),undefined,log).addConfig("author.name",io_getInput("author_name"),undefined,log).addConfig("committer.email",io_getInput("committer_email"),undefined,log).addConfig("committer.name",io_getInput("committer_name"),undefined,log);if(isDebug()){const e=pickGitIdentityConfig((await va.listConfig()).all);core_debug(Object.keys(e).length?"> Current git identity config\n"+JSON.stringify(e,null,2):"> Git identity config set (no identity keys present in listConfig)")}let o;try{o=io_getInput("fetch",true)}catch{o=io_getInput("fetch")}if(o){info("> Fetching repo...");await va.fetch(matchGitArgs(o===true?"":o),log)}else info("> Not fetching repo.");const a=io_getInput("new_branch");if(a){info("> Checking-out branch...");if(!o)warning("Creating a new branch without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.");await va.checkout([a]).then(()=>{log(undefined,`'${a}' branch already existed.`)}).catch(()=>{log(undefined,`Creating '${a}' branch.`);return va.checkout(["-b",a],log)})}const l=io_getInput("pull");if(l){await pullFromRemote(l,{restage:true,ignoreErrors:t})}else info("> Not pulling from repo.");info("> Creating commit...");const u=await va.commit(io_getInput("message"),matchGitArgs(io_getInput("commit")||""));log(undefined,u);if(!u.commit){throw new Error("Commit did not produce a SHA; refusing to report committed=true.")}io_setOutput("committed","true");io_setOutput("commit_long_sha",u.commit);io_setOutput("commit_sha",u.commit.substring(0,7));if(io_getInput("tag")){info("> Tagging commit...");if(!o)warning("Creating a tag without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.");await va.tag(matchGitArgs(io_getInput("tag")||""),(e,t)=>{if(t)io_setOutput("tagged","true");return log(e,t)}).then(e=>{io_setOutput("tagged","true");return log(null,e)}).catch(e=>setFailed(e))}else info("> No tag info provided.");let g;try{g=io_getInput("push",true)}catch{g=io_getInput("push")}if(g){const e=parsePushAttempts(io_getInput("push_attempts")||"1");for(let r=1;r<=e;r++){try{info(e>1?`> Pushing commit to repo (attempt ${r}/${e})...`:"> Pushing commit to repo...");await pushCommit(g);break}catch(s){if(r===e)throw s;const n=s instanceof Error?s.message:String(s);warning(`Push failed (attempt ${r}/${e}): ${neutralizeLogString(n)}`);if(l){await pullFromRemote(l,{restage:false,ignoreErrors:t});const e=(await va.revparse(["HEAD"])).trim();io_setOutput("commit_long_sha",e);io_setOutput("commit_sha",e.substring(0,7))}}}if(io_getInput("tag")){info("> Pushing tags to repo...");await va.pushTags("origin",matchGitArgs(io_getInput("tag_push")||"")).then(e=>{io_setOutput("tag_pushed","true");return log(null,e)}).catch(e=>setFailed(e))}else info("> No tags to push.")}else info("> Not pushing anything.");endGroup();info("> Task completed.")}else{endGroup();info(e?"> Dry run: working tree clean. Nothing would be committed.":"> Working tree clean. Nothing to commit.")}})().then(()=>{if(Ua.length===1)throw Ua[0];else if(Ua.length>1){Ua.forEach(e=>error(e));throw"There have been multiple runtime errors."}}).then(logOutputs).catch(e=>{endGroup();logOutputs();setFailed(e)});async function logDryRunRemainingSteps(){info(`> Would set git identity: ${io_getInput("author_name")} <${io_getInput("author_email")}> (committer: ${io_getInput("committer_name")} <${io_getInput("committer_email")}>)`);let e;try{e=io_getInput("fetch",true)}catch{e=io_getInput("fetch")}if(e){info(`> Would fetch repo${e===true?"":` with: ${e}`}.`)}else info("> Would not fetch repo.");const t=io_getInput("new_branch");if(t){info(`> Would check out branch '${t}'.`);if(!e)warning("Creating a new branch without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.")}const r=io_getInput("pull");if(r)info(`> Would pull from remote with: ${r}.`);else info("> Would not pull from repo.");info(`> Would create commit with message: "${io_getInput("message")}"${io_getInput("commit")?` (extra args: ${io_getInput("commit")})`:""}.`);if(io_getInput("tag")){info(`> Would tag commit with: ${io_getInput("tag")}.`);if(!e)warning("Creating a tag without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.")}else info("> No tag info provided.");let s;try{s=io_getInput("push",true)}catch{s=io_getInput("push")}if(s){const e=parsePushAttempts(io_getInput("push_attempts")||"1");if(s===true){const t=io_getInput("new_branch");info(t?`> Would push commit to repo (set upstream for '${t}')${e>1?` with up to ${e} attempts`:""}.`:`> Would push commit to repo${e>1?` with up to ${e} attempts`:""}.`)}else info(`> Would push commit to repo with: ${s}${e>1?` (up to ${e} attempts)`:""}.`);if(io_getInput("tag")){info(`> Would push tags to repo${io_getInput("tag_push")?` with: ${io_getInput("tag_push")}`:""}.`)}else info("> No tags to push.")}else info("> Would not push anything.")}async function pullFromRemote(e,t){info("> Pulling from remote...");core_debug(`Current git pull arguments: ${e}`);await va.fetch(undefined,log).pull(undefined,undefined,matchGitArgs(e),log);info("> Checking for conflicts...");const r=await va.status(undefined,log);if(r.conflicted.length){throw new Error(`There are ${r.conflicted.length} conflicting files: ${r.conflicted.map(neutralizeLogString).join(", ")}`)}info("> No conflicts found.");if(t.restage){info("> Re-staging files...");if(io_getInput("add"))await add(t.ignoreErrors);if(io_getInput("remove"))await main_remove(t.ignoreErrors)}}async function pushCommit(e){if(e===true){const e=io_getInput("new_branch");if(e){core_debug(`Running: git push --set-upstream origin -- ${e}`);await va.raw(["push","--set-upstream","origin","--",e],(e,t)=>{if(t)io_setOutput("pushed","true");return log(e,t)})}else{core_debug("Running: git push origin --set-upstream");await va.push("origin",undefined,{"--set-upstream":null},(e,t)=>{if(t)io_setOutput("pushed","true");return log(e,t)})}}else{core_debug(`Running: git push ${e}`);await va.push(undefined,undefined,matchGitArgs(e),(e,t)=>{if(t)io_setOutput("pushed","true");return log(e,t)})}}async function add(e="none",t=false){const r=io_getInput("add");if(!r)return[];const s=parseInputArray(r);const n=[];for(const r of s){const s=t?["--dry-run",...matchGitArgs(r)]:matchGitArgs(r);n.push(await va.add(s,(t,r)=>log(e==="all"?null:t,r)).catch(t=>{if(e==="all")return;if(t.message.includes("fatal: pathspec")&&t.message.includes("did not match any files")){if(e==="pathspec")return;const t=io_getInput("pathspec_error_handling"),s=new Error(`Add command did not match any file: git add ${r}`);if(t==="exitImmediately")throw s;if(t==="exitAtEnd")Ua.push(s)}else throw t}))}if(t){await assertGitlinksWithTempIndex(s,e)}else{const e=await va.raw(["diff","--cached","--raw"]);assertNoUnexpectedGitlinks(findUnexpectedGitlinks(e))}return n}async function assertGitlinksWithTempIndex(e,t){const r=u.join(n.tmpdir(),`add-and-commit-${process.pid}-${Date.now()}.index`);try{const s=(await va.raw(["rev-parse","--git-path","index"])).trim();const n=u.isAbsolute(s)?s:u.join(Fa,s);if(l.existsSync(n)){l.copyFileSync(n,r)}else{const e=lo({baseDir:Fa}).env({...process.env,GIT_INDEX_FILE:r});const t=await va.raw(["rev-parse","--verify","HEAD"]).then(()=>true).catch(()=>false);if(t){await e.raw(["read-tree","HEAD"])}else{await e.raw(["read-tree","--empty"])}}const o=lo({baseDir:Fa}).env({...process.env,GIT_INDEX_FILE:r});for(const r of e){await o.add(matchGitArgs(r),(e,r)=>log(t==="all"?null:e,r)).catch(e=>{if(t==="all")return;if(e.message.includes("fatal: pathspec")&&e.message.includes("did not match any files")){if(t==="pathspec")return;const e=io_getInput("pathspec_error_handling");if(e==="exitImmediately"){throw new Error(`Add command did not match any file: git add ${r}`)}return}throw e})}const a=await o.raw(["diff","--cached","--raw"]);assertNoUnexpectedGitlinks(findUnexpectedGitlinks(a))}finally{l.rmSync(r,{force:true});l.rmSync(`${r}.lock`,{force:true})}}async function main_remove(e="none",t=false){const r=io_getInput("remove");if(!r)return[];const s=parseInputArray(r);const n=[];for(const r of s){const s=t?["--dry-run",...matchGitArgs(r)]:matchGitArgs(r);n.push(await va.rm(s,(t,r)=>log(e==="all"?null:t,r)).catch(t=>{if(e==="all")return;if(t.message.includes("fatal: pathspec")&&t.message.includes("did not match any files")){if(e==="pathspec")return;const t=io_getInput("pathspec_error_handling"),s=new Error(`Remove command did not match any file:\n git rm ${r}`);if(t==="exitImmediately")throw s;if(t==="exitAtEnd")Ua.push(s)}else throw t}))}return n} \ No newline at end of file +/* v8 ignore else -- @preserve */var zA="0.0.0-development";function _buildMessageForResponseErrors(e){return`Request failed due to following response errors:\n`+e.errors.map(e=>` - ${e.message}`).join("\n")}var KA=class extends Error{constructor(e,t,r){super(_buildMessageForResponseErrors(r));this.request=e;this.headers=t;this.response=r;this.errors=r.errors;this.data=r.data;if(Error.captureStackTrace){Error.captureStackTrace(this,this.constructor)}}name="GraphqlResponseError";errors;data};var ZA=["method","baseUrl","url","headers","request","query","mediaType","operationName"];var $A=["query","method","url"];var XA=/\/api\/v3\/?$/;function graphql(e,t,r){if(r){if(typeof t==="string"&&"query"in r){return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`))}for(const e in r){if(!$A.includes(e))continue;return Promise.reject(new Error(`[@octokit/graphql] "${e}" cannot be used as variable name`))}}const s=typeof t==="string"?Object.assign({query:t},r):t;const n=Object.keys(s).reduce((e,t)=>{if(ZA.includes(t)){e[t]=s[t];return e}if(!e.variables){e.variables={}}e.variables[t]=s[t];return e},{});const o=s.baseUrl||e.endpoint.DEFAULTS.baseUrl;if(XA.test(o)){n.url=o.replace(XA,"/api/graphql")}return e(n).then(e=>{if(e.data.errors){const t={};for(const r of Object.keys(e.headers)){t[r]=e.headers[r]}throw new KA(n,t,e.data)}return e.data.data})}function graphql_dist_bundle_withDefaults(e,t){const r=e.defaults(t);const newApi=(e,t)=>graphql(r,e,t);return Object.assign(newApi,{defaults:graphql_dist_bundle_withDefaults.bind(null,r),endpoint:r.endpoint})}var ea=graphql_dist_bundle_withDefaults(jA,{headers:{"user-agent":`octokit-graphql.js/${zA} ${getUserAgent()}`},method:"POST",url:"/graphql"});function withCustomRequest(e){return graphql_dist_bundle_withDefaults(e,{method:"POST",url:"/graphql"})}var ta="(?:[a-zA-Z0-9_-]+)";var ra="\\.";var sa=new RegExp(`^${ta}${ra}${ta}${ra}${ta}$`);var na=sa.test.bind(sa);async function auth(e){const t=na(e);const r=e.startsWith("v1.")||e.startsWith("ghs_");const s=e.startsWith("ghu_");const n=t?"app":r?"installation":s?"user-to-server":"oauth";return{type:"token",token:e,tokenType:n}}function withAuthorizationPrefix(e){if(e.split(/\./).length===3){return`bearer ${e}`}return`token ${e}`}async function hook(e,t,r,s){const n=t.endpoint.merge(r,s);n.headers.authorization=withAuthorizationPrefix(e);return t(n)}var oa=function createTokenAuth2(e){if(!e){throw new Error("[@octokit/auth-token] No token passed to createTokenAuth")}if(typeof e!=="string"){throw new Error("[@octokit/auth-token] Token passed to createTokenAuth is not a string")}e=e.replace(/^(token|bearer) +/i,"");return Object.assign(auth.bind(null,e),{hook:hook.bind(null,e)})};const ia="7.0.6";const dist_src_noop=()=>{};const Aa=console.warn.bind(console);const aa=console.error.bind(console);function dist_src_createLogger(e={}){if(typeof e.debug!=="function"){e.debug=dist_src_noop}if(typeof e.info!=="function"){e.info=dist_src_noop}if(typeof e.warn!=="function"){e.warn=Aa}if(typeof e.error!=="function"){e.error=aa}return e}const ca=`octokit-core.js/${ia} ${getUserAgent()}`;class Octokit{static VERSION=ia;static defaults(e){const t=class extends(this){constructor(...t){const r=t[0]||{};if(typeof e==="function"){super(e(r));return}super(Object.assign({},e,r,r.userAgent&&e.userAgent?{userAgent:`${r.userAgent} ${e.userAgent}`}:null))}};return t}static plugins=[];static plugin(...e){const t=this.plugins;const r=class extends(this){static plugins=t.concat(e.filter(e=>!t.includes(e)))};return r}constructor(e={}){const t=new OA.Collection;const r={baseUrl:jA.endpoint.DEFAULTS.baseUrl,headers:{},request:Object.assign({},e.request,{hook:t.bind(null,"request")}),mediaType:{previews:[],format:""}};r.headers["user-agent"]=e.userAgent?`${e.userAgent} ${ca}`:ca;if(e.baseUrl){r.baseUrl=e.baseUrl}if(e.previews){r.mediaType.previews=e.previews}if(e.timeZone){r.headers["time-zone"]=e.timeZone}this.request=jA.defaults(r);this.graphql=withCustomRequest(this.request).defaults(r);this.log=dist_src_createLogger(e.log);this.hook=t;if(!e.authStrategy){if(!e.auth){this.auth=async()=>({type:"unauthenticated"})}else{const r=oa(e.auth);t.wrap("request",r.hook);this.auth=r}}else{const{authStrategy:r,...s}=e;const n=r(Object.assign({request:this.request,log:this.log,octokit:this,octokitOptions:s},e.auth));t.wrap("request",n.hook);this.auth=n}const s=this.constructor;for(let t=0;t({async next(){if(!l)return{done:true};try{const e=await n({method:o,url:l,headers:a});const t=normalizePaginatedListResponse(e);l=((t.headers.link||"").match(/<([^<>]+)>;\s*rel="next"/)||[])[1];if(!l&&"total_commits"in t.data){const e=new URL(t.url);const r=e.searchParams;const s=parseInt(r.get("page")||"1",10);const n=parseInt(r.get("per_page")||"250",10);if(s*n{if(n.done){return t}let o=false;function done(){o=true}t=t.concat(s?s(n.value,done):n.value.data);if(o){return t}return gather(e,t,r,s)})}var Ea=Object.assign(paginate,{iterator:iterator});var fa=null&&["GET /advisories","GET /app/hook/deliveries","GET /app/installation-requests","GET /app/installations","GET /assignments/{assignment_id}/accepted_assignments","GET /classrooms","GET /classrooms/{classroom_id}/assignments","GET /enterprises/{enterprise}/code-security/configurations","GET /enterprises/{enterprise}/code-security/configurations/{configuration_id}/repositories","GET /enterprises/{enterprise}/dependabot/alerts","GET /enterprises/{enterprise}/teams","GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships","GET /enterprises/{enterprise}/teams/{enterprise-team}/organizations","GET /events","GET /gists","GET /gists/public","GET /gists/starred","GET /gists/{gist_id}/comments","GET /gists/{gist_id}/commits","GET /gists/{gist_id}/forks","GET /installation/repositories","GET /issues","GET /licenses","GET /marketplace_listing/plans","GET /marketplace_listing/plans/{plan_id}/accounts","GET /marketplace_listing/stubbed/plans","GET /marketplace_listing/stubbed/plans/{plan_id}/accounts","GET /networks/{owner}/{repo}/events","GET /notifications","GET /organizations","GET /organizations/{org}/dependabot/repository-access","GET /orgs/{org}/actions/cache/usage-by-repository","GET /orgs/{org}/actions/hosted-runners","GET /orgs/{org}/actions/permissions/repositories","GET /orgs/{org}/actions/permissions/self-hosted-runners/repositories","GET /orgs/{org}/actions/runner-groups","GET /orgs/{org}/actions/runner-groups/{runner_group_id}/hosted-runners","GET /orgs/{org}/actions/runner-groups/{runner_group_id}/repositories","GET /orgs/{org}/actions/runner-groups/{runner_group_id}/runners","GET /orgs/{org}/actions/runners","GET /orgs/{org}/actions/secrets","GET /orgs/{org}/actions/secrets/{secret_name}/repositories","GET /orgs/{org}/actions/variables","GET /orgs/{org}/actions/variables/{name}/repositories","GET /orgs/{org}/attestations/repositories","GET /orgs/{org}/attestations/{subject_digest}","GET /orgs/{org}/blocks","GET /orgs/{org}/campaigns","GET /orgs/{org}/code-scanning/alerts","GET /orgs/{org}/code-security/configurations","GET /orgs/{org}/code-security/configurations/{configuration_id}/repositories","GET /orgs/{org}/codespaces","GET /orgs/{org}/codespaces/secrets","GET /orgs/{org}/codespaces/secrets/{secret_name}/repositories","GET /orgs/{org}/copilot/billing/seats","GET /orgs/{org}/copilot/metrics","GET /orgs/{org}/dependabot/alerts","GET /orgs/{org}/dependabot/secrets","GET /orgs/{org}/dependabot/secrets/{secret_name}/repositories","GET /orgs/{org}/events","GET /orgs/{org}/failed_invitations","GET /orgs/{org}/hooks","GET /orgs/{org}/hooks/{hook_id}/deliveries","GET /orgs/{org}/insights/api/route-stats/{actor_type}/{actor_id}","GET /orgs/{org}/insights/api/subject-stats","GET /orgs/{org}/insights/api/user-stats/{user_id}","GET /orgs/{org}/installations","GET /orgs/{org}/invitations","GET /orgs/{org}/invitations/{invitation_id}/teams","GET /orgs/{org}/issues","GET /orgs/{org}/members","GET /orgs/{org}/members/{username}/codespaces","GET /orgs/{org}/migrations","GET /orgs/{org}/migrations/{migration_id}/repositories","GET /orgs/{org}/organization-roles/{role_id}/teams","GET /orgs/{org}/organization-roles/{role_id}/users","GET /orgs/{org}/outside_collaborators","GET /orgs/{org}/packages","GET /orgs/{org}/packages/{package_type}/{package_name}/versions","GET /orgs/{org}/personal-access-token-requests","GET /orgs/{org}/personal-access-token-requests/{pat_request_id}/repositories","GET /orgs/{org}/personal-access-tokens","GET /orgs/{org}/personal-access-tokens/{pat_id}/repositories","GET /orgs/{org}/private-registries","GET /orgs/{org}/projects","GET /orgs/{org}/projectsV2","GET /orgs/{org}/projectsV2/{project_number}/fields","GET /orgs/{org}/projectsV2/{project_number}/items","GET /orgs/{org}/properties/values","GET /orgs/{org}/public_members","GET /orgs/{org}/repos","GET /orgs/{org}/rulesets","GET /orgs/{org}/rulesets/rule-suites","GET /orgs/{org}/rulesets/{ruleset_id}/history","GET /orgs/{org}/secret-scanning/alerts","GET /orgs/{org}/security-advisories","GET /orgs/{org}/settings/immutable-releases/repositories","GET /orgs/{org}/settings/network-configurations","GET /orgs/{org}/team/{team_slug}/copilot/metrics","GET /orgs/{org}/teams","GET /orgs/{org}/teams/{team_slug}/discussions","GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments","GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/comments/{comment_number}/reactions","GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/reactions","GET /orgs/{org}/teams/{team_slug}/invitations","GET /orgs/{org}/teams/{team_slug}/members","GET /orgs/{org}/teams/{team_slug}/projects","GET /orgs/{org}/teams/{team_slug}/repos","GET /orgs/{org}/teams/{team_slug}/teams","GET /projects/{project_id}/collaborators","GET /repos/{owner}/{repo}/actions/artifacts","GET /repos/{owner}/{repo}/actions/caches","GET /repos/{owner}/{repo}/actions/organization-secrets","GET /repos/{owner}/{repo}/actions/organization-variables","GET /repos/{owner}/{repo}/actions/runners","GET /repos/{owner}/{repo}/actions/runs","GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts","GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs","GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs","GET /repos/{owner}/{repo}/actions/secrets","GET /repos/{owner}/{repo}/actions/variables","GET /repos/{owner}/{repo}/actions/workflows","GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs","GET /repos/{owner}/{repo}/activity","GET /repos/{owner}/{repo}/assignees","GET /repos/{owner}/{repo}/attestations/{subject_digest}","GET /repos/{owner}/{repo}/branches","GET /repos/{owner}/{repo}/check-runs/{check_run_id}/annotations","GET /repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs","GET /repos/{owner}/{repo}/code-scanning/alerts","GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}/instances","GET /repos/{owner}/{repo}/code-scanning/analyses","GET /repos/{owner}/{repo}/codespaces","GET /repos/{owner}/{repo}/codespaces/devcontainers","GET /repos/{owner}/{repo}/codespaces/secrets","GET /repos/{owner}/{repo}/collaborators","GET /repos/{owner}/{repo}/comments","GET /repos/{owner}/{repo}/comments/{comment_id}/reactions","GET /repos/{owner}/{repo}/commits","GET /repos/{owner}/{repo}/commits/{commit_sha}/comments","GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls","GET /repos/{owner}/{repo}/commits/{ref}/check-runs","GET /repos/{owner}/{repo}/commits/{ref}/check-suites","GET /repos/{owner}/{repo}/commits/{ref}/status","GET /repos/{owner}/{repo}/commits/{ref}/statuses","GET /repos/{owner}/{repo}/compare/{basehead}","GET /repos/{owner}/{repo}/compare/{base}...{head}","GET /repos/{owner}/{repo}/contributors","GET /repos/{owner}/{repo}/dependabot/alerts","GET /repos/{owner}/{repo}/dependabot/secrets","GET /repos/{owner}/{repo}/deployments","GET /repos/{owner}/{repo}/deployments/{deployment_id}/statuses","GET /repos/{owner}/{repo}/environments","GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies","GET /repos/{owner}/{repo}/environments/{environment_name}/deployment_protection_rules/apps","GET /repos/{owner}/{repo}/environments/{environment_name}/secrets","GET /repos/{owner}/{repo}/environments/{environment_name}/variables","GET /repos/{owner}/{repo}/events","GET /repos/{owner}/{repo}/forks","GET /repos/{owner}/{repo}/hooks","GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries","GET /repos/{owner}/{repo}/invitations","GET /repos/{owner}/{repo}/issues","GET /repos/{owner}/{repo}/issues/comments","GET /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions","GET /repos/{owner}/{repo}/issues/events","GET /repos/{owner}/{repo}/issues/{issue_number}/comments","GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by","GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocking","GET /repos/{owner}/{repo}/issues/{issue_number}/events","GET /repos/{owner}/{repo}/issues/{issue_number}/labels","GET /repos/{owner}/{repo}/issues/{issue_number}/reactions","GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues","GET /repos/{owner}/{repo}/issues/{issue_number}/timeline","GET /repos/{owner}/{repo}/keys","GET /repos/{owner}/{repo}/labels","GET /repos/{owner}/{repo}/milestones","GET /repos/{owner}/{repo}/milestones/{milestone_number}/labels","GET /repos/{owner}/{repo}/notifications","GET /repos/{owner}/{repo}/pages/builds","GET /repos/{owner}/{repo}/projects","GET /repos/{owner}/{repo}/pulls","GET /repos/{owner}/{repo}/pulls/comments","GET /repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions","GET /repos/{owner}/{repo}/pulls/{pull_number}/comments","GET /repos/{owner}/{repo}/pulls/{pull_number}/commits","GET /repos/{owner}/{repo}/pulls/{pull_number}/files","GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews","GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments","GET /repos/{owner}/{repo}/releases","GET /repos/{owner}/{repo}/releases/{release_id}/assets","GET /repos/{owner}/{repo}/releases/{release_id}/reactions","GET /repos/{owner}/{repo}/rules/branches/{branch}","GET /repos/{owner}/{repo}/rulesets","GET /repos/{owner}/{repo}/rulesets/rule-suites","GET /repos/{owner}/{repo}/rulesets/{ruleset_id}/history","GET /repos/{owner}/{repo}/secret-scanning/alerts","GET /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number}/locations","GET /repos/{owner}/{repo}/security-advisories","GET /repos/{owner}/{repo}/stargazers","GET /repos/{owner}/{repo}/subscribers","GET /repos/{owner}/{repo}/tags","GET /repos/{owner}/{repo}/teams","GET /repos/{owner}/{repo}/topics","GET /repositories","GET /search/code","GET /search/commits","GET /search/issues","GET /search/labels","GET /search/repositories","GET /search/topics","GET /search/users","GET /teams/{team_id}/discussions","GET /teams/{team_id}/discussions/{discussion_number}/comments","GET /teams/{team_id}/discussions/{discussion_number}/comments/{comment_number}/reactions","GET /teams/{team_id}/discussions/{discussion_number}/reactions","GET /teams/{team_id}/invitations","GET /teams/{team_id}/members","GET /teams/{team_id}/projects","GET /teams/{team_id}/repos","GET /teams/{team_id}/teams","GET /user/blocks","GET /user/codespaces","GET /user/codespaces/secrets","GET /user/emails","GET /user/followers","GET /user/following","GET /user/gpg_keys","GET /user/installations","GET /user/installations/{installation_id}/repositories","GET /user/issues","GET /user/keys","GET /user/marketplace_purchases","GET /user/marketplace_purchases/stubbed","GET /user/memberships/orgs","GET /user/migrations","GET /user/migrations/{migration_id}/repositories","GET /user/orgs","GET /user/packages","GET /user/packages/{package_type}/{package_name}/versions","GET /user/public_emails","GET /user/repos","GET /user/repository_invitations","GET /user/social_accounts","GET /user/ssh_signing_keys","GET /user/starred","GET /user/subscriptions","GET /user/teams","GET /users","GET /users/{username}/attestations/{subject_digest}","GET /users/{username}/events","GET /users/{username}/events/orgs/{org}","GET /users/{username}/events/public","GET /users/{username}/followers","GET /users/{username}/following","GET /users/{username}/gists","GET /users/{username}/gpg_keys","GET /users/{username}/keys","GET /users/{username}/orgs","GET /users/{username}/packages","GET /users/{username}/projects","GET /users/{username}/projectsV2","GET /users/{username}/projectsV2/{project_number}/fields","GET /users/{username}/projectsV2/{project_number}/items","GET /users/{username}/received_events","GET /users/{username}/received_events/public","GET /users/{username}/repos","GET /users/{username}/social_accounts","GET /users/{username}/ssh_signing_keys","GET /users/{username}/starred","GET /users/{username}/subscriptions"];function isPaginatingEndpoint(e){if(typeof e==="string"){return fa.includes(e)}else{return false}}function paginateRest(e){return{paginate:Object.assign(paginate.bind(null,e),{iterator:iterator.bind(null,e)})}}paginateRest.VERSION=pa;const Ca=new Context;const Qa=getApiBaseUrl();const Ia={baseUrl:Qa,request:{agent:getProxyAgent(Qa),fetch:getProxyFetch(Qa)}};const Ba=Octokit.plugin(restEndpointMethods,paginateRest).defaults(Ia);function getOctokitOptions(e,t){const r=Object.assign({},t||{});const s=getAuthString(e,r);if(s){r.auth=s}const n=getUserAgentWithOrchestrationId(r.userAgent);if(n){r.userAgent=n}return r}const ma=new Context;function getOctokit(e,t,...r){const s=Ba.plugin(...r);return new s(getOctokitOptions(e,t))}function getOctokitClient(){const e=io_getInput("github_token");if(!e){throw new Error("github_token is required")}return getOctokit(e)}async function getUserInfo(e){if(!e)return undefined;const t=getOctokitClient();const r=await t.rest.users.getByUsername({username:e});core_debug(`Fetched github actor from the API: ${JSON.stringify(r?.data,null,2)}`);return{name:r?.data?.name,email:r?.data?.email}}const wa=/[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu;function neutralizeLogString(e){return e.replace(wa,e=>{const t=e.codePointAt(0).toString(16).padStart(4,"0");return`\\u${t}`})}const ya="[Circular]";function neutralizeForLog(e,t=new WeakSet){if(typeof e==="string")return neutralizeLogString(e);if(typeof e==="number"||typeof e==="boolean"||e===null||e===undefined){return e}if(e instanceof Error){const t=new Error(neutralizeLogString(e.message));t.name=neutralizeLogString(e.name);if(e.stack){t.stack=neutralizeLogString(e.stack)}return t}if(Array.isArray(e)){if(t.has(e))return ya;t.add(e);return e.map(e=>neutralizeForLog(e,t))}if(typeof e==="object"){if(t.has(e))return ya;t.add(e);const r={};for(const[s,n]of Object.entries(e)){r[neutralizeLogString(s)]=neutralizeForLog(n,t)}return r}return e}function log(e,t){if(t)console.log(neutralizeForLog(t));if(e){const t=neutralizeForLog(e);if(typeof t==="string"||t instanceof Error){error(t)}else{error(String(t))}}}const ba=["user.name","user.email","author.name","author.email","committer.name","committer.email"];function pickGitIdentityConfig(e){const t={};for(const r of ba){if(Object.prototype.hasOwnProperty.call(e,r)){t[r]=e[r]}}return t}function assertValidBranchName(e){if(!e||!e.trim()){throw new Error("The new_branch value is empty.")}if(e.startsWith("-")){throw new Error(`The new_branch value '${e}' cannot start with '-' (it would be interpreted as a git option).`)}for(const t of e){const r=t.codePointAt(0);if(r<=31||r===127||r>=128&&r<=159||/\s/u.test(t)){throw new Error(`The new_branch value '${e}' contains whitespace or control characters.`)}}try{(0,ue.execFileSync)("git",["check-ref-format","--branch",e],{stdio:"ignore"})}catch{throw new Error(`The new_branch value '${e}' is not a valid git branch name.`)}}const ka=[{canonical:"upload-pack",minPrefix:"upl"},{canonical:"receive-pack",minPrefix:"rece"},{canonical:"exec",minPrefix:"e"}];const Ta=[{canonical:"file",minPrefix:"fi"}];const Ra=[{canonical:"message",minPrefix:"mes"},{canonical:"local-user",minPrefix:"local-"},{canonical:"cleanup",minPrefix:"cleanup"},{canonical:"file",minPrefix:"fi"},{canonical:"upload-pack",minPrefix:"upl"},{canonical:"receive-pack",minPrefix:"rece"},{canonical:"exec",minPrefix:"e"}];const Da=new Set(["m","u","F"]);function getLongOptionName(e){if(!e.startsWith("--")||e==="--")return undefined;const t=e.slice(2);const r=t.indexOf("=");return(r===-1?t:t.slice(0,r)).toLowerCase()}function longOptionHasInlineValue(e){if(!e.startsWith("--")||e==="--")return false;return e.slice(2).includes("=")}function matchesLongOptionPrefix(e,t){const r=getLongOptionName(e);if(!r)return false;return t.some(({canonical:e,minPrefix:t})=>r.length>=t.length&&e.startsWith(r))}function isDangerousRemoteHelperOption(e){return matchesLongOptionPrefix(e,ka)}function isDangerousMessageFileShortOption(e){if(!e.startsWith("-")||e.startsWith("--"))return false;const t=e.slice(1);for(let e=0;e` - ${e}`).join("\n");const r=e.map(e=>` git rm --cached -- ${e}`).join("\n");throw new Error(`Refusing to commit unexpected gitlink(s) (embedded git repository staged as mode 160000):\n${t}\n`+"Git records a nested .git directory as a gitlink, not as its files. "+`Remove the nested .git directory, or unstage the path(s) with:\n${r}`)}function parseInputArray(e){core_debug(`Parsing input array: ${e}`);try{const t=load(e);if(t&&Array.isArray(t)&&t.every(e=>typeof e==="string")){core_debug(`Input parsed as YAML array of length ${t.length}`);return t}}catch{}core_debug("Input parsed as single string");return[e]}function readJSON(e){let t;try{t=fs.readFileSync(e,{encoding:"utf8"})}catch{throw`Couldn't read file. File path: ${e}`}try{return JSON.parse(t)}catch{throw`Couldn't parse file to JSON. File path: ${e}`}}const Fa={committed:"false",commit_long_sha:undefined,commit_sha:undefined,pushed:"false",tagged:"false",tag_pushed:"false"};Object.entries(Fa).forEach(([e,t])=>setOutput(e,t));function io_getInput(e,t=false){if(t)return getBooleanInput(e);return getInput(e)}function io_setOutput(e,t){core_debug(`Setting output: ${e}=${t}`);Fa[e]=t;setOutput(e,t)}function parsePushAttempts(e){const t=e.trim();if(!/^\+?\d+$/.test(t)){throw new Error(`'${e}' is not a valid value for push_attempts. It must be a positive integer (≥ 1).`)}const r=Number.parseInt(t,10);if(!Number.isSafeInteger(r)||r<1){throw new Error(`'${e}' is not a valid value for push_attempts. It must be a positive integer (≥ 1).`)}return r}function logOutputs(){startGroup("Outputs");for(const e in Fa){info(`${e}: ${Fa[e]}`)}endGroup()}async function checkInputs(){function setInput(e,t){if(t)return process.env[`INPUT_${e.toUpperCase()}`]=t;else return delete process.env[`INPUT_${e.toUpperCase()}`]}function setDefault(e,t){if(!io_getInput(e))setInput(e,t);return io_getInput(e)}if(!io_getInput("add")&&!io_getInput("remove"))throw new Error("Both 'add' and 'remove' are empty, the action has nothing to do.");if(io_getInput("add")){const e=parseInputArray(io_getInput("add"));if(e.length===1)info("Add input parsed as single string, running 1 git add command.");else if(e.length>1)info(`Add input parsed as string array, running ${e.length} git add commands.`);else setFailed("Add input: array length < 1")}if(io_getInput("remove")){const e=parseInputArray(io_getInput("remove")||"");if(e.length===1)info("Remove input parsed as single string, running 1 git rm command.");else if(e.length>1)info(`Remove input parsed as string array, running ${e.length} git rm commands.`);else setFailed("Remove input: array length < 1")}const e=["github_actor","user_info","github_actions"];if(!e.includes(io_getInput("default_author")))throw new Error(`'${io_getInput("default_author")}' is not a valid value for default_author. Valid values: ${e.join(", ")}`);if(io_getInput("dry_run",true))info("> Dry run enabled: no mutating git operations will be performed.");if(io_getInput("allow_unsafe_git_protocols",true))warning("allow_unsafe_git_protocols is enabled: transport allowlist and scheme:: remote-helper URL checks are disabled. Only use this with fully trusted git argument inputs.");if(io_getInput("fetch")){let e;try{e=io_getInput("fetch",true)}catch{e=io_getInput("fetch")}core_debug(`Current fetch option: '${e}' (parsed as ${typeof e})`)}let t,r;switch(io_getInput("default_author")){case"github_actor":{t=process.env.GITHUB_ACTOR??"";r=`${process.env.GITHUB_ACTOR}@users.noreply.github.com`;break}case"user_info":{if(!io_getInput("author_name")||!io_getInput("author_email")){const e=await getUserInfo(process.env.GITHUB_ACTOR);if(!e?.name)warning("Couldn't fetch author name, filling with github_actor.");if(!e?.email)warning("Couldn't fetch author email, filling with github_actor.");e?.name&&(t=e?.name);e?.email&&(r=e.email);if(t&&r)break}!t&&(t=process.env.GITHUB_ACTOR??"");!r&&(r=`${process.env.GITHUB_ACTOR}@users.noreply.github.com`);break}case"github_actions":{t="github-actions";r="41898282+github-actions[bot]@users.noreply.github.com";break}default:throw new Error("This should not happen, please contact the author of this action. (checkInputs.author)")}setDefault("author_name",t);setDefault("author_email",r);info(`> Using '${io_getInput("author_name")} <${io_getInput("author_email")}>' as author.`);if(io_getInput("committer_name")||io_getInput("committer_email"))info(`> Using custom committer info: ${io_getInput("committer_name")||io_getInput("author_name")+" [from author info]"} <${io_getInput("committer_email")||io_getInput("author_email")+" [from author info]"}>`);setDefault("committer_name",io_getInput("author_name"));setDefault("committer_email",io_getInput("author_email"));core_debug(`Committer: ${io_getInput("committer_name")} <${io_getInput("committer_email")}>`);setDefault("message",`Commit from GitHub Actions (${process.env.GITHUB_WORKFLOW})`);info(`> Using "${io_getInput("message")}" as commit message.`);const s=io_getInput("new_branch");if(s)assertValidBranchName(s);const n=["ignore","exitImmediately","exitAtEnd"];if(!n.includes(io_getInput("pathspec_error_handling")))throw new Error(`"${io_getInput("pathspec_error_handling")}" is not a valid value for the 'pathspec_error_handling' input. Valid values are: ${n.join(", ")}`);if(io_getInput("pull")==="NO-PULL")warning("`NO-PULL` is a legacy option for the `pull` input. If you don't want the action to pull the repo, simply remove this input.");if(io_getInput("push")){let e;try{e=io_getInput("push",true)}catch{e=io_getInput("push")}core_debug(`Current push option: '${e}' (parsed as ${typeof e})`)}const o=parsePushAttempts(io_getInput("push_attempts")||"1");core_debug(`Current push_attempts option: ${o}`);if(o>1&&!io_getInput("pull")){warning("push_attempts is greater than 1 but pull is not set. Retries will re-run push only; without pull (e.g. --rebase), concurrent remote updates are unlikely to recover.")}if(!io_getInput("github_token"))warning("No github_token has been detected, the action may fail if it needs to use the API")}const va=u.join(process.cwd(),io_getInput("cwd")||"");const Ua=lo({baseDir:va});function gitChildEnv(e={}){const t={...process.env,...e};if(!io_getInput("allow_unsafe_git_protocols",true)){t.GIT_ALLOW_PROTOCOL="https:http:ssh:file:git";t.GIT_PROTOCOL_FROM_USER="0"}return t}function parseGitArgs(e){return matchGitArgs(e,{allowUnsafeGitProtocols:io_getInput("allow_unsafe_git_protocols",true)})}const Na=[];info(`Running in ${va}`);(async()=>{await checkInputs();Ua.env(gitChildEnv());const e=io_getInput("dry_run",true);startGroup("Internal logs");info(e?"> Staging files (dry run)...":"> Staging files...");const t=io_getInput("pathspec_error_handling")==="ignore"?"pathspec":"none";let r=false;if(io_getInput("add")){info(e?"> Adding files (dry run)...":"> Adding files...");const s=await add(t,e);if(e)r=r||s.some(e=>typeof e==="string"&&e.trim().length>0)}else info("> No files to add.");if(io_getInput("remove")){info(e?"> Removing files (dry run)...":"> Removing files...");const s=await main_remove(t,e);if(e)r=r||s.some(e=>{if(e===null||e===undefined)return false;const t=typeof e==="string"?e:String(e);return t.trim().length>0})}else info("> No files to remove.");info("> Checking for uncommitted changes in the git working tree...");const s=(await Ua.diffSummary(["--cached"])).files.length;const n=parseGitArgs(io_getInput("commit")||"").includes("--allow-empty");if(s>0||r||n){info(e?`> Dry run: would proceed (${s} already staged`+`${r?", staging probes reported changes":""}`+`${n?", --allow-empty":""}).`:`> Found ${s} changed files.`);core_debug(`--allow-empty argument detected: ${n}`);if(e){await logDryRunRemainingSteps();endGroup();info("> Dry run completed. No changes were made.");return}await Ua.addConfig("user.email",io_getInput("author_email"),undefined,log).addConfig("user.name",io_getInput("author_name"),undefined,log).addConfig("author.email",io_getInput("author_email"),undefined,log).addConfig("author.name",io_getInput("author_name"),undefined,log).addConfig("committer.email",io_getInput("committer_email"),undefined,log).addConfig("committer.name",io_getInput("committer_name"),undefined,log);if(isDebug()){const e=pickGitIdentityConfig((await Ua.listConfig()).all);core_debug(Object.keys(e).length?"> Current git identity config\n"+JSON.stringify(e,null,2):"> Git identity config set (no identity keys present in listConfig)")}let o;try{o=io_getInput("fetch",true)}catch{o=io_getInput("fetch")}if(o){info("> Fetching repo...");await Ua.fetch(parseGitArgs(o===true?"":o),log)}else info("> Not fetching repo.");const a=io_getInput("new_branch");if(a){info("> Checking-out branch...");if(!o)warning("Creating a new branch without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.");await Ua.checkout([a]).then(()=>{log(undefined,`'${a}' branch already existed.`)}).catch(()=>{log(undefined,`Creating '${a}' branch.`);return Ua.checkout(["-b",a],log)})}const l=io_getInput("pull");if(l){await pullFromRemote(l,{restage:true,ignoreErrors:t})}else info("> Not pulling from repo.");info("> Creating commit...");const u=await Ua.commit(io_getInput("message"),parseGitArgs(io_getInput("commit")||""));log(undefined,u);if(!u.commit){throw new Error("Commit did not produce a SHA; refusing to report committed=true.")}io_setOutput("committed","true");io_setOutput("commit_long_sha",u.commit);io_setOutput("commit_sha",u.commit.substring(0,7));if(io_getInput("tag")){info("> Tagging commit...");if(!o)warning("Creating a tag without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.");await Ua.tag(parseGitArgs(io_getInput("tag")||""),(e,t)=>{if(t)io_setOutput("tagged","true");return log(e,t)}).then(e=>{io_setOutput("tagged","true");return log(null,e)}).catch(e=>setFailed(e))}else info("> No tag info provided.");let g;try{g=io_getInput("push",true)}catch{g=io_getInput("push")}if(g){const e=parsePushAttempts(io_getInput("push_attempts")||"1");for(let r=1;r<=e;r++){try{info(e>1?`> Pushing commit to repo (attempt ${r}/${e})...`:"> Pushing commit to repo...");await pushCommit(g);break}catch(s){if(r===e)throw s;const n=s instanceof Error?s.message:String(s);warning(`Push failed (attempt ${r}/${e}): ${neutralizeLogString(n)}`);if(l){await pullFromRemote(l,{restage:false,ignoreErrors:t});const e=(await Ua.revparse(["HEAD"])).trim();io_setOutput("commit_long_sha",e);io_setOutput("commit_sha",e.substring(0,7))}}}if(io_getInput("tag")){info("> Pushing tags to repo...");await Ua.pushTags("origin",parseGitArgs(io_getInput("tag_push")||"")).then(e=>{io_setOutput("tag_pushed","true");return log(null,e)}).catch(e=>setFailed(e))}else info("> No tags to push.")}else info("> Not pushing anything.");endGroup();info("> Task completed.")}else{endGroup();info(e?"> Dry run: working tree clean. Nothing would be committed.":"> Working tree clean. Nothing to commit.")}})().then(()=>{if(Na.length===1)throw Na[0];else if(Na.length>1){Na.forEach(e=>error(e));throw"There have been multiple runtime errors."}}).then(logOutputs).catch(e=>{endGroup();logOutputs();setFailed(e)});async function logDryRunRemainingSteps(){info(`> Would set git identity: ${io_getInput("author_name")} <${io_getInput("author_email")}> (committer: ${io_getInput("committer_name")} <${io_getInput("committer_email")}>)`);let e;try{e=io_getInput("fetch",true)}catch{e=io_getInput("fetch")}if(e){info(`> Would fetch repo${e===true?"":` with: ${e}`}.`)}else info("> Would not fetch repo.");const t=io_getInput("new_branch");if(t){info(`> Would check out branch '${t}'.`);if(!e)warning("Creating a new branch without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.")}const r=io_getInput("pull");if(r)info(`> Would pull from remote with: ${r}.`);else info("> Would not pull from repo.");info(`> Would create commit with message: "${io_getInput("message")}"${io_getInput("commit")?` (extra args: ${io_getInput("commit")})`:""}.`);if(io_getInput("tag")){info(`> Would tag commit with: ${io_getInput("tag")}.`);if(!e)warning("Creating a tag without fetching the repo first could result in an error when pushing to GitHub. Refer to the action README for more info about this topic.")}else info("> No tag info provided.");let s;try{s=io_getInput("push",true)}catch{s=io_getInput("push")}if(s){const e=parsePushAttempts(io_getInput("push_attempts")||"1");if(s===true){const t=io_getInput("new_branch");info(t?`> Would push commit to repo (set upstream for '${t}')${e>1?` with up to ${e} attempts`:""}.`:`> Would push commit to repo${e>1?` with up to ${e} attempts`:""}.`)}else info(`> Would push commit to repo with: ${s}${e>1?` (up to ${e} attempts)`:""}.`);if(io_getInput("tag")){info(`> Would push tags to repo${io_getInput("tag_push")?` with: ${io_getInput("tag_push")}`:""}.`)}else info("> No tags to push.")}else info("> Would not push anything.")}async function pullFromRemote(e,t){info("> Pulling from remote...");core_debug(`Current git pull arguments: ${e}`);await Ua.fetch(undefined,log).pull(undefined,undefined,parseGitArgs(e),log);info("> Checking for conflicts...");const r=await Ua.status(undefined,log);if(r.conflicted.length){throw new Error(`There are ${r.conflicted.length} conflicting files: ${r.conflicted.map(neutralizeLogString).join(", ")}`)}info("> No conflicts found.");if(t.restage){info("> Re-staging files...");if(io_getInput("add"))await add(t.ignoreErrors);if(io_getInput("remove"))await main_remove(t.ignoreErrors)}}async function pushCommit(e){if(e===true){const e=io_getInput("new_branch");if(e){core_debug(`Running: git push --set-upstream origin -- ${e}`);await Ua.raw(["push","--set-upstream","origin","--",e],(e,t)=>{if(t)io_setOutput("pushed","true");return log(e,t)})}else{core_debug("Running: git push origin --set-upstream");await Ua.push("origin",undefined,{"--set-upstream":null},(e,t)=>{if(t)io_setOutput("pushed","true");return log(e,t)})}}else{core_debug(`Running: git push ${e}`);await Ua.push(undefined,undefined,parseGitArgs(e),(e,t)=>{if(t)io_setOutput("pushed","true");return log(e,t)})}}async function add(e="none",t=false){const r=io_getInput("add");if(!r)return[];const s=parseInputArray(r);const n=[];for(const r of s){const s=t?["--dry-run",...parseGitArgs(r)]:parseGitArgs(r);n.push(await Ua.add(s,(t,r)=>log(e==="all"?null:t,r)).catch(t=>{if(e==="all")return;if(t.message.includes("fatal: pathspec")&&t.message.includes("did not match any files")){if(e==="pathspec")return;const t=io_getInput("pathspec_error_handling"),s=new Error(`Add command did not match any file: git add ${r}`);if(t==="exitImmediately")throw s;if(t==="exitAtEnd")Na.push(s)}else throw t}))}if(t){await assertGitlinksWithTempIndex(s,e)}else{const e=await Ua.raw(["diff","--cached","--raw"]);assertNoUnexpectedGitlinks(findUnexpectedGitlinks(e))}return n}async function assertGitlinksWithTempIndex(e,t){const r=u.join(n.tmpdir(),`add-and-commit-${process.pid}-${Date.now()}.index`);try{const s=(await Ua.raw(["rev-parse","--git-path","index"])).trim();const n=u.isAbsolute(s)?s:u.join(va,s);if(l.existsSync(n)){l.copyFileSync(n,r)}else{const e=lo({baseDir:va}).env(gitChildEnv({GIT_INDEX_FILE:r}));const t=await Ua.raw(["rev-parse","--verify","HEAD"]).then(()=>true).catch(()=>false);if(t){await e.raw(["read-tree","HEAD"])}else{await e.raw(["read-tree","--empty"])}}const o=lo({baseDir:va}).env(gitChildEnv({GIT_INDEX_FILE:r}));for(const r of e){await o.add(parseGitArgs(r),(e,r)=>log(t==="all"?null:e,r)).catch(e=>{if(t==="all")return;if(e.message.includes("fatal: pathspec")&&e.message.includes("did not match any files")){if(t==="pathspec")return;const e=io_getInput("pathspec_error_handling");if(e==="exitImmediately"){throw new Error(`Add command did not match any file: git add ${r}`)}return}throw e})}const a=await o.raw(["diff","--cached","--raw"]);assertNoUnexpectedGitlinks(findUnexpectedGitlinks(a))}finally{l.rmSync(r,{force:true});l.rmSync(`${r}.lock`,{force:true})}}async function main_remove(e="none",t=false){const r=io_getInput("remove");if(!r)return[];const s=parseInputArray(r);const n=[];for(const r of s){const s=t?["--dry-run",...parseGitArgs(r)]:parseGitArgs(r);n.push(await Ua.rm(s,(t,r)=>log(e==="all"?null:t,r)).catch(t=>{if(e==="all")return;if(t.message.includes("fatal: pathspec")&&t.message.includes("did not match any files")){if(e==="pathspec")return;const t=io_getInput("pathspec_error_handling"),s=new Error(`Remove command did not match any file:\n git rm ${r}`);if(t==="exitImmediately")throw s;if(t==="exitAtEnd")Na.push(s)}else throw t}))}return n} \ No newline at end of file diff --git a/src/io.ts b/src/io.ts index 15cdb124..50ca9926 100644 --- a/src/io.ts +++ b/src/io.ts @@ -3,6 +3,7 @@ import {assertValidBranchName, getUserInfo, parseInputArray} from './util'; export interface InputTypes { add: string; + allow_unsafe_git_protocols: boolean; author_name: string; author_email: string; commit: string | undefined; @@ -155,6 +156,13 @@ export async function checkInputs() { ); // #endregion + // #region allow_unsafe_git_protocols + if (getInput('allow_unsafe_git_protocols', true)) + core.warning( + 'allow_unsafe_git_protocols is enabled: transport allowlist and scheme:: remote-helper URL checks are disabled. Only use this with fully trusted git argument inputs.', + ); + // #endregion + // #region fetch if (getInput('fetch')) { let value: string | boolean; diff --git a/src/main.ts b/src/main.ts index c3c93bd4..07ecb0d8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,12 +23,30 @@ import { const baseDir = path.join(process.cwd(), getInput('cwd') || ''); const git = simpleGit({baseDir}); +/** Env for git child processes; restricts transports unless opt-out is set. */ +function gitChildEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = {...process.env, ...extra}; + if (!getInput('allow_unsafe_git_protocols', true)) { + env.GIT_ALLOW_PROTOCOL = 'https:http:ssh:file:git'; + env.GIT_PROTOCOL_FROM_USER = '0'; + } + return env; +} + +function parseGitArgs(string: string) { + return matchGitArgs(string, { + allowUnsafeGitProtocols: getInput('allow_unsafe_git_protocols', true), + }); +} + const exitErrors: Error[] = []; core.info(`Running in ${baseDir}`); (async () => { await checkInputs(); + git.env(gitChildEnv()); + const dryRun = getInput('dry_run', true); core.startGroup('Internal logs'); @@ -63,7 +81,7 @@ core.info(`Running in ${baseDir}`); core.info('> Checking for uncommitted changes in the git working tree...'); const changedFiles = (await git.diffSummary(['--cached'])).files.length; - const allowEmpty = matchGitArgs(getInput('commit') || '').includes( + const allowEmpty = parseGitArgs(getInput('commit') || '').includes( '--allow-empty', ); // continue if there are any changes or if the allow-empty commit argument is included @@ -110,7 +128,7 @@ core.info(`Running in ${baseDir}`); if (fetchOption) { core.info('> Fetching repo...'); await git.fetch( - matchGitArgs(fetchOption === true ? '' : fetchOption), + parseGitArgs(fetchOption === true ? '' : fetchOption), log, ); } else core.info('> Not fetching repo.'); @@ -143,7 +161,7 @@ core.info(`Running in ${baseDir}`); core.info('> Creating commit...'); const data = await git.commit( getInput('message'), - matchGitArgs(getInput('commit') || ''), + parseGitArgs(getInput('commit') || ''), ); log(undefined, data); // simple-git can resolve with an empty SHA when no commit was created @@ -166,7 +184,7 @@ core.info(`Running in ${baseDir}`); ); await git - .tag(matchGitArgs(getInput('tag') || ''), (err, data?) => { + .tag(parseGitArgs(getInput('tag') || ''), (err, data?) => { if (data) setOutput('tagged', 'true'); return log(err, data); }) @@ -220,7 +238,7 @@ core.info(`Running in ${baseDir}`); core.info('> Pushing tags to repo...'); await git - .pushTags('origin', matchGitArgs(getInput('tag_push') || '')) + .pushTags('origin', parseGitArgs(getInput('tag_push') || '')) .then(data => { setOutput('tag_pushed', 'true'); return log(null, data); @@ -352,7 +370,7 @@ async function pullFromRemote( core.debug(`Current git pull arguments: ${pullOption}`); await git .fetch(undefined, log) - .pull(undefined, undefined, matchGitArgs(pullOption), log); + .pull(undefined, undefined, parseGitArgs(pullOption), log); core.info('> Checking for conflicts...'); const status = await git.status(undefined, log); @@ -404,7 +422,7 @@ async function pushCommit(pushOption: true | string) { await git.push( undefined, undefined, - matchGitArgs(pushOption), + parseGitArgs(pushOption), (err, data?) => { if (data) setOutput('pushed', 'true'); return log(err, data); @@ -425,8 +443,8 @@ async function add( for (const args of parsed) { const gitArgs = dryRun - ? ['--dry-run', ...matchGitArgs(args)] - : matchGitArgs(args); + ? ['--dry-run', ...parseGitArgs(args)] + : parseGitArgs(args); res.push( // Push the result of every git command (which are executed in order) to the array // If any of them fails, the whole function will return a Promise rejection @@ -490,10 +508,9 @@ async function assertGitlinksWithTempIndex( if (fs.existsSync(indexPath)) { fs.copyFileSync(indexPath, tmpIndex); } else { - const gitSeed = simpleGit({baseDir}).env({ - ...process.env, - GIT_INDEX_FILE: tmpIndex, - }); + const gitSeed = simpleGit({baseDir}).env( + gitChildEnv({GIT_INDEX_FILE: tmpIndex}), + ); const headResolves = await git .raw(['rev-parse', '--verify', 'HEAD']) .then(() => true) @@ -505,14 +522,13 @@ async function assertGitlinksWithTempIndex( } } - const gitTmp = simpleGit({baseDir}).env({ - ...process.env, - GIT_INDEX_FILE: tmpIndex, - }); + const gitTmp = simpleGit({baseDir}).env( + gitChildEnv({GIT_INDEX_FILE: tmpIndex}), + ); for (const args of addArgGroups) { await gitTmp - .add(matchGitArgs(args), (err, data) => + .add(parseGitArgs(args), (err, data) => log(ignoreErrors === 'all' ? null : err, data), ) .catch((e: Error) => { @@ -556,8 +572,8 @@ async function remove( for (const args of parsed) { const gitArgs = dryRun - ? ['--dry-run', ...matchGitArgs(args)] - : matchGitArgs(args); + ? ['--dry-run', ...parseGitArgs(args)] + : parseGitArgs(args); res.push( // Push the result of every git command (which are executed in order) to the array // If any of them fails, the whole function will return a Promise rejection diff --git a/src/util.ts b/src/util.ts index 0bdc6057..ea4c1fb4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -323,6 +323,24 @@ function assertBalancedQuotes(input: string): void { } } +/** + * Git remote-helper URL form (`ext::command`, `hg::…`, etc.). + * @see https://git-scm.com/docs/gitremote-helpers + */ +const REMOTE_HELPER_URL = /^[A-Za-z0-9+.-]+::/; + +function isRemoteHelperUrl(arg: string): boolean { + return REMOTE_HELPER_URL.test(arg); +} + +export type MatchGitArgsOptions = { + /** + * When true, allow `scheme::` remote-helper URL tokens. + * Does not disable `--upload-pack` / `-F` denylists. + */ + allowUnsafeGitProtocols?: boolean; +}; + /** * Matches the given string to an array of arguments. * The parsing is made by `string-argv`: if your way of using argument is not supported, the issue is theirs! @@ -350,8 +368,12 @@ function assertBalancedQuotes(input: string): void { * @throws If the args include unmatched quotes * @throws If the args include a blocked remote-helper override (`--upload-pack`, `--receive-pack`, `--exec`, or abbreviations) * @throws If the args include a blocked message-from-file flag (`-F`, `--file`, abbreviations, or short-option clusters containing `F`) + * @throws If the args include a `scheme::` remote-helper URL (unless `allowUnsafeGitProtocols`) */ -export function matchGitArgs(string: string) { +export function matchGitArgs( + string: string, + options: MatchGitArgsOptions = {}, +) { assertBalancedQuotes(string); const parsed = parseArgsStringToArgv(string); @@ -359,6 +381,8 @@ export function matchGitArgs(string: string) { - Original: ${string} - Parsed: ${JSON.stringify(parsed)}`); + const allowUnsafe = options.allowUnsafeGitProtocols === true; + let skipNext = false; for (const arg of parsed) { if (skipNext) { @@ -376,6 +400,11 @@ export function matchGitArgs(string: string) { `Git argument '${arg}' is not allowed: reading a tag/commit message from a file (-F/--file) can exfiltrate runner filesystem contents into git history.`, ); } + if (!allowUnsafe && isRemoteHelperUrl(arg)) { + throw new Error( + `Git argument '${arg}' is not allowed: remote-helper URLs (scheme::…) can execute arbitrary commands on the runner. Set allow_unsafe_git_protocols to true only if you fully trust this input.`, + ); + } skipNext = consumesFollowingArgument(arg); } diff --git a/test/integration/helpers.ts b/test/integration/helpers.ts index 21f81d15..9d8d350b 100644 --- a/test/integration/helpers.ts +++ b/test/integration/helpers.ts @@ -159,6 +159,7 @@ function parseGitHubOutput(filePath: string): Record { export interface ActionInputs { add?: string; + allow_unsafe_git_protocols?: string; author_name?: string; author_email?: string; commit?: string; @@ -214,6 +215,7 @@ export function runAction( // Mirror action.yml defaults that matter when spawning lib/ directly. cwd: '.', add: '.', + allow_unsafe_git_protocols: 'false', default_author: 'github_actor', dry_run: 'false', pathspec_error_handling: 'ignore', diff --git a/test/util.test.ts b/test/util.test.ts index 19a61da7..f6a5b648 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -252,6 +252,48 @@ describe('matchGitArgs', () => { /message from a file/, ); }); + + it('rejects scheme:: remote-helper URL tokens (PoC form)', () => { + expect(() => matchGitArgs('ext::sh -c touch\\ /tmp/pwned')).toThrow( + /remote-helper URLs/, + ); + expect(() => matchGitArgs('ext::touch /tmp/pwned')).toThrow( + /allow_unsafe_git_protocols/, + ); + expect(() => matchGitArgs('evil::anything')).toThrow(/remote-helper URLs/); + expect(() => matchGitArgs('origin evil::x --force')).toThrow( + /remote-helper URLs/, + ); + expect(() => matchGitArgs("'ext::sh -c touch /tmp/pwned'")).toThrow( + /remote-helper URLs/, + ); + }); + + it('allows scheme:: tokens when allowUnsafeGitProtocols is true', () => { + expect( + matchGitArgs('ext::sh -c true', {allowUnsafeGitProtocols: true}), + ).toStrictEqual(['ext::sh', '-c', 'true']); + expect( + matchGitArgs('evil::anything', {allowUnsafeGitProtocols: true}), + ).toStrictEqual(['evil::anything']); + expect( + matchGitArgs("'ext::sh -c true'", {allowUnsafeGitProtocols: true}), + ).toStrictEqual(['ext::sh -c true']); + }); + + it('still rejects --upload-pack when allowUnsafeGitProtocols is true', () => { + expect(() => + matchGitArgs('--upload-pack=/bin/sh', {allowUnsafeGitProtocols: true}), + ).toThrow(/not allowed/); + }); + + it('allows :: inside option values via skipNext', () => { + expect(matchGitArgs('-m "foo::bar"')).toStrictEqual(['-m', 'foo::bar']); + expect(matchGitArgs('--message foo::bar')).toStrictEqual([ + '--message', + 'foo::bar', + ]); + }); }); describe('pickGitIdentityConfig', () => { From 5cbcbd2eb98d21ad5fb7dd6be0c1bf5a9be56c01 Mon Sep 17 00:00:00 2001 From: Federico Grandi Date: Fri, 14 Aug 2026 21:17:54 +0200 Subject: [PATCH 2/2] fix: quote allow_unsafe description and correct README anchor Co-authored-by: Cursor --- README.md | 2 +- action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 36b6e83b..a2095f50 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ What does this mean for you? It means that strings that contain a lot of nested Remote-helper overrides (`--upload-pack`, `--receive-pack`, `--exec`, and abbreviations of those) are rejected: they can make git run an arbitrary Git transport program during fetch/pull/push. Remote-helper URL forms (`ext::…` and other `scheme::` tokens) are rejected for the same reason. -Git child processes are also limited to the `https`, `http`, `ssh`, `file`, and `git` transports (`GIT_ALLOW_PROTOCOL`) unless you set [`allow_unsafe_git_protocols`](#allow_unsafe_git_protocols) to `true` (only for trusted custom remotes/helpers). +Git child processes are also limited to the `https`, `http`, `ssh`, `file`, and `git` transports (`GIT_ALLOW_PROTOCOL`) unless you set [`allow_unsafe_git_protocols`](#allow-unsafe-git-protocols) to `true` (only for trusted custom remotes/helpers). Message-from-file flags (`-F`, `--file`, abbreviations such as `--fi`, and short-option clusters that include `F` such as `-aF`) are rejected: they can embed arbitrary runner filesystem contents into a tag or commit message and, with a push, into the repository history. Unmatched `'` / `"` quotes are also rejected: `string-argv` can otherwise split on an odd quote and turn part of a value into extra flags (for example a branch name like `fix'--force` becoming `fix` plus `--force`). Do not interpolate untrusted data (for example values from `github.event.*`, `github.head_ref`, or repository content that contributors can edit) into `fetch`, `pull`, `push`, `tag`, `tag_push`, or `commit` without sanitizing them first. When the branch name is dynamic, prefer the default `push: true` with [`new_branch`](#creating-a-new-branch) instead of embedding the ref in a custom `push` string. diff --git a/action.yml b/action.yml index 8ffd3508..ef42c8fe 100644 --- a/action.yml +++ b/action.yml @@ -69,7 +69,7 @@ inputs: required: false allow_unsafe_git_protocols: - description: If true, disables the transport protocol allowlist (https/http/ssh/file/git) and allows scheme:: remote-helper URLs in git argument inputs. Keep false unless you need a custom remote helper and fully trust those inputs. + description: 'If true, disables the transport protocol allowlist (https/http/ssh/file/git) and allows scheme:: remote-helper URLs in git argument inputs. Keep false unless you need a custom remote helper and fully trust those inputs.' required: false default: 'false'