mirror of
https://github.com/dorny/paths-filter.git
synced 2025-06-08 00:59:04 +00:00
Adds ignore path functionality
This commit is contained in:
parent
b0bc141e31
commit
d748e34e85
10 changed files with 9748 additions and 32251 deletions
18
.github/filters.yml
vendored
18
.github/filters.yml
vendored
|
@ -1,4 +1,20 @@
|
|||
error:
|
||||
- not_existing_path/**/*
|
||||
any:
|
||||
- "**/*"
|
||||
- "**/*"
|
||||
anyignore:
|
||||
-
|
||||
paths: "**/*"
|
||||
paths_ignore:
|
||||
- "**/*.md"
|
||||
anyignorenull:
|
||||
-
|
||||
paths: "**/*"
|
||||
paths_ignore:
|
||||
- "**local.ts"
|
||||
- "**local.md"
|
||||
anyignoreall:
|
||||
-
|
||||
paths: "**/*"
|
||||
paths_ignore:
|
||||
- "**/*.no"
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
npm install
|
||||
npm ci
|
||||
npm run all
|
||||
|
||||
self-test:
|
||||
|
|
35
.github/workflows/pull-request-verification.yml
vendored
35
.github/workflows/pull-request-verification.yml
vendored
|
@ -213,4 +213,37 @@ jobs:
|
|||
run: exit 1
|
||||
- name: changes-test
|
||||
if: contains(fromJSON(steps.filter.outputs.changes), 'error') || !contains(fromJSON(steps.filter.outputs.changes), 'any')
|
||||
run: exit 1
|
||||
run: exit 1
|
||||
- name: print context
|
||||
run: |
|
||||
echo "${{ tojson(steps.filter) }}"
|
||||
|
||||
test-ignore-changes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
echo "NEW FILE" > local.ts
|
||||
echo "IGNORE FILE" > local.md
|
||||
- run: git add local.ts local.md
|
||||
- uses: ./
|
||||
id: filter
|
||||
with:
|
||||
base: HEAD
|
||||
filters: '.github/filters.yml'
|
||||
- name: print context
|
||||
run: |
|
||||
echo "${{ tojson(steps.filter) }}"
|
||||
- name: filter-test
|
||||
if: steps.filter.outputs.any != 'true'
|
||||
run: exit 1
|
||||
- name: ignore-test
|
||||
if: steps.filter.outputs.anyignore_count != 1
|
||||
run: exit 1
|
||||
- name: ignore_testnull
|
||||
if: steps.filter.outputs.anyignorenull == true
|
||||
run: exit 1
|
||||
- name: ignore_testall
|
||||
if: steps.filter.outputs.anyignoreall_count != 2
|
||||
run: |
|
||||
exit 1
|
||||
|
|
11
README.md
11
README.md
|
@ -95,6 +95,8 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
|
|||
# indicate if there's a changed file matching any of the rules.
|
||||
# Optionally, there can be a second output variable
|
||||
# set to list of all files matching the filter.
|
||||
# Optionally, filters can be an object including path, meaning the rule, and paths_ignore,
|
||||
# being a an array of rules to ignore.
|
||||
# Filters can be provided inline as a string (containing valid YAML document),
|
||||
# or as a relative path to a file (e.g.: .github/filters.yaml).
|
||||
# Filters syntax is documented by example - see examples section.
|
||||
|
@ -151,7 +153,7 @@ For more information, see [CHANGELOG](https://github.com/dorny/paths-filter/blob
|
|||
# Default: ${{ github.token }}
|
||||
token: ''
|
||||
|
||||
# Optionally provide a list of files you ahve generated elsewhere.
|
||||
# Optionally provide a list of files you have generated elsewhere.
|
||||
# Performs the glob matching against these files instead. Negates
|
||||
# all other inputs less filters & list-files.
|
||||
customfiles: ''
|
||||
|
@ -207,7 +209,7 @@ jobs:
|
|||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Execute <b>job</b> in a workflow only if some file in a subfolder is changed</summary>
|
||||
<summary>Execute <b>job</b> in a workflow only if some file in a subfolder is changed. Note excluding frontend's readme.</summary>
|
||||
|
||||
```yml
|
||||
jobs:
|
||||
|
@ -227,7 +229,10 @@ jobs:
|
|||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
-
|
||||
paths: ['frontend/**']
|
||||
paths_ignore:
|
||||
- 'frontend/README.md'
|
||||
|
||||
# JOB to build and test backend code
|
||||
backend:
|
||||
|
|
|
@ -179,6 +179,19 @@ describe('matching specific change status', () => {
|
|||
const match = filter.match(files)
|
||||
expect(match.src).toEqual(files)
|
||||
})
|
||||
test('matches path based on rules including using ignore', () => {
|
||||
const yaml = `
|
||||
ignore:
|
||||
-
|
||||
paths: ["config/**"]
|
||||
paths_ignore:
|
||||
- "**.md"
|
||||
`
|
||||
const filter = new Filter(yaml)
|
||||
const files = modified(['config/settings.yml', 'config/settings.md', 'nothing/todo/with.this'])
|
||||
const match = filter.match(files)
|
||||
expect(match.ignore).toEqual(modified(['config/settings.yml']))
|
||||
})
|
||||
})
|
||||
|
||||
function modified(paths: string[]): File[] {
|
||||
|
|
30680
dist/index.js
vendored
30680
dist/index.js
vendored
File diff suppressed because one or more lines are too long
11105
package-lock.json
generated
11105
package-lock.json
generated
File diff suppressed because it is too large
Load diff
42
package.json
42
package.json
|
@ -25,29 +25,27 @@
|
|||
"author": "YourNameOrOrganization",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.4",
|
||||
"@actions/exec": "^1.0.4",
|
||||
"@actions/github": "^2.2.0",
|
||||
"@octokit/webhooks": "^7.6.2",
|
||||
"picomatch": "^2.2.2"
|
||||
"@actions/core": "^1.9.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^5.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/js-yaml": "^3.12.4",
|
||||
"@types/minimatch": "^3.0.3",
|
||||
"@types/node": "^14.0.5",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@vercel/ncc": "^0.33.1",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-plugin-github": "^4.3.6",
|
||||
"eslint-plugin-jest": "^22.21.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest-circus": "^27.4.6",
|
||||
"js-yaml": "^3.14.0",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-jest": "^27.1.3",
|
||||
"typescript": "^3.9.3"
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/node": "^18.7.3",
|
||||
"@types/picomatch": "^2.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-plugin-github": "^4.3.7",
|
||||
"eslint-plugin-jest": "^26.8.2",
|
||||
"jest": "^28.1.3",
|
||||
"jest-circus": "^28.1.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,23 @@ import {File, ChangeStatus} from './file'
|
|||
interface FilterYaml {
|
||||
[name: string]: FilterItemYaml
|
||||
}
|
||||
type FilterItemYaml =
|
||||
type FilterItemYaml = includesFilter | {paths: includesFilter; paths_ignore: excludesFilter} | FilterItemYaml[] // Supports referencing another rule via YAML anchor
|
||||
|
||||
type includesFilter =
|
||||
| string // Filename pattern, e.g. "path/to/*.js"
|
||||
| string[] // Array of filename patterns e.g. ["path/to/thing/**", "path/to/another/**"]
|
||||
| {[changeTypes: string]: string | string[]} // Change status and filename, e.g. added|modified: "path/to/*.js"
|
||||
| FilterItemYaml[] // Supports referencing another rule via YAML anchor
|
||||
|
||||
type excludesFilter = string[] // Filename pattern, e.g. "path/to/*.js"
|
||||
|
||||
// Minimatch options used in all matchers
|
||||
const MatchOptions = {
|
||||
dot: true
|
||||
type matchoptions = {
|
||||
dot: boolean
|
||||
ignore: string[]
|
||||
}
|
||||
const defaultMatchOptions: matchoptions = {
|
||||
dot: true,
|
||||
ignore: []
|
||||
}
|
||||
|
||||
// Internal representation of one item in named filter rule
|
||||
|
@ -43,7 +52,7 @@ export class Filter {
|
|||
return
|
||||
}
|
||||
|
||||
const doc = jsyaml.safeLoad(yaml) as FilterYaml
|
||||
const doc = jsyaml.load(yaml) as FilterYaml
|
||||
if (typeof doc !== 'object') {
|
||||
this.throwInvalidFormatError('Root element is not an object')
|
||||
}
|
||||
|
@ -67,9 +76,11 @@ export class Filter {
|
|||
)
|
||||
}
|
||||
|
||||
private parseFilterItemYaml(item: FilterItemYaml): FilterRuleItem[] {
|
||||
private parseFilterItemYaml(item: FilterItemYaml, excludes: excludesFilter = []): FilterRuleItem[] {
|
||||
var MatchOptions: matchoptions = Object.assign(defaultMatchOptions)
|
||||
MatchOptions.ignore = excludes
|
||||
if (Array.isArray(item)) {
|
||||
return flat(item.map(i => this.parseFilterItemYaml(i)))
|
||||
return flat(item.map(i => this.parseFilterItemYaml(i, excludes)))
|
||||
}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
|
@ -77,21 +88,25 @@ export class Filter {
|
|||
}
|
||||
|
||||
if (typeof item === 'object') {
|
||||
return Object.entries(item).map(([key, pattern]) => {
|
||||
if (typeof key !== 'string' || (typeof pattern !== 'string' && !Array.isArray(pattern))) {
|
||||
this.throwInvalidFormatError(
|
||||
`Expected [key:string]= pattern:string | string[], but [${key}:${typeof key}]= ${pattern}:${typeof pattern} found`
|
||||
)
|
||||
}
|
||||
return {
|
||||
status: key
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => x.toLowerCase()) as ChangeStatus[],
|
||||
isMatch: picomatch(pattern, MatchOptions)
|
||||
}
|
||||
})
|
||||
if (item.paths_ignore && item.paths) {
|
||||
return this.parseFilterItemYaml(item.paths, item.paths_ignore as excludesFilter)
|
||||
} else {
|
||||
return Object.entries(item).map(([key, pattern]) => {
|
||||
if (typeof key !== 'string' || (typeof pattern !== 'string' && !Array.isArray(pattern))) {
|
||||
this.throwInvalidFormatError(
|
||||
`Expected [key:string]= pattern:string | string[], but [${key}:${typeof key}]= ${pattern}:${typeof pattern} found`
|
||||
)
|
||||
}
|
||||
return {
|
||||
status: key
|
||||
.split('|')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)
|
||||
.map(x => x.toLowerCase()) as ChangeStatus[],
|
||||
isMatch: picomatch(pattern, MatchOptions)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.throwInvalidFormatError(`Unexpected element type '${typeof item}'`)
|
||||
|
|
34
src/main.ts
34
src/main.ts
|
@ -1,16 +1,12 @@
|
|||
import * as fs from 'fs'
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import {Webhooks} from '@octokit/webhooks'
|
||||
|
||||
import {Filter, FilterResults} from './filter'
|
||||
import {File, ChangeStatus} from './file'
|
||||
import * as git from './git'
|
||||
import {backslashEscape, shellEscape} from './list-format/shell-escape'
|
||||
import {csvEscape} from './list-format/csv-escape'
|
||||
|
||||
type ExportFormat = 'none' | 'csv' | 'json' | 'shell' | 'escape'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const workingDirectory = core.getInput('working-directory', {required: false})
|
||||
|
@ -41,7 +37,11 @@ async function run(): Promise<void> {
|
|||
const results = filter.match(files)
|
||||
exportResults(results, listFiles)
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
let message
|
||||
if (error instanceof Error) message = error.message
|
||||
else message = String(error)
|
||||
// we'll proceed, but let's report it
|
||||
core.setFailed(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,9 +101,9 @@ async function getChangedFiles(token: string, base: string, ref: string, initial
|
|||
if (base) {
|
||||
core.warning(`'base' input parameter is ignored when 'ref' is not also set on PR events.`)
|
||||
}
|
||||
const pr = github.context.payload.pull_request as Webhooks.WebhookPayloadPullRequestPullRequest
|
||||
if (token) {
|
||||
return await getChangedFilesFromApi(token, pr)
|
||||
const pr = github.context.payload.pull_request
|
||||
if (token && pr?.number) {
|
||||
return await getChangedFilesFromApi(token, pr.number)
|
||||
}
|
||||
if (github.context.eventName === 'pull_request_target') {
|
||||
// pull_request_target is executed in context of base branch and GITHUB_SHA points to last commit in base branch
|
||||
|
@ -121,8 +121,7 @@ async function getChangedFiles(token: string, base: string, ref: string, initial
|
|||
async function getChangedFilesFromGit(base: string, head: string, initialFetchDepth: number): Promise<File[]> {
|
||||
const defaultBranch = github.context.payload.repository?.default_branch
|
||||
|
||||
const beforeSha =
|
||||
github.context.eventName === 'push' ? (github.context.payload as Webhooks.WebhookPayloadPush).before : null
|
||||
const beforeSha = github.context.eventName === 'push' ? github.context.payload.before : null
|
||||
|
||||
const currentRef = await git.getCurrentRef()
|
||||
|
||||
|
@ -182,22 +181,19 @@ async function getChangedFilesFromGit(base: string, head: string, initialFetchDe
|
|||
}
|
||||
|
||||
// Uses github REST api to get list of files changed in PR
|
||||
async function getChangedFilesFromApi(
|
||||
token: string,
|
||||
prNumber: Webhooks.WebhookPayloadPullRequestPullRequest
|
||||
): Promise<File[]> {
|
||||
core.startGroup(`Fetching list of changed files for PR#${prNumber.number} from Github API`)
|
||||
async function getChangedFilesFromApi(token: string, prNumber: number): Promise<File[]> {
|
||||
core.startGroup(`Fetching list of changed files for PR#${prNumber} from Github API`)
|
||||
try {
|
||||
const client = new github.GitHub(token)
|
||||
const client = github.getOctokit(token)
|
||||
const per_page = 100
|
||||
const files: File[] = []
|
||||
|
||||
for (let page = 1; ; page++) {
|
||||
core.info(`Invoking listFiles(pull_number: ${prNumber.number}, page: ${page}, per_page: ${per_page})`)
|
||||
const response = await client.pulls.listFiles({
|
||||
core.info(`Invoking listFiles(pull_number: ${prNumber}, page: ${page}, per_page: ${per_page})`)
|
||||
const response = await client.rest.pulls.listFiles({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
pull_number: prNumber.number,
|
||||
pull_number: prNumber,
|
||||
per_page,
|
||||
page
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue