Implement branch list using callbacks from exec function (#1045)

When trying to list local branches to figure out what needs cleaned up during runs on non-ephemeral Actions Runners, we use git rev-parse --symbolic-full-name to get a list of branches. This can lead to ambiguous ref name errors when there are branches and tags with similar names.

Part of the reason we use rev-parse --symbolic-full-name vs git branch --list or git rev-parse --symbolic seems to related to a bug in Git 2.18. Until we can deprecate our usage of Git 2.18, I think we need to keep --symbolic-full-name. Since part of the problem is that these ambiguous ref name errors clog the Actions annotation limits, this is a mitigation to suppress those messages until we can get rid of the workaround.
This commit is contained in:
Cory Miller 2022-12-14 16:08:53 -05:00 committed by GitHub
parent 755da8c3cf
commit 8856415920
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 243 additions and 57 deletions

View file

@ -1,6 +1,6 @@
--- ---
name: qs name: qs
version: 6.10.1 version: 6.11.0
type: npm type: npm
summary: A querystring parser that supports nesting and arrays, with a depth limit summary: A querystring parser that supports nesting and arrays, with a depth limit
homepage: https://github.com/ljharb/qs homepage: https://github.com/ljharb/qs

View file

@ -0,0 +1,80 @@
import * as exec from '@actions/exec'
import * as fshelper from '../lib/fs-helper'
import * as commandManager from '../lib/git-command-manager'
let git: commandManager.IGitCommandManager
let mockExec = jest.fn()
describe('git-auth-helper tests', () => {
beforeAll(async () => {})
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
})
afterEach(() => {
jest.restoreAllMocks()
})
afterAll(() => {})
it('branch list matches', async () => {
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
return 0
}
if (args.includes('rev-parse')) {
options.listeners.stdline(Buffer.from('refs/heads/foo'))
options.listeners.stdline(Buffer.from('refs/heads/bar'))
return 0
}
return 1
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
git = await commandManager.createCommandManager(workingDirectory, lfs)
let branches = await git.branchList(false)
expect(branches).toHaveLength(2)
expect(branches.sort()).toEqual(['foo', 'bar'].sort())
})
it('ambiguous ref name output is captured', async () => {
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)
if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
return 0
}
if (args.includes('rev-parse')) {
options.listeners.stdline(Buffer.from('refs/heads/foo'))
// If refs/tags/v1 and refs/heads/tags/v1 existed on this repository
options.listeners.errline(
Buffer.from("error: refname 'tags/v1' is ambiguous")
)
return 0
}
return 1
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
git = await commandManager.createCommandManager(workingDirectory, lfs)
let branches = await git.branchList(false)
expect(branches).toHaveLength(1)
expect(branches.sort()).toEqual(['foo'].sort())
})
})

127
dist/index.js vendored
View file

@ -7441,8 +7441,10 @@ class GitCommandManager {
const result = []; const result = [];
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from // Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
// "branch --list" is more difficult when in a detached HEAD state. // "branch --list" is more difficult when in a detached HEAD state.
// Note, this implementation uses "rev-parse --symbolic-full-name" because there is a bug // TODO(https://github.com/actions/checkout/issues/786): this implementation uses
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. // "rev-parse --symbolic-full-name" because there is a bug
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
// 2.18 is no longer supported, we can switch back to --symbolic.
const args = ['rev-parse', '--symbolic-full-name']; const args = ['rev-parse', '--symbolic-full-name'];
if (remote) { if (remote) {
args.push('--remotes=origin'); args.push('--remotes=origin');
@ -7450,18 +7452,42 @@ class GitCommandManager {
else { else {
args.push('--branches'); args.push('--branches');
} }
const output = yield this.execGit(args); const stderr = [];
for (let branch of output.stdout.trim().split('\n')) { const errline = [];
branch = branch.trim(); const stdout = [];
if (branch) { const stdline = [];
if (branch.startsWith('refs/heads/')) { const listeners = {
branch = branch.substr('refs/heads/'.length); stderr: (data) => {
} stderr.push(data.toString());
else if (branch.startsWith('refs/remotes/')) { },
branch = branch.substr('refs/remotes/'.length); errline: (data) => {
} errline.push(data.toString());
result.push(branch); },
stdout: (data) => {
stdout.push(data.toString());
},
stdline: (data) => {
stdline.push(data.toString());
} }
};
// Suppress the output in order to avoid flooding annotations with innocuous errors.
yield this.execGit(args, false, true, listeners);
core.debug(`stderr callback is: ${stderr}`);
core.debug(`errline callback is: ${errline}`);
core.debug(`stdout callback is: ${stdout}`);
core.debug(`stdline callback is: ${stdline}`);
for (let branch of stdline) {
branch = branch.trim();
if (!branch) {
continue;
}
if (branch.startsWith('refs/heads/')) {
branch = branch.substring('refs/heads/'.length);
}
else if (branch.startsWith('refs/remotes/')) {
branch = branch.substring('refs/remotes/'.length);
}
result.push(branch);
} }
return result; return result;
}); });
@ -7712,7 +7738,7 @@ class GitCommandManager {
return result; return result;
}); });
} }
execGit(args, allowAllExitCodes = false, silent = false) { execGit(args, allowAllExitCodes = false, silent = false, customListeners = {}) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
fshelper.directoryExistsSync(this.workingDirectory, true); fshelper.directoryExistsSync(this.workingDirectory, true);
const result = new GitOutput(); const result = new GitOutput();
@ -7723,20 +7749,24 @@ class GitCommandManager {
for (const key of Object.keys(this.gitEnv)) { for (const key of Object.keys(this.gitEnv)) {
env[key] = this.gitEnv[key]; env[key] = this.gitEnv[key];
} }
const defaultListener = {
stdout: (data) => {
stdout.push(data.toString());
}
};
const mergedListeners = Object.assign(Object.assign({}, defaultListener), customListeners);
const stdout = []; const stdout = [];
const options = { const options = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
env, env,
silent, silent,
ignoreReturnCode: allowAllExitCodes, ignoreReturnCode: allowAllExitCodes,
listeners: { listeners: mergedListeners
stdout: (data) => {
stdout.push(data.toString());
}
}
}; };
result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options); result.exitCode = yield exec.exec(`"${this.gitPath}"`, args, options);
result.stdout = stdout.join(''); result.stdout = stdout.join('');
core.debug(result.exitCode.toString());
core.debug(result.stdout);
return result; return result;
}); });
} }
@ -13947,6 +13977,7 @@ var encode = function encode(str, defaultEncoder, charset, kind, format) {
i += 1; i += 1;
c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF)); c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF));
/* eslint operator-linebreak: [2, "before"] */
out += hexTable[0xF0 | (c >> 18)] out += hexTable[0xF0 | (c >> 18)]
+ hexTable[0x80 | ((c >> 12) & 0x3F)] + hexTable[0x80 | ((c >> 12) & 0x3F)]
+ hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | ((c >> 6) & 0x3F)]
@ -17572,7 +17603,7 @@ var parseObject = function (chain, val, options, valuesParsed) {
) { ) {
obj = []; obj = [];
obj[index] = leaf; obj[index] = leaf;
} else { } else if (cleanRoot !== '__proto__') {
obj[cleanRoot] = leaf; obj[cleanRoot] = leaf;
} }
} }
@ -34704,6 +34735,7 @@ var arrayPrefixGenerators = {
}; };
var isArray = Array.isArray; var isArray = Array.isArray;
var split = String.prototype.split;
var push = Array.prototype.push; var push = Array.prototype.push;
var pushToArray = function (arr, valueOrArray) { var pushToArray = function (arr, valueOrArray) {
push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]); push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]);
@ -34740,10 +34772,13 @@ var isNonNullishPrimitive = function isNonNullishPrimitive(v) {
|| typeof v === 'bigint'; || typeof v === 'bigint';
}; };
var sentinel = {};
var stringify = function stringify( var stringify = function stringify(
object, object,
prefix, prefix,
generateArrayPrefix, generateArrayPrefix,
commaRoundTrip,
strictNullHandling, strictNullHandling,
skipNulls, skipNulls,
encoder, encoder,
@ -34759,8 +34794,23 @@ var stringify = function stringify(
) { ) {
var obj = object; var obj = object;
if (sideChannel.has(object)) { var tmpSc = sideChannel;
throw new RangeError('Cyclic object value'); var step = 0;
var findFlag = false;
while ((tmpSc = tmpSc.get(sentinel)) !== void undefined && !findFlag) {
// Where object last appeared in the ref tree
var pos = tmpSc.get(object);
step += 1;
if (typeof pos !== 'undefined') {
if (pos === step) {
throw new RangeError('Cyclic object value');
} else {
findFlag = true; // Break while
}
}
if (typeof tmpSc.get(sentinel) === 'undefined') {
step = 0;
}
} }
if (typeof filter === 'function') { if (typeof filter === 'function') {
@ -34787,6 +34837,14 @@ var stringify = function stringify(
if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) { if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) {
if (encoder) { if (encoder) {
var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format); var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format);
if (generateArrayPrefix === 'comma' && encodeValuesOnly) {
var valuesArray = split.call(String(obj), ',');
var valuesJoined = '';
for (var i = 0; i < valuesArray.length; ++i) {
valuesJoined += (i === 0 ? '' : ',') + formatter(encoder(valuesArray[i], defaults.encoder, charset, 'value', format));
}
return [formatter(keyValue) + (commaRoundTrip && isArray(obj) && valuesArray.length === 1 ? '[]' : '') + '=' + valuesJoined];
}
return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))]; return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))];
} }
return [formatter(prefix) + '=' + formatter(String(obj))]; return [formatter(prefix) + '=' + formatter(String(obj))];
@ -34801,7 +34859,7 @@ var stringify = function stringify(
var objKeys; var objKeys;
if (generateArrayPrefix === 'comma' && isArray(obj)) { if (generateArrayPrefix === 'comma' && isArray(obj)) {
// we need to join elements in // we need to join elements in
objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : undefined }]; objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
} else if (isArray(filter)) { } else if (isArray(filter)) {
objKeys = filter; objKeys = filter;
} else { } else {
@ -34809,24 +34867,28 @@ var stringify = function stringify(
objKeys = sort ? keys.sort(sort) : keys; objKeys = sort ? keys.sort(sort) : keys;
} }
for (var i = 0; i < objKeys.length; ++i) { var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? prefix + '[]' : prefix;
var key = objKeys[i];
var value = typeof key === 'object' && key.value !== undefined ? key.value : obj[key]; for (var j = 0; j < objKeys.length; ++j) {
var key = objKeys[j];
var value = typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key];
if (skipNulls && value === null) { if (skipNulls && value === null) {
continue; continue;
} }
var keyPrefix = isArray(obj) var keyPrefix = isArray(obj)
? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(prefix, key) : prefix ? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(adjustedPrefix, key) : adjustedPrefix
: prefix + (allowDots ? '.' + key : '[' + key + ']'); : adjustedPrefix + (allowDots ? '.' + key : '[' + key + ']');
sideChannel.set(object, true); sideChannel.set(object, step);
var valueSideChannel = getSideChannel(); var valueSideChannel = getSideChannel();
valueSideChannel.set(sentinel, sideChannel);
pushToArray(values, stringify( pushToArray(values, stringify(
value, value,
keyPrefix, keyPrefix,
generateArrayPrefix, generateArrayPrefix,
commaRoundTrip,
strictNullHandling, strictNullHandling,
skipNulls, skipNulls,
encoder, encoder,
@ -34850,7 +34912,7 @@ var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
return defaults; return defaults;
} }
if (opts.encoder !== null && opts.encoder !== undefined && typeof opts.encoder !== 'function') { if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
throw new TypeError('Encoder has to be a function.'); throw new TypeError('Encoder has to be a function.');
} }
@ -34923,6 +34985,10 @@ module.exports = function (object, opts) {
} }
var generateArrayPrefix = arrayPrefixGenerators[arrayFormat]; var generateArrayPrefix = arrayPrefixGenerators[arrayFormat];
if (opts && 'commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') {
throw new TypeError('`commaRoundTrip` must be a boolean, or absent');
}
var commaRoundTrip = generateArrayPrefix === 'comma' && opts && opts.commaRoundTrip;
if (!objKeys) { if (!objKeys) {
objKeys = Object.keys(obj); objKeys = Object.keys(obj);
@ -34943,6 +35009,7 @@ module.exports = function (object, opts) {
obj[key], obj[key],
key, key,
generateArrayPrefix, generateArrayPrefix,
commaRoundTrip,
options.strictNullHandling, options.strictNullHandling,
options.skipNulls, options.skipNulls,
options.encode ? options.encoder : null, options.encode ? options.encoder : null,

12
package-lock.json generated
View file

@ -17110,9 +17110,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.10.1", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
}, },
@ -31553,9 +31553,9 @@
"dev": true "dev": true
}, },
"qs": { "qs": {
"version": "6.10.1", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": { "requires": {
"side-channel": "^1.0.4" "side-channel": "^1.0.4"
} }

View file

@ -94,8 +94,11 @@ class GitCommandManager {
// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from // Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
// "branch --list" is more difficult when in a detached HEAD state. // "branch --list" is more difficult when in a detached HEAD state.
// Note, this implementation uses "rev-parse --symbolic-full-name" because there is a bug
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. // TODO(https://github.com/actions/checkout/issues/786): this implementation uses
// "rev-parse --symbolic-full-name" because there is a bug
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
// 2.18 is no longer supported, we can switch back to --symbolic.
const args = ['rev-parse', '--symbolic-full-name'] const args = ['rev-parse', '--symbolic-full-name']
if (remote) { if (remote) {
@ -104,21 +107,49 @@ class GitCommandManager {
args.push('--branches') args.push('--branches')
} }
const output = await this.execGit(args) const stderr: string[] = []
const errline: string[] = []
const stdout: string[] = []
const stdline: string[] = []
for (let branch of output.stdout.trim().split('\n')) { const listeners = {
branch = branch.trim() stderr: (data: Buffer) => {
if (branch) { stderr.push(data.toString())
if (branch.startsWith('refs/heads/')) { },
branch = branch.substr('refs/heads/'.length) errline: (data: Buffer) => {
} else if (branch.startsWith('refs/remotes/')) { errline.push(data.toString())
branch = branch.substr('refs/remotes/'.length) },
} stdout: (data: Buffer) => {
stdout.push(data.toString())
result.push(branch) },
stdline: (data: Buffer) => {
stdline.push(data.toString())
} }
} }
// Suppress the output in order to avoid flooding annotations with innocuous errors.
await this.execGit(args, false, true, listeners)
core.debug(`stderr callback is: ${stderr}`)
core.debug(`errline callback is: ${errline}`)
core.debug(`stdout callback is: ${stdout}`)
core.debug(`stdline callback is: ${stdline}`)
for (let branch of stdline) {
branch = branch.trim()
if (!branch) {
continue
}
if (branch.startsWith('refs/heads/')) {
branch = branch.substring('refs/heads/'.length)
} else if (branch.startsWith('refs/remotes/')) {
branch = branch.substring('refs/remotes/'.length)
}
result.push(branch)
}
return result return result
} }
@ -395,7 +426,8 @@ class GitCommandManager {
private async execGit( private async execGit(
args: string[], args: string[],
allowAllExitCodes = false, allowAllExitCodes = false,
silent = false silent = false,
customListeners = {}
): Promise<GitOutput> { ): Promise<GitOutput> {
fshelper.directoryExistsSync(this.workingDirectory, true) fshelper.directoryExistsSync(this.workingDirectory, true)
@ -409,22 +441,29 @@ class GitCommandManager {
env[key] = this.gitEnv[key] env[key] = this.gitEnv[key]
} }
const stdout: string[] = [] const defaultListener = {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
const mergedListeners = {...defaultListener, ...customListeners}
const stdout: string[] = []
const options = { const options = {
cwd: this.workingDirectory, cwd: this.workingDirectory,
env, env,
silent, silent,
ignoreReturnCode: allowAllExitCodes, ignoreReturnCode: allowAllExitCodes,
listeners: { listeners: mergedListeners
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
} }
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options) result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('') result.stdout = stdout.join('')
core.debug(result.exitCode.toString())
core.debug(result.stdout)
return result return result
} }