Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.4.0 - support local execution with act #40

Merged
merged 14 commits into from
Sep 29, 2020
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## v2.4.0
- [Support pushes of tags or when tag is used as base](https://github.com/dorny/paths-filter/pull/40)
- [Use git log to detect changes from PRs merge commit if token is not available](https://github.com/dorny/paths-filter/pull/40)
- [Support local execution with act](https://github.com/dorny/paths-filter/pull/40)
- [Improved processing of repository initial push](https://github.com/dorny/paths-filter/pull/40)
- [Improved processing of first push of new branch](https://github.com/dorny/paths-filter/pull/40)


## v2.3.0
- [Improved documentation](https://github.com/dorny/paths-filter/pull/37)
- [Change detection using git "three dot" diff](https://github.com/dorny/paths-filter/pull/35)
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ doesn't allow this because they doesn't work on a level of individual jobs or st
- Minimatch [dot](https://www.npmjs.com/package/minimatch#dot) option is set to true.
Globbing will match also paths where file or folder name starts with a dot.
- It's recommended to quote your path expressions with `'` or `"`. Otherwise you will get an error if it starts with `*`.
- Local execution with [act](https://github.com/nektos/act) works only with alternative runner image. Default runner doesn't have `git` binary.
- Use: `act -P ubuntu-latest=nektos/act-environments-ubuntu:18.04`


# What's New

- Support for tag pushes and tags as a base reference
- Fixes for various edge cases when event payload is incomplete
- Supports local execution with [act](https://github.com/nektos/act)
- Fixed behavior of feature branch workflow:
- Detects only changes introduced by feature branch. Later modifications on base branch are ignored.
- Filter by type of file change:
Expand Down Expand Up @@ -68,7 +72,7 @@ For more information see [CHANGELOG](https://github.com/actions/checkout/blob/ma
# Filters syntax is documented by example - see examples section.
filters: ''

# Branch against which the changes will be detected.
# Branch or tag against which the changes will be detected.
# If it references same branch it was pushed to,
# changes are detected against the most recent commit before the push.
# Otherwise it uses git merge-base to find best common ancestor between
Expand Down
20 changes: 7 additions & 13 deletions __tests__/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,13 @@ describe('parsing output of the git diff command', () => {
})

describe('git utility function tests (those not invoking git)', () => {
test('Detects if ref references a tag', () => {
expect(git.isTagRef('refs/tags/v1.0')).toBeTruthy()
expect(git.isTagRef('refs/heads/master')).toBeFalsy()
expect(git.isTagRef('master')).toBeFalsy()
})
test('Trims "refs/" from ref', () => {
expect(git.trimRefs('refs/heads/master')).toBe('heads/master')
expect(git.trimRefs('heads/master')).toBe('heads/master')
expect(git.trimRefs('master')).toBe('master')
})
test('Trims "refs/" and "heads/" from ref', () => {
expect(git.trimRefsHeads('refs/heads/master')).toBe('master')
expect(git.trimRefsHeads('heads/master')).toBe('master')
expect(git.trimRefsHeads('master')).toBe('master')
expect(git.getShortName('refs/heads/master')).toBe('master')
expect(git.getShortName('heads/master')).toBe('heads/master')
expect(git.getShortName('master')).toBe('master')

expect(git.getShortName('refs/tags/v1')).toBe('v1')
expect(git.getShortName('tags/v1')).toBe('tags/v1')
expect(git.getShortName('v1')).toBe('v1')
})
})
208 changes: 141 additions & 67 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3807,51 +3807,73 @@ var __importStar = (this && this.__importStar) || function (mod) {
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.trimRefsHeads = exports.trimRefs = exports.isTagRef = exports.listAllFilesAsAdded = exports.parseGitDiffOutput = exports.getChangesSinceRef = exports.getChangesAgainstSha = exports.NULL_SHA = void 0;
const exec_1 = __webpack_require__(986);
exports.getShortName = exports.getCurrentRef = exports.listAllFilesAsAdded = exports.parseGitDiffOutput = exports.getChangesSinceMergeBase = exports.getChanges = exports.getChangesInLastCommit = exports.NULL_SHA = void 0;
const exec_1 = __importDefault(__webpack_require__(807));
const core = __importStar(__webpack_require__(470));
const file_1 = __webpack_require__(258);
exports.NULL_SHA = '0000000000000000000000000000000000000000';
async function getChangesAgainstSha(sha) {
// Fetch single commit
core.startGroup(`Fetching ${sha} from origin`);
await exec_1.exec('git', ['fetch', '--depth=1', '--no-tags', 'origin', sha]);
core.endGroup();
// Get differences between sha and HEAD
core.startGroup(`Change detection ${sha}..HEAD`);
async function getChangesInLastCommit() {
core.startGroup(`Change detection in last commit`);
let output = '';
try {
output = (await exec_1.default('git', ['log', '--format=', '--no-renames', '--name-status', '-z', '-n', '1'])).stdout;
}
finally {
fixStdOutNullTermination();
core.endGroup();
}
return parseGitDiffOutput(output);
}
exports.getChangesInLastCommit = getChangesInLastCommit;
async function getChanges(ref) {
if (!(await hasCommit(ref))) {
// Fetch single commit
core.startGroup(`Fetching ${ref} from origin`);
await exec_1.default('git', ['fetch', '--depth=1', '--no-tags', '--no-auto-gc', 'origin', ref]);
core.endGroup();
}
// Get differences between ref and HEAD
core.startGroup(`Change detection ${ref}..HEAD`);
let output = '';
try {
// Two dots '..' change detection - directly compares two versions
await exec_1.exec('git', ['diff', '--no-renames', '--name-status', '-z', `${sha}..HEAD`], {
listeners: {
stdout: (data) => (output += data.toString())
}
});
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}..HEAD`])).stdout;
}
finally {
fixStdOutNullTermination();
core.endGroup();
}
return parseGitDiffOutput(output);
}
exports.getChangesAgainstSha = getChangesAgainstSha;
async function getChangesSinceRef(ref, initialFetchDepth) {
// Fetch and add base branch
core.startGroup(`Fetching ${ref} from origin until merge-base is found`);
await exec_1.exec('git', ['fetch', `--depth=${initialFetchDepth}`, '--no-tags', 'origin', `${ref}:${ref}`]);
exports.getChanges = getChanges;
async function getChangesSinceMergeBase(ref, initialFetchDepth) {
if (!(await hasCommit(ref))) {
// Fetch and add base branch
core.startGroup(`Fetching ${ref}`);
try {
await exec_1.default('git', ['fetch', `--depth=${initialFetchDepth}`, '--no-tags', 'origin', `${ref}:${ref}`]);
}
finally {
core.endGroup();
}
}
async function hasMergeBase() {
return (await exec_1.exec('git', ['merge-base', ref, 'HEAD'], { ignoreReturnCode: true })) === 0;
return (await exec_1.default('git', ['merge-base', ref, 'HEAD'], { ignoreReturnCode: true })).code === 0;
}
async function countCommits() {
return (await getNumberOfCommits('HEAD')) + (await getNumberOfCommits(ref));
}
core.startGroup(`Searching for merge-base with ${ref}`);
// Fetch more commits until merge-base is found
if (!(await hasMergeBase())) {
let deepen = initialFetchDepth;
let lastCommitsCount = await countCommits();
do {
await exec_1.exec('git', ['fetch', `--deepen=${deepen}`, '--no-tags', '--no-auto-gc', '-q']);
await exec_1.default('git', ['fetch', `--deepen=${deepen}`, '--no-tags', '--no-auto-gc']);
const count = await countCommits();
if (count <= lastCommitsCount) {
core.info('No merge base found - all files will be listed as added');
Expand All @@ -3868,19 +3890,15 @@ async function getChangesSinceRef(ref, initialFetchDepth) {
let output = '';
try {
// Three dots '...' change detection - finds merge-base and compares against it
await exec_1.exec('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}...HEAD`], {
listeners: {
stdout: (data) => (output += data.toString())
}
});
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}...HEAD`])).stdout;
}
finally {
fixStdOutNullTermination();
core.endGroup();
}
return parseGitDiffOutput(output);
}
exports.getChangesSinceRef = getChangesSinceRef;
exports.getChangesSinceMergeBase = getChangesSinceMergeBase;
function parseGitDiffOutput(output) {
const tokens = output.split('\u0000').filter(s => s.length > 0);
const files = [];
Expand All @@ -3897,11 +3915,7 @@ async function listAllFilesAsAdded() {
core.startGroup('Listing all files tracked by git');
let output = '';
try {
await exec_1.exec('git', ['ls-files', '-z'], {
listeners: {
stdout: (data) => (output += data.toString())
}
});
output = (await exec_1.default('git', ['ls-files', '-z'])).stdout;
}
finally {
fixStdOutNullTermination();
Expand All @@ -3916,32 +3930,50 @@ async function listAllFilesAsAdded() {
}));
}
exports.listAllFilesAsAdded = listAllFilesAsAdded;
function isTagRef(ref) {
return ref.startsWith('refs/tags/');
}
exports.isTagRef = isTagRef;
function trimRefs(ref) {
return trimStart(ref, 'refs/');
async function getCurrentRef() {
core.startGroup(`Determining current ref`);
try {
const branch = (await exec_1.default('git', ['branch', '--show-current'])).stdout.trim();
if (branch) {
return branch;
}
const describe = await exec_1.default('git', ['describe', '--tags', '--exact-match'], { ignoreReturnCode: true });
if (describe.code === 0) {
return describe.stdout.trim();
}
return (await exec_1.default('git', ['rev-parse', 'HEAD'])).stdout.trim();
}
finally {
core.endGroup();
}
}
exports.trimRefs = trimRefs;
function trimRefsHeads(ref) {
const trimRef = trimStart(ref, 'refs/');
return trimStart(trimRef, 'heads/');
exports.getCurrentRef = getCurrentRef;
function getShortName(ref) {
if (!ref)
return '';
const heads = 'refs/heads/';
const tags = 'refs/tags/';
if (ref.startsWith(heads))
return ref.slice(heads.length);
if (ref.startsWith(tags))
return ref.slice(tags.length);
return ref;
}
exports.getShortName = getShortName;
async function hasCommit(ref) {
core.startGroup(`Checking if commit for ${ref} is locally available`);
try {
return (await exec_1.default('git', ['cat-file', '-e', `${ref}^{commit}`], { ignoreReturnCode: true })).code === 0;
}
finally {
core.endGroup();
}
}
exports.trimRefsHeads = trimRefsHeads;
async function getNumberOfCommits(ref) {
let output = '';
await exec_1.exec('git', ['rev-list', `--count`, ref], {
listeners: {
stdout: (data) => (output += data.toString())
}
});
const output = (await exec_1.default('git', ['rev-list', `--count`, ref])).stdout;
const count = parseInt(output);
return isNaN(count) ? 0 : count;
}
function trimStart(ref, start) {
return ref.startsWith(start) ? ref.substr(start.length) : ref;
}
function fixStdOutNullTermination() {
// Previous command uses NULL as delimiters and output is printed to stdout.
// We have to make sure next thing written to stdout will start on new line.
Expand Down Expand Up @@ -4641,9 +4673,11 @@ function getConfigFileContent(configPath) {
async function getChangedFiles(token, base, initialFetchDepth) {
if (github.context.eventName === 'pull_request' || github.context.eventName === 'pull_request_target') {
const pr = github.context.payload.pull_request;
return token
? await getChangedFilesFromApi(token, pr)
: await git.getChangesSinceRef(pr.base.ref, initialFetchDepth);
if (token) {
return await getChangedFilesFromApi(token, pr);
}
core.info('Github token is not available - changes will be detected from PRs merge commit');
return await git.getChangesInLastCommit();
}
else if (github.context.eventName === 'push') {
return getChangedFilesFromPush(base, initialFetchDepth);
Expand All @@ -4653,26 +4687,41 @@ async function getChangedFiles(token, base, initialFetchDepth) {
}
}
async function getChangedFilesFromPush(base, initialFetchDepth) {
var _a;
const push = github.context.payload;
// No change detection for pushed tags
if (git.isTagRef(push.ref)) {
core.info('Workflow is triggered by pushing of tag - all files will be listed as added');
return await git.listAllFilesAsAdded();
}
const baseRef = git.trimRefsHeads(base || push.repository.default_branch);
const pushRef = git.trimRefsHeads(push.ref);
// If base references same branch it was pushed to, we will do comparison against the previously pushed commit.
const defaultRef = (_a = push.repository) === null || _a === void 0 ? void 0 : _a.default_branch;
const pushRef = git.getShortName(push.ref) ||
(core.warning(`'ref' field is missing in PUSH event payload - using current branch, tag or commit SHA`),
await git.getCurrentRef());
const baseRef = git.getShortName(base) || defaultRef;
if (!baseRef) {
throw new Error("This action requires 'base' input to be configured or 'repository.default_branch' to be set in the event payload");
}
// If base references same branch it was pushed to,
// we will do comparison against the previously pushed commit
if (baseRef === pushRef) {
if (!push.before) {
core.warning(`'before' field is missing in PUSH event payload - changes will be detected from last commit`);
return await git.getChangesInLastCommit();
}
// If there is no previously pushed commit,
// we will do comparison against the default branch or return all as added
if (push.before === git.NULL_SHA) {
core.info('First push of a branch detected - all files will be listed as added');
return await git.listAllFilesAsAdded();
if (defaultRef && baseRef !== defaultRef) {
core.info(`First push of a branch detected - changes will be detected against the default branch ${defaultRef}`);
return await git.getChangesSinceMergeBase(defaultRef, initialFetchDepth);
}
else {
core.info('Initial push detected - all files will be listed as added');
return await git.listAllFilesAsAdded();
}
}
core.info(`Changes will be detected against the last previously pushed commit on same branch (${pushRef})`);
return await git.getChangesAgainstSha(push.before);
return await git.getChanges(push.before);
}
// Changes introduced by current branch against the base branch
core.info(`Changes will be detected against the branch ${baseRef}`);
return await git.getChangesSinceRef(baseRef, initialFetchDepth);
return await git.getChangesSinceMergeBase(baseRef, initialFetchDepth);
}
// Uses github REST api to get list of files changed in PR
async function getChangedFilesFromApi(token, pullRequest) {
Expand Down Expand Up @@ -15612,6 +15661,31 @@ exports.getUserAgent = getUserAgent;
//# sourceMappingURL=index.js.map


/***/ }),

/***/ 807:
/***/ (function(__unusedmodule, exports, __webpack_require__) {

"use strict";

Object.defineProperty(exports, "__esModule", { value: true });
const exec_1 = __webpack_require__(986);
// Wraps original exec() function
// Returns exit code and whole stdout/stderr
async function exec(commandLine, args, options) {
options = options || {};
let stdout = '';
let stderr = '';
options.listeners = {
stdout: (data) => (stdout += data.toString()),
stderr: (data) => (stderr += data.toString())
};
const code = await exec_1.exec(commandLine, args, options);
return { code, stdout, stderr };
}
exports.default = exec;


/***/ }),

/***/ 809:
Expand Down
21 changes: 21 additions & 0 deletions src/exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {exec as execImpl, ExecOptions} from '@actions/exec'

// Wraps original exec() function
// Returns exit code and whole stdout/stderr
export default async function exec(commandLine: string, args?: string[], options?: ExecOptions): Promise<ExecResult> {
options = options || {}
let stdout = ''
let stderr = ''
options.listeners = {
stdout: (data: Buffer) => (stdout += data.toString()),
stderr: (data: Buffer) => (stderr += data.toString())
}
const code = await execImpl(commandLine, args, options)
return {code, stdout, stderr}
}

export interface ExecResult {
code: number
stdout: string
stderr: string
}
Loading