24 Commits

Author SHA1 Message Date
Michal Dorner
e5b96fe4da Merge pull request #68 from dorny/list-files-csv
Add list-files: csv format
2021-02-20 11:32:11 +01:00
Michal Dorner
a339507743 Skip CI when modifying only README or CHANGELOG 2021-02-20 11:30:21 +01:00
Michal Dorner
febe8330ca Update README and CHANGELOG 2021-02-20 11:28:11 +01:00
Michal Dorner
b5fa2d5c02 Add list-files: csv format 2021-02-20 11:21:30 +01:00
Michal Dorner
e2bed85912 Mention test-reporter in README 2021-02-01 21:58:39 +01:00
Michal Dorner
7c0f15b688 Update CHANGELOG 2021-01-29 20:10:42 +01:00
Michal Dorner
cbc3287af3 Merge pull request #65 from dorny/add-count-output
Add count output
2021-01-29 20:03:56 +01:00
Michal Dorner
a2730492f0 Add test for ${FILTER_NAME}_count output 2021-01-29 19:59:09 +01:00
Michal Dorner
c2766acabb Add ${FILTER_NAME}_count output 2021-01-29 19:54:44 +01:00
Michal Dorner
363576b9ea Merge pull request #61 from tun0/fix-grouping
Fix grouping of changes
2021-01-26 21:31:41 +01:00
Michal Dorner
b1a097ef7b Rebuild dist/index.js 2021-01-26 21:28:06 +01:00
Ruben Laban
2c79a825c0 Fix grouping 2021-01-26 12:22:37 +01:00
Michal Dorner
4e7fcc37b4 Fix link to CHANGELOG in README 2020-12-17 22:54:24 +01:00
Michal Dorner
c506bed1ae Update CHANGELOG.md for v2.7.0 2020-12-17 22:51:53 +01:00
Michal Dorner
9b7572ffb2 Merge pull request #59 from dorny/array-to-use-in-matrix
Add "changes" output variable to support matrix job configuration
2020-12-17 22:48:28 +01:00
Michal Dorner
9e8c9af501 Add "changes" output variable to support matrix job configuration 2020-12-17 22:33:11 +01:00
Michal Dorner
84e1697bff Merge pull request #58 from dorny/unquoted-shell-escape
Improved listing of matching files with `list-files: shell` and `list-files: escape` options
2020-12-17 22:28:11 +01:00
Michal Dorner
e4d886f503 Provide shell and escape options when formatting matching files 2020-12-17 22:13:28 +01:00
Michal Dorner
ada1eee648 Simplify shell escaping - escape chars instead of quoting whole string 2020-12-17 22:13:28 +01:00
Michal Dorner
44ac6d8e25 Merge pull request #57 from lgatto/patch-1
Fix typos in README.md
2020-12-17 10:28:30 +01:00
Laurent Gatto
3c5b7d242c Update README.md 2020-12-17 08:57:31 +01:00
Michal Dorner
eb75a1edc1 Merge pull request #53 from dorny/detect-local-changes
Support local changes
2020-11-22 21:17:54 +01:00
Michal Dorner
181b35e268 Update docs for v2.6.0 2020-11-22 21:17:07 +01:00
Michal Dorner
1934d574ce Support local changes 2020-11-22 20:59:32 +01:00
13 changed files with 411 additions and 77 deletions

View File

@@ -1,6 +1,7 @@
name: "Build"
on:
push:
paths-ignore: [ '*.md' ]
branches:
- master

View File

@@ -1,6 +1,7 @@
name: "Pull Request Verification"
on:
pull_request:
paths-ignore: [ '*.md' ]
branches:
- master
- develop
@@ -29,6 +30,9 @@ jobs:
- name: filter-test
if: steps.filter.outputs.any != 'true' || steps.filter.outputs.error == 'true'
run: exit 1
- name: changes-test
if: contains(fromJSON(steps.filter.outputs.changes), 'error') || !contains(fromJSON(steps.filter.outputs.changes), 'any')
run: exit 1
test-external:
runs-on: ubuntu-latest
@@ -71,6 +75,26 @@ jobs:
if: steps.filter.outputs.any != 'true' || steps.filter.outputs.error == 'true'
run: exit 1
test-local-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: echo "NEW FILE" > local
- run: git add local
- uses: ./
id: filter
with:
base: HEAD
filters: |
local:
- local
- name: filter-test
if: steps.filter.outputs.local != 'true'
run: exit 1
- name: count-test
if: steps.filter.outputs.local_count != 1
run: exit 1
test-change-type:
runs-on: ubuntu-latest
steps:
@@ -102,15 +126,12 @@ jobs:
- name: Print 'deleted_files'
run: echo ${{steps.filter.outputs.deleted_files}}
- name: filter-test
# only single quotes are supported in GH action literal
# single quote needs to be escaped with single quote
# '''add.txt''' resolves to string 'add.txt'
if: |
steps.filter.outputs.added != 'true'
|| steps.filter.outputs.deleted != 'true'
|| steps.filter.outputs.modified != 'true'
|| steps.filter.outputs.any != 'true'
|| steps.filter.outputs.added_files != '''add.txt'''
|| steps.filter.outputs.modified_files != '''LICENSE'''
|| steps.filter.outputs.deleted_files != '''README.md'''
|| steps.filter.outputs.added_files != 'add.txt'
|| steps.filter.outputs.modified_files != 'LICENSE'
|| steps.filter.outputs.deleted_files != 'README.md'
run: exit 1

View File

@@ -1,5 +1,19 @@
# Changelog
## v2.9.0
- [Add list-files: csv format](https://github.com/dorny/paths-filter/pull/68)
## v2.8.0
- [Add count output variable](https://github.com/dorny/paths-filter/pull/65)
- [Fix log grouping of changes](https://github.com/dorny/paths-filter/pull/61)
## v2.7.0
- [Add "changes" output variable to support matrix job configuration](https://github.com/dorny/paths-filter/pull/59)
- [Improved listing of matching files with `list-files: shell` and `list-files: escape` options](https://github.com/dorny/paths-filter/pull/58)
## v2.6.0
- [Support local changes](https://github.com/dorny/paths-filter/pull/53)
## v2.5.3
- [Fixed mapping of removed/deleted change status from github API](https://github.com/dorny/paths-filter/pull/51)
- [Fixed retrieval of all changes via Github API when there are 100+ changes](https://github.com/dorny/paths-filter/pull/50)

100
README.md
View File

@@ -5,7 +5,7 @@ based on the files modified by pull request, feature branch or in pushed commits
It saves time and resources especially in monorepo setups, where you can run slow tasks (e.g. integration tests or deployments) only for changed components.
Github workflows built-in [path filters](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onpushpull_requestpaths)
doesn't allow this because they doesn't work on a level of individual jobs or steps.
don't allow this because they don't work on a level of individual jobs or steps.
**Real world usage examples:**
- [sentry.io](https://sentry.io/) - [backend-test-py3.6.yml](https://github.com/getsentry/sentry/blob/ca0e43dc5602a9ab2e06d3f6397cc48fb5a78541/.github/workflows/backend-test-py3.6.yml#L32)
@@ -35,6 +35,10 @@ doesn't allow this because they doesn't work on a level of individual jobs or st
when `base` input parameter is same as the branch that triggered the workflow:
- Changes are detected from last commit
- Uses git commands to detect changes - repository must be already [checked out](https://github.com/actions/checkout)
- **Local changes**
- Workflow triggered by any event when `base` input parameter is set to `HEAD`
- Changes are detected against current HEAD
- Untracked files are ignored
## Example
```yaml
@@ -54,7 +58,7 @@ For more scenarios see [examples](#examples) section.
## Notes:
- Paths expressions are evaluated using [picomatch](https://github.com/micromatch/picomatch) library.
Documentation for path expression format can be found on project github page.
- Micromatch [dot](https://github.com/micromatch/picomatch#options) option is set to true.
- Picomatch [dot](https://github.com/micromatch/picomatch#options) 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.
@@ -62,19 +66,15 @@ For more scenarios see [examples](#examples) section.
# What's New
- Add `list-files: csv` format
- Configure matrix job to run for each folder with changes using `changes` output
- Improved listing of matching files with `list-files: shell` and `list-files: escape` options
- Support local changes
- Fixed retrieval of all changes via Github API when there are 100+ changes
- Paths expressions are now evaluated using [picomatch](https://github.com/micromatch/picomatch) library
- Support workflows triggered by any event
- Fixed compatibility with older (<2.23) versions of git
- 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:
- Optionally consider if file was added, modified or deleted
For more information see [CHANGELOG](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
For more information see [CHANGELOG](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
# Usage
@@ -122,9 +122,13 @@ For more information see [CHANGELOG](https://github.com/actions/checkout/blob/ma
# Enables listing of files matching the filter:
# 'none' - Disables listing of matching files (default).
# 'csv' - Coma separated list of filenames.
# If needed it uses double quotes to wrap filename with unsafe characters.
# 'json' - Matching files paths are formatted as JSON array.
# 'shell' - Matching files paths are escaped and space-delimited.
# Output is usable as command line argument list in linux shell.
# 'shell' - Space delimited list usable as command line argument list in Linux shell.
# If needed it uses single or double quotes to wrap filename with unsafe characters.
# 'escape'- Space delimited list usable as command line argument list in Linux shell.
# Backslash escapes every potentially unsafe character.
# Default: none
list-files: ''
@@ -145,7 +149,9 @@ For more information see [CHANGELOG](https://github.com/actions/checkout/blob/ma
- For each filter it sets output variable named by the filter to the text:
- `'true'` - if **any** of changed files matches any of filter rules
- `'false'` - if **none** of changed files matches any of filter rules
- For each filter it sets output variable with name `${FILTER_NAME}_count` to the count of matching files.
- If enabled, for each filter it sets output variable with name `${FILTER_NAME}_files`. It will contain list of all files matching the filter.
- `changes` - JSON array with names of all filters matching any of changed files.
# Examples
@@ -229,6 +235,41 @@ jobs:
```
</details>
<details>
<summary>Use change detection to configure matrix job</summary>
```yaml
jobs:
# JOB to run change detection
changes:
runs-on: ubuntu-latest
outputs:
# Expose matched filters as job 'packages' output variable
packages: ${{ steps.filter.outputs.changes }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
package1: src/package1
package2: src/package2
# JOB to build and test each of modified packages
build:
needs: changes
strategy:
matrix:
# Parse JSON array containing names of all filters matching any of changed files
# e.g. ['package1', 'package2'] if both package folders contains changes
package: ${{ fromJson(needs.changes.outputs.packages) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- ...
```
</details>
## Change detection workflows
<details>
@@ -304,6 +345,35 @@ jobs:
```
</details>
<details>
<summary><b>Local changes:</b> Detect staged and unstaged local changes</summary>
```yaml
on:
push:
branches: # Push to following branches will trigger the workflow
- master
- develop
- release/**
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Some action which modifies files tracked by git (e.g. code linter)
- uses: johndoe/some-action@v1
# Filter to detect which files were modified
# Changes could be for example automatically committed
- uses: dorny/paths-filter@v2
id: filter
with:
base: HEAD
filters: ... # Configure your filters
```
</details>
## Advanced options
<details>
@@ -376,7 +446,7 @@ jobs:
# Enable listing of files matching each filter.
# Paths to files will be available in `${FILTER_NAME}_files` output variable.
# Paths will be escaped and space-delimited.
# Output is usable as command line argument list in linux shell
# Output is usable as command line argument list in Linux shell
list-files: shell
# In this example changed files will be checked by linter.
@@ -415,6 +485,8 @@ jobs:
```
</details>
# See also
- [test-reporter](https://github.com/dorny/test-reporter) - Displays test results from popular testing frameworks directly in GitHub
# License

View File

@@ -0,0 +1,23 @@
import {csvEscape} from '../src/list-format/csv-escape'
describe('csvEscape() backslash escapes every character except subset of definitely safe characters', () => {
test('simple filename should not be modified', () => {
expect(csvEscape('file.txt')).toBe('file.txt')
})
test('directory separator should be preserved and not escaped', () => {
expect(csvEscape('path/to/file.txt')).toBe('path/to/file.txt')
})
test('filename with spaces should be quoted', () => {
expect(csvEscape('file with space')).toBe('"file with space"')
})
test('filename with "," should be quoted', () => {
expect(csvEscape('file, with coma')).toBe('"file, with coma"')
})
test('Double quote should be escaped by another double quote', () => {
expect(csvEscape('file " with double quote')).toBe('"file "" with double quote"')
})
})

View File

@@ -1,16 +1,57 @@
import shellEscape from '../src/shell-escape'
import {backslashEscape, shellEscape} from '../src/list-format/shell-escape'
test('simple path escaped', () => {
expect(shellEscape('file')).toBe("'file'")
describe('escape() backslash escapes every character except subset of definitely safe characters', () => {
test('simple filename should not be modified', () => {
expect(backslashEscape('file.txt')).toBe('file.txt')
})
test('directory separator should be preserved and not escaped', () => {
expect(backslashEscape('path/to/file.txt')).toBe('path/to/file.txt')
})
test('spaces should be escaped with backslash', () => {
expect(backslashEscape('file with space')).toBe('file\\ with\\ space')
})
test('quotes should be escaped with backslash', () => {
expect(backslashEscape('file\'with quote"')).toBe('file\\\'with\\ quote\\"')
})
test('$variables should be escaped', () => {
expect(backslashEscape('$var')).toBe('\\$var')
})
})
test('path with space is wrapped with single quotes', () => {
expect(shellEscape('file with space')).toBe("'file with space'")
})
describe('shellEscape() returns human readable filenames with as few escaping applied as possible', () => {
test('simple filename should not be modified', () => {
expect(shellEscape('file.txt')).toBe('file.txt')
})
test('path with quote is divided into quoted segments and escaped quote', () => {
expect(shellEscape("file'with quote")).toBe("'file'\\''with quote'")
})
test('path with leading quote does not have double quotes at beginning', () => {
expect(shellEscape("'file-leading-quote")).toBe("\\''file-leading-quote'")
test('directory separator should be preserved and not escaped', () => {
expect(shellEscape('path/to/file.txt')).toBe('path/to/file.txt')
})
test('filename with spaces should be quoted', () => {
expect(shellEscape('file with space')).toBe("'file with space'")
})
test('filename with spaces should be quoted', () => {
expect(shellEscape('file with space')).toBe("'file with space'")
})
test('filename with $ should be quoted', () => {
expect(shellEscape('$var')).toBe("'$var'")
})
test('filename with " should be quoted', () => {
expect(shellEscape('file"name')).toBe("'file\"name'")
})
test('filename with single quote should be wrapped in double quotes', () => {
expect(shellEscape("file'with quote")).toBe('"file\'with quote"')
})
test('filename with single quote and special characters is split and quoted/escaped as needed', () => {
expect(shellEscape("file'with $quote")).toBe("file\\''with $quote'")
})
})

View File

@@ -22,8 +22,13 @@ inputs:
description: |
Enables listing of files matching the filter:
'none' - Disables listing of matching files (default).
'json' - Matching files paths are serialized as JSON array.
'shell' - Matching files paths are escaped and space-delimited. Output is usable as command line argument list in linux shell.
'csv' - Coma separated list of filenames.
If needed it uses double quotes to wrap filename with unsafe characters.
'json' - Serialized as JSON array.
'shell' - Space delimited list usable as command line argument list in linux shell.
If needed it uses single or double quotes to wrap filename with unsafe characters.
'escape'- Space delimited list usable as command line argument list in linux shell.
Backslash escapes every potentially unsafe character.
required: true
default: none
initial-fetch-depth:
@@ -34,6 +39,9 @@ inputs:
This option takes effect only when changes are detected using git against different base branch.
required: false
default: '10'
outputs:
changes:
description: JSON array with names of all filters matching any of changed files
runs:
using: 'node12'
main: 'dist/index.js'

138
dist/index.js vendored
View File

@@ -3811,11 +3811,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isGitSha = exports.getShortName = exports.getCurrentRef = exports.listAllFilesAsAdded = exports.parseGitDiffOutput = exports.getChangesSinceMergeBase = exports.getChanges = exports.getChangesInLastCommit = exports.NULL_SHA = void 0;
exports.isGitSha = exports.getShortName = exports.getCurrentRef = exports.listAllFilesAsAdded = exports.parseGitDiffOutput = exports.getChangesSinceMergeBase = exports.getChangesOnHead = exports.getChanges = exports.getChangesInLastCommit = exports.HEAD = 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';
exports.HEAD = 'HEAD';
async function getChangesInLastCommit() {
core.startGroup(`Change detection in last commit`);
let output = '';
@@ -3850,6 +3851,20 @@ async function getChanges(ref) {
return parseGitDiffOutput(output);
}
exports.getChanges = getChanges;
async function getChangesOnHead() {
// Get current changes - both staged and unstaged
core.startGroup(`Change detection on HEAD`);
let output = '';
try {
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', 'HEAD'])).stdout;
}
finally {
fixStdOutNullTermination();
core.endGroup();
}
return parseGitDiffOutput(output);
}
exports.getChangesOnHead = getChangesOnHead;
async function getChangesSinceMergeBase(ref, initialFetchDepth) {
if (!(await hasCommit(ref))) {
// Fetch and add base branch
@@ -4626,9 +4641,6 @@ 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 });
const fs = __importStar(__webpack_require__(747));
const core = __importStar(__webpack_require__(470));
@@ -4636,7 +4648,8 @@ const github = __importStar(__webpack_require__(469));
const filter_1 = __webpack_require__(235);
const file_1 = __webpack_require__(258);
const git = __importStar(__webpack_require__(136));
const shell_escape_1 = __importDefault(__webpack_require__(751));
const shell_escape_1 = __webpack_require__(206);
const csv_escape_1 = __webpack_require__(410);
async function run() {
try {
const workingDirectory = core.getInput('working-directory', { required: false });
@@ -4675,6 +4688,11 @@ function getConfigFileContent(configPath) {
return fs.readFileSync(configPath, { encoding: 'utf8' });
}
async function getChangedFiles(token, base, initialFetchDepth) {
// if base is 'HEAD' only local uncommitted changes will be detected
// This is the simplest case as we don't need to fetch more commits or evaluate current/before refs
if (base === git.HEAD) {
return await git.getChangesOnHead();
}
if (github.context.eventName === 'pull_request' || github.context.eventName === 'pull_request_target') {
const pr = github.context.payload.pull_request;
if (token) {
@@ -4692,7 +4710,7 @@ async function getChangedFilesFromGit(base, initialFetchDepth) {
const defaultRef = (_a = github.context.payload.repository) === null || _a === void 0 ? void 0 : _a.default_branch;
const beforeSha = github.context.eventName === 'push' ? github.context.payload.before : null;
const pushRef = git.getShortName(github.context.ref) ||
(core.warning(`'ref' field is missing in PUSH event payload - using current branch, tag or commit SHA`),
(core.warning(`'ref' field is missing in event payload - using current branch, tag or commit SHA`),
await git.getCurrentRef());
const baseRef = git.getShortName(base) || defaultRef;
if (!baseRef) {
@@ -4700,11 +4718,11 @@ async function getChangedFilesFromGit(base, initialFetchDepth) {
}
const isBaseRefSha = git.isGitSha(baseRef);
const isBaseSameAsPush = baseRef === pushRef;
// If base is commit SHA will do comparison against the referenced commit
// Or If base references same branch it was pushed to, we will do comparison against the previously pushed commit
// If base is commit SHA we will do comparison against the referenced commit
// Or if base references same branch it was pushed to, we will do comparison against the previously pushed commit
if (isBaseRefSha || isBaseSameAsPush) {
if (!isBaseRefSha && !beforeSha) {
core.warning(`'before' field is missing in PUSH event payload - changes will be detected from last commit`);
core.warning(`'before' field is missing in event payload - changes will be detected from last commit`);
return await git.getChangesInLastCommit();
}
const baseSha = isBaseRefSha ? baseRef : beforeSha;
@@ -4774,10 +4792,12 @@ async function getChangedFilesFromApi(token, pullRequest) {
}
function exportResults(results, format) {
core.info('Results:');
const changes = [];
for (const [key, files] of Object.entries(results)) {
const value = files.length > 0;
core.startGroup(`Filter ${key} = ${value}`);
if (files.length > 0) {
changes.push(key);
core.info('Matching files:');
for (const file of files) {
core.info(`${file.filename} [${file.status}]`);
@@ -4787,26 +4807,39 @@ function exportResults(results, format) {
core.info('Matching files: none');
}
core.setOutput(key, value);
core.setOutput(`${key}_count`, files.length);
if (format !== 'none') {
const filesValue = serializeExport(files, format);
core.setOutput(`${key}_files`, filesValue);
}
core.endGroup();
}
if (results['changes'] === undefined) {
const changesJson = JSON.stringify(changes);
core.info(`Changes output set to ${changesJson}`);
core.setOutput('changes', changesJson);
}
else {
core.info('Cannot set changes output variable - name already used by filter output');
}
core.endGroup();
}
function serializeExport(files, format) {
const fileNames = files.map(file => file.filename);
switch (format) {
case 'csv':
return fileNames.map(csv_escape_1.csvEscape).join(',');
case 'json':
return JSON.stringify(fileNames);
case 'escape':
return fileNames.map(shell_escape_1.backslashEscape).join(' ');
case 'shell':
return fileNames.map(shell_escape_1.default).join(' ');
return fileNames.map(shell_escape_1.shellEscape).join(' ');
default:
return '';
}
}
function isExportFormat(value) {
return value === 'none' || value === 'shell' || value === 'json';
return ['none', 'csv', 'shell', 'json', 'escape'].includes(value);
}
run();
@@ -4998,6 +5031,43 @@ module.exports = {
};
/***/ }),
/***/ 206:
/***/ (function(__unusedmodule, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.shellEscape = exports.backslashEscape = void 0;
// Backslash escape every character except small subset of definitely safe characters
function backslashEscape(value) {
return value.replace(/([^a-zA-Z0-9,._+:@%/-])/gm, '\\$1');
}
exports.backslashEscape = backslashEscape;
// Returns filename escaped for usage as shell argument.
// Applies "human readable" approach with as few escaping applied as possible
function shellEscape(value) {
if (value === '')
return value;
// Only safe characters
if (/^[a-zA-Z0-9,._+:@%/-]+$/m.test(value)) {
return value;
}
if (value.includes("'")) {
// Only safe characters, single quotes and white-spaces
if (/^[a-zA-Z0-9,._+:@%/'\s-]+$/m.test(value)) {
return `"${value}"`;
}
// Split by single quote and apply escaping recursively
return value.split("'").map(shellEscape).join("\\'");
}
// Contains some unsafe characters but no single quote
return `'${value}'`;
}
exports.shellEscape = shellEscape;
/***/ }),
/***/ 211:
@@ -8783,6 +8853,33 @@ function Octokit(plugins, options) {
}
/***/ }),
/***/ 410:
/***/ (function(__unusedmodule, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.csvEscape = void 0;
// Returns filename escaped for CSV
// Wraps file name into "..." only when it contains some potentially unsafe character
function csvEscape(value) {
if (value === '')
return value;
// Only safe characters
if (/^[a-zA-Z0-9._+:@%/-]+$/m.test(value)) {
return value;
}
// https://tools.ietf.org/html/rfc4180
// If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote
return `"${value.replace(/"/g, '""')}"`;
}
exports.csvEscape = csvEscape;
/***/ }),
/***/ 413:
@@ -15195,23 +15292,6 @@ function sync (path, options) {
module.exports = require("fs");
/***/ }),
/***/ 751:
/***/ (function(__unusedmodule, exports) {
"use strict";
// Credits to https://github.com/xxorax/node-shell-escape
Object.defineProperty(exports, "__esModule", { value: true });
function shellEscape(value) {
return `'${value.replace(/'/g, "'\\''")}'`
.replace(/^(?:'')+/g, '') // unduplicate single-quote at the beginning
.replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
}
exports.default = shellEscape;
/***/ }),
/***/ 753:

View File

@@ -3,6 +3,7 @@ import * as core from '@actions/core'
import {File, ChangeStatus} from './file'
export const NULL_SHA = '0000000000000000000000000000000000000000'
export const HEAD = 'HEAD'
export async function getChangesInLastCommit(): Promise<File[]> {
core.startGroup(`Change detection in last commit`)
@@ -39,6 +40,20 @@ export async function getChanges(ref: string): Promise<File[]> {
return parseGitDiffOutput(output)
}
export async function getChangesOnHead(): Promise<File[]> {
// Get current changes - both staged and unstaged
core.startGroup(`Change detection on HEAD`)
let output = ''
try {
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', 'HEAD'])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}
return parseGitDiffOutput(output)
}
export async function getChangesSinceMergeBase(ref: string, initialFetchDepth: number): Promise<File[]> {
if (!(await hasCommit(ref))) {
// Fetch and add base branch

View File

@@ -0,0 +1,16 @@
// Returns filename escaped for CSV
// Wraps file name into "..." only when it contains some potentially unsafe character
export function csvEscape(value: string): string {
if (value === '') return value
// Only safe characters
if (/^[a-zA-Z0-9._+:@%/-]+$/m.test(value)) {
return value
}
// https://tools.ietf.org/html/rfc4180
// If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote
return `"${value.replace(/"/g, '""')}"`
}

View File

@@ -0,0 +1,28 @@
// Backslash escape every character except small subset of definitely safe characters
export function backslashEscape(value: string): string {
return value.replace(/([^a-zA-Z0-9,._+:@%/-])/gm, '\\$1')
}
// Returns filename escaped for usage as shell argument.
// Applies "human readable" approach with as few escaping applied as possible
export function shellEscape(value: string): string {
if (value === '') return value
// Only safe characters
if (/^[a-zA-Z0-9,._+:@%/-]+$/m.test(value)) {
return value
}
if (value.includes("'")) {
// Only safe characters, single quotes and white-spaces
if (/^[a-zA-Z0-9,._+:@%/'\s-]+$/m.test(value)) {
return `"${value}"`
}
// Split by single quote and apply escaping recursively
return value.split("'").map(shellEscape).join("\\'")
}
// Contains some unsafe characters but no single quote
return `'${value}'`
}

View File

@@ -6,9 +6,10 @@ import {Webhooks} from '@octokit/webhooks'
import {Filter, FilterResults} from './filter'
import {File, ChangeStatus} from './file'
import * as git from './git'
import shellEscape from './shell-escape'
import {backslashEscape, shellEscape} from './list-format/shell-escape'
import {csvEscape} from './list-format/csv-escape'
type ExportFormat = 'none' | 'json' | 'shell'
type ExportFormat = 'none' | 'csv' | 'json' | 'shell' | 'escape'
async function run(): Promise<void> {
try {
@@ -55,6 +56,12 @@ function getConfigFileContent(configPath: string): string {
}
async function getChangedFiles(token: string, base: string, initialFetchDepth: number): Promise<File[]> {
// if base is 'HEAD' only local uncommitted changes will be detected
// This is the simplest case as we don't need to fetch more commits or evaluate current/before refs
if (base === git.HEAD) {
return await git.getChangesOnHead()
}
if (github.context.eventName === 'pull_request' || github.context.eventName === 'pull_request_target') {
const pr = github.context.payload.pull_request as Webhooks.WebhookPayloadPullRequestPullRequest
if (token) {
@@ -75,7 +82,7 @@ async function getChangedFilesFromGit(base: string, initialFetchDepth: number):
const pushRef =
git.getShortName(github.context.ref) ||
(core.warning(`'ref' field is missing in PUSH event payload - using current branch, tag or commit SHA`),
(core.warning(`'ref' field is missing in event payload - using current branch, tag or commit SHA`),
await git.getCurrentRef())
const baseRef = git.getShortName(base) || defaultRef
@@ -88,11 +95,11 @@ async function getChangedFilesFromGit(base: string, initialFetchDepth: number):
const isBaseRefSha = git.isGitSha(baseRef)
const isBaseSameAsPush = baseRef === pushRef
// If base is commit SHA will do comparison against the referenced commit
// Or If base references same branch it was pushed to, we will do comparison against the previously pushed commit
// If base is commit SHA we will do comparison against the referenced commit
// Or if base references same branch it was pushed to, we will do comparison against the previously pushed commit
if (isBaseRefSha || isBaseSameAsPush) {
if (!isBaseRefSha && !beforeSha) {
core.warning(`'before' field is missing in PUSH event payload - changes will be detected from last commit`)
core.warning(`'before' field is missing in event payload - changes will be detected from last commit`)
return await git.getChangesInLastCommit()
}
@@ -169,10 +176,12 @@ async function getChangedFilesFromApi(
function exportResults(results: FilterResults, format: ExportFormat): void {
core.info('Results:')
const changes = []
for (const [key, files] of Object.entries(results)) {
const value = files.length > 0
core.startGroup(`Filter ${key} = ${value}`)
if (files.length > 0) {
changes.push(key)
core.info('Matching files:')
for (const file of files) {
core.info(`${file.filename} [${file.status}]`)
@@ -182,19 +191,32 @@ function exportResults(results: FilterResults, format: ExportFormat): void {
}
core.setOutput(key, value)
core.setOutput(`${key}_count`, files.length)
if (format !== 'none') {
const filesValue = serializeExport(files, format)
core.setOutput(`${key}_files`, filesValue)
}
core.endGroup()
}
if (results['changes'] === undefined) {
const changesJson = JSON.stringify(changes)
core.info(`Changes output set to ${changesJson}`)
core.setOutput('changes', changesJson)
} else {
core.info('Cannot set changes output variable - name already used by filter output')
}
core.endGroup()
}
function serializeExport(files: File[], format: ExportFormat): string {
const fileNames = files.map(file => file.filename)
switch (format) {
case 'csv':
return fileNames.map(csvEscape).join(',')
case 'json':
return JSON.stringify(fileNames)
case 'escape':
return fileNames.map(backslashEscape).join(' ')
case 'shell':
return fileNames.map(shellEscape).join(' ')
default:
@@ -203,7 +225,7 @@ function serializeExport(files: File[], format: ExportFormat): string {
}
function isExportFormat(value: string): value is ExportFormat {
return value === 'none' || value === 'shell' || value === 'json'
return ['none', 'csv', 'shell', 'json', 'escape'].includes(value)
}
run()

View File

@@ -1,7 +0,0 @@
// Credits to https://github.com/xxorax/node-shell-escape
export default function shellEscape(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
.replace(/^(?:'')+/g, '') // unduplicate single-quote at the beginning
.replace(/\\'''/g, "\\'") // remove non-escaped single-quote if there are enclosed between 2 escaped
}