镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Add a step like this to your workflow:
Multiple options let you provide the `git` arguments that you want the action to use. It's important to note that these arguments **are not actually used with a CLI command**, but they are parsed by a package called [`string-argv`](https://npm.im/string-argv), and then used with [`simple-git`](https://npm.im/simple-git).
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://github.com/proxy/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 local program during fetch/pull/push.
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.
Do not interpolate untrusted data (for example values from `github.event.*`) into `fetch`, `pull`, `push`, or `tag_push` without sanitizing them first.

### Adding files
Expand Down Expand Up @@ -129,7 +129,7 @@ One way to use this is if you want to force push to a branch of your repo: you'l

### Creating a new branch

If you want the action to commit in a new branch, you can use the `new_branch` input.
If you want the action to commit in a new branch, you can use the `new_branch` input. This must be a valid git branch name (not raw git arguments): it cannot be empty, start with `-`, contain whitespace/control characters, or fail `git check-ref-format --branch` (for example `feature..name`, `name@{x}`, `name~1`, or a name ending with `.`).

Please note that if the branch exists, the action will still try push to it, but it's possible that the push will be rejected by the remote as non-straightforward.

Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ inputs:
description: The message for the commit
required: false
new_branch:
description: The name of the branch to create.
description: The name of the branch to create (must pass git check-ref-format --branch; cannot start with '-' or contain whitespace/control characters).
required: false
pathspec_error_handling:
description: The way the action should handle pathspec errors from the add and remove commands.
Expand Down
16 changes: 8 additions & 8 deletions lib/index.js

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion src/io.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as core from '@actions/core';
import {getUserInfo, parseInputArray} from './util';
import {assertValidBranchName, getUserInfo, parseInputArray} from './util';

export interface InputTypes {
add: string;
Expand Down Expand Up @@ -220,6 +220,11 @@ export async function checkInputs() {
core.info(`> Using "${getInput('message')}" as commit message.`);
// #endregion

// #region new_branch
const newBranch = getInput('new_branch');
if (newBranch) assertValidBranchName(newBranch);
// #endregion

// #region pathspec_error_handling
const peh_valid = ['ignore', 'exitImmediately', 'exitAtEnd'];
if (!peh_valid.includes(getInput('pathspec_error_handling')))
Expand Down
40 changes: 24 additions & 16 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ core.info(`Running in ${baseDir}`);
);

await git
.checkout(targetBranch)
.checkout([targetBranch])
.then(() => {
log(undefined, `'${targetBranch}' branch already existed.`);
})
.catch(() => {
log(undefined, `Creating '${targetBranch}' branch.`);
return git.checkoutLocalBranch(targetBranch, log);
return git.checkout(['-b', targetBranch], log);
});
}

Expand Down Expand Up @@ -155,20 +155,28 @@ core.info(`Running in ${baseDir}`);
core.info('> Pushing commit to repo...');

if (pushOption === true) {
core.debug(
`Running: git push origin ${
getInput('new_branch') || ''
} --set-upstream`,
);
await git.push(
'origin',
getInput('new_branch'),
{'--set-upstream': null},
(err, data?) => {
if (data) setOutput('pushed', 'true');
return log(err, data);
},
);
const branch = getInput('new_branch');
if (branch) {
core.debug(`Running: git push --set-upstream origin -- ${branch}`);
await git.raw(
['push', '--set-upstream', 'origin', '--', branch],
(err, data?) => {
if (data) setOutput('pushed', 'true');
return log(err, data);
},
);
} else {
core.debug('Running: git push origin --set-upstream');
await git.push(
'origin',
undefined,
{'--set-upstream': null},
(err, data?) => {
if (data) setOutput('pushed', 'true');
return log(err, data);
},
);
}
} else {
core.debug(`Running: git push ${pushOption}`);
await git.push(
Expand Down
41 changes: 41 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {parseArgsStringToArgv} from 'string-argv';
import * as core from '@actions/core';
import * as YAML from 'js-yaml';
import {getOctokit} from '@actions/github';
import {execFileSync} from 'child_process';
import * as fs from 'fs';
import {getInput} from './io';

Expand Down Expand Up @@ -35,6 +36,46 @@ export function log(err: any, data?: any) {
if (err) core.error(err);
}

/**
* Ensures `name` is safe to pass as a single git branch/ref positional argument.
* Rejects empty values, leading hyphens (git option injection), whitespace/control
* chars, and names rejected by `git check-ref-format --branch`.
*/
export function assertValidBranchName(name: string): void {
if (!name || !name.trim()) {
throw new Error('The new_branch value is empty.');
}
if (name.startsWith('-')) {
throw new Error(
`The new_branch value '${name}' cannot start with '-' (it would be interpreted as a git option).`,
);
}
for (const char of name) {
const code = char.codePointAt(0)!;
if (
code <= 0x1f || // C0 controls
code === 0x7f || // DEL
(code >= 0x80 && code <= 0x9f) || // C1 controls
/\s/u.test(char) // Unicode whitespace (e.g. NBSP)
) {
throw new Error(
`The new_branch value '${name}' contains whitespace or control characters.`,
);
}
}

try {
// Leading '-' is already rejected above so this cannot be parsed as a flag.
execFileSync('git', ['check-ref-format', '--branch', name], {
stdio: 'ignore',
});
} catch {
throw new Error(
`The new_branch value '${name}' is not a valid git branch name.`,
);
}
}

/**
* Remote-helper overrides that make the local git client execute an arbitrary
* program during fetch/pull/push. Git also accepts unique abbreviations of
Expand Down
77 changes: 76 additions & 1 deletion test/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {matchGitArgs, parseInputArray} from '../src/util';
import {
assertValidBranchName,
matchGitArgs,
parseInputArray,
} from '../src/util';

describe('parseInputArray', () => {
beforeAll(() => {
Expand Down Expand Up @@ -32,6 +36,77 @@ describe('parseInputArray', () => {
});
});

describe('assertValidBranchName', () => {
beforeAll(() => {
process.env.GITHUB_EVENT_PATH = 'a';
process.env.GITHUB_EVENT_NAME = 'b';
process.env.GITHUB_REF = 'c';
process.env.GITHUB_ACTOR = 'd';
});

afterAll(() => {
delete process.env.GITHUB_EVENT_PATH;
delete process.env.GITHUB_EVENT_NAME;
delete process.env.GITHUB_REF;
delete process.env.GITHUB_ACTOR;
});

it('accepts typical branch names', () => {
expect(() => assertValidBranchName('feature/foo')).not.toThrow();
expect(() =>
assertValidBranchName('dependabot/npm-and-yarn/pkg-1.2.3'),
).not.toThrow();
expect(() => assertValidBranchName('fix/issue-123')).not.toThrow();
});

it('rejects empty or whitespace-only names', () => {
expect(() => assertValidBranchName('')).toThrow(/empty/);
expect(() => assertValidBranchName(' ')).toThrow(/empty/);
});

it('rejects names that start with a hyphen', () => {
expect(() => assertValidBranchName('--force')).toThrow(/cannot start with/);
expect(() => assertValidBranchName('-f')).toThrow(/cannot start with/);
expect(() => assertValidBranchName('--delete')).toThrow(
/cannot start with/,
);
expect(() => assertValidBranchName('-branch')).toThrow(/cannot start with/);
});

it('rejects names with whitespace or control characters', () => {
expect(() => assertValidBranchName('has space')).toThrow(
/whitespace or control/,
);
expect(() => assertValidBranchName('has\ttab')).toThrow(
/whitespace or control/,
);
expect(() => assertValidBranchName('has\nnewline')).toThrow(
/whitespace or control/,
);
expect(() => assertValidBranchName('has\u00A0nbsp')).toThrow(
/whitespace or control/,
);
expect(() => assertValidBranchName('has\u0085nel')).toThrow(
/whitespace or control/,
);
});

it('rejects names that fail git check-ref-format --branch', () => {
expect(() => assertValidBranchName('feature..name')).toThrow(
/not a valid git branch name/,
);
expect(() => assertValidBranchName('name@{x}')).toThrow(
/not a valid git branch name/,
);
expect(() => assertValidBranchName('name~1')).toThrow(
/not a valid git branch name/,
);
expect(() => assertValidBranchName('name.')).toThrow(
/not a valid git branch name/,
);
});
});

describe('matchGitArgs', () => {
beforeAll(() => {
process.env.GITHUB_EVENT_PATH = 'a';
Expand Down