mirror of
https://github.com/actions/labeler
synced 2026-09-04 19:15:10 +02:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98ce1450c7 | ||
|
|
2c2a2313b2 | ||
|
|
b7a8804475 | ||
|
|
bf12e9b00b | ||
|
|
b8dd2d9be0 | ||
|
|
53affe8ca4 | ||
|
|
f612d9ad18 | ||
|
|
f27b608878 | ||
|
|
c5dadc2a45 | ||
|
|
e52e4fb63e | ||
|
|
77a4082b84 | ||
|
|
25abb3cad4 | ||
|
|
395c8cfdb1 | ||
|
|
634933edcd | ||
|
|
f1a63e87db | ||
|
|
b0a1180683 | ||
|
|
110d44140c | ||
|
|
bee50fefe1 | ||
|
|
6463cdb00e | ||
|
|
c209686724 | ||
|
|
5184940b54 | ||
|
|
3629d5568b | ||
|
|
d24f7f3731 | ||
|
|
425a1f1422 |
@@ -1,7 +0,0 @@
|
||||
# Ignore list
|
||||
/*
|
||||
|
||||
# Do not ignore these folders:
|
||||
!__tests__/
|
||||
!__mocks__/
|
||||
!src/
|
||||
@@ -1,51 +0,0 @@
|
||||
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:eslint-plugin-jest/recommended',
|
||||
'eslint-config-prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint', 'eslint-plugin-node', 'eslint-plugin-jest'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-ignore': 'allow-with-description'
|
||||
}
|
||||
],
|
||||
'no-console': 'error',
|
||||
'yoda': 'error',
|
||||
'prefer-const': [
|
||||
'error',
|
||||
{
|
||||
destructuring: 'all'
|
||||
}
|
||||
],
|
||||
'no-control-regex': 'off',
|
||||
'no-constant-condition': ['error', {checkLoops: false}],
|
||||
'node/no-extraneous-import': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*{test,spec}.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'jest/no-standalone-expect': 'off',
|
||||
'jest/no-conditional-expect': 'off',
|
||||
'no-console': 'off',
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
'jest/globals': true
|
||||
}
|
||||
};
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
name: Basic validation
|
||||
uses: actions/reusable-workflows/.github/workflows/basic-validation.yml@main
|
||||
with:
|
||||
node-version: '20.x'
|
||||
node-version: '24.x'
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
name: Check dist/
|
||||
uses: actions/reusable-workflows/.github/workflows/check-dist.yml@main
|
||||
with:
|
||||
node-version: '20.x'
|
||||
node-version: '24.x'
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Publish
|
||||
id: publish
|
||||
uses: actions/publish-immutable-action@0.0.3
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update the ${{ env.TAG_NAME }} tag
|
||||
uses: actions/publish-action@v0.2.2
|
||||
uses: actions/publish-action@v0.4.0
|
||||
with:
|
||||
source-tag: ${{ env.TAG_NAME }}
|
||||
slack-webhook: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
+5
-1
@@ -11,4 +11,8 @@ allowed:
|
||||
- unlicense
|
||||
|
||||
reviewed:
|
||||
npm:
|
||||
npm:
|
||||
- "@actions/http-client"
|
||||
- balanced-match
|
||||
- brace-expansion
|
||||
- minimatch
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/core"
|
||||
version: 1.11.1
|
||||
version: 3.0.1
|
||||
type: npm
|
||||
summary: Actions core lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/core
|
||||
|
||||
Generated
+2
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/exec"
|
||||
version: 1.1.1
|
||||
version: 3.0.0
|
||||
type: npm
|
||||
summary: Actions exec lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
|
||||
@@ -17,4 +17,4 @@ licenses:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
notices: []
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/github"
|
||||
version: 6.0.0
|
||||
version: 9.1.1
|
||||
type: npm
|
||||
summary: Actions github lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/github
|
||||
|
||||
Generated
+2
-2
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: "@actions/http-client"
|
||||
version: 2.2.0
|
||||
version: 3.0.2
|
||||
type: npm
|
||||
summary: Actions Http Client
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
|
||||
license: mit
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "@actions/http-client"
|
||||
version: 4.0.1
|
||||
type: npm
|
||||
summary: Actions Http Client
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
Actions Http Client for Node.js
|
||||
|
||||
Copyright (c) GitHub, Inc.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
Generated
+2
-2
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/io"
|
||||
version: 1.1.3
|
||||
version: 3.0.2
|
||||
type: npm
|
||||
summary: Actions io lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/io
|
||||
@@ -17,4 +17,4 @@ licenses:
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
notices: []
|
||||
|
||||
Generated
-30
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: "@fastify/busboy"
|
||||
version: 2.0.0
|
||||
type: npm
|
||||
summary: A streaming parser for HTML form data for node.js
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
notices: []
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/auth-token"
|
||||
version: 4.0.0
|
||||
version: 6.0.0
|
||||
type: npm
|
||||
summary: GitHub API token authentication for browsers and Node.js
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/core"
|
||||
version: 5.0.1
|
||||
version: 7.0.6
|
||||
type: npm
|
||||
summary: Extendable client for GitHub's REST & GraphQL APIs
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/endpoint"
|
||||
version: 9.0.1
|
||||
version: 11.0.3
|
||||
type: npm
|
||||
summary: Turns REST API endpoints into generic request options
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/graphql"
|
||||
version: 7.0.2
|
||||
version: 9.0.3
|
||||
type: npm
|
||||
summary: GitHub GraphQL API client for browsers and Node
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
---
|
||||
name: "@octokit/openapi-types"
|
||||
version: 19.0.1
|
||||
version: 27.0.0
|
||||
type: npm
|
||||
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |-
|
||||
Copyright 2020 Gregor Martynus
|
||||
text: |
|
||||
Copyright (c) GitHub 2025 - Licensed as MIT.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/plugin-paginate-rest"
|
||||
version: 9.1.0
|
||||
version: 14.0.0
|
||||
type: npm
|
||||
summary: Octokit plugin to paginate REST API endpoint responses
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/plugin-rest-endpoint-methods"
|
||||
version: 10.1.0
|
||||
version: 17.0.0
|
||||
type: npm
|
||||
summary: Octokit plugin adding one method for all of api.github.com REST API endpoints
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/plugin-retry"
|
||||
version: 6.0.1
|
||||
version: 8.1.0
|
||||
type: npm
|
||||
summary: Automatic retry plugin for octokit
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/request-error"
|
||||
version: 5.0.1
|
||||
version: 7.1.0
|
||||
type: npm
|
||||
summary: Error class for Octokit request errors
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+2
-2
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: "@octokit/request"
|
||||
version: 8.1.4
|
||||
version: 10.0.10
|
||||
type: npm
|
||||
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
|
||||
and Node
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: "@octokit/types"
|
||||
version: 12.1.0
|
||||
version: 16.0.0
|
||||
type: npm
|
||||
summary: Shared TypeScript definitions for Octokit projects
|
||||
homepage:
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+5
-26
@@ -1,39 +1,18 @@
|
||||
---
|
||||
name: balanced-match
|
||||
version: 1.0.2
|
||||
version: 4.0.4
|
||||
type: npm
|
||||
summary: Match balanced character pairs, like "{" and "}"
|
||||
homepage: https://github.com/juliangruber/balanced-match
|
||||
license: mit
|
||||
homepage:
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
(MIT)
|
||||
|
||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
Original code Copyright Julian Gruber <julian@juliangruber.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
- sources: README.md
|
||||
text: |-
|
||||
(MIT)
|
||||
|
||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
Port to TypeScript Copyright Isaac Z. Schlueter <i@izs.me>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
Generated
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: before-after-hook
|
||||
version: 2.2.3
|
||||
version: 4.0.0
|
||||
type: npm
|
||||
summary: asynchronous before/error/after hooks for internal functionality
|
||||
homepage:
|
||||
homepage:
|
||||
license: apache-2.0
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
Generated
+6
-27
@@ -1,16 +1,18 @@
|
||||
---
|
||||
name: brace-expansion
|
||||
version: 2.0.1
|
||||
version: 5.0.9
|
||||
type: npm
|
||||
summary: Brace expansion as known from sh/bash
|
||||
homepage: https://github.com/juliangruber/brace-expansion
|
||||
license: mit
|
||||
homepage:
|
||||
license: other
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
Copyright Julian Gruber <julian@juliangruber.com>
|
||||
|
||||
TypeScript port Copyright Isaac Z. Schlueter <i@izs.me>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -22,29 +24,6 @@ licenses:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
- sources: README.md
|
||||
text: |-
|
||||
(MIT)
|
||||
|
||||
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
|
||||
Generated
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: content-type
|
||||
version: 2.0.0
|
||||
type: npm
|
||||
summary: Create and parse HTTP Content-Type header
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2015 Douglas Christopher Wilson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: |-
|
||||
[MIT](LICENSE)
|
||||
|
||||
[npm-image]: https://img.shields.io/npm/v/content-type
|
||||
[npm-url]: https://npmjs.org/package/content-type
|
||||
[downloads-image]: https://img.shields.io/npm/dm/content-type
|
||||
[downloads-url]: https://npmjs.org/package/content-type
|
||||
[build-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-type/ci.yml?branch=master
|
||||
[build-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml?query=branch%3Amaster
|
||||
[coverage-image]: https://img.shields.io/codecov/c/gh/jshttp/content-type
|
||||
[coverage-url]: https://codecov.io/gh/jshttp/content-type
|
||||
[license-image]: http://img.shields.io/npm/l/content-type.svg?style=flat
|
||||
[license-url]: LICENSE
|
||||
notices: []
|
||||
Generated
-28
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: deprecation
|
||||
version: 2.3.1
|
||||
type: npm
|
||||
summary: Log a deprecation message with stack
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Gregor Martynus and contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
- sources: README.md
|
||||
text: "[ISC](LICENSE)"
|
||||
notices: []
|
||||
Generated
+2
-2
@@ -1,9 +1,9 @@
|
||||
---
|
||||
name: js-yaml
|
||||
version: 4.1.0
|
||||
version: 5.2.3
|
||||
type: npm
|
||||
summary: YAML 1.2 parser and serializer
|
||||
homepage: https://github.com/nodeca/js-yaml
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
|
||||
+11
-18
@@ -1,16 +1,17 @@
|
||||
---
|
||||
name: is-plain-object
|
||||
version: 5.0.0
|
||||
name: json-with-bigint
|
||||
version: 3.5.8
|
||||
type: npm
|
||||
summary: Returns true if an object was created by the `Object` constructor, or Object.create(null).
|
||||
homepage: https://github.com/jonschlinkert/is-plain-object
|
||||
summary: JS library that allows you to easily serialize and deserialize data with
|
||||
BigInt values
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-2017, Jon Schlinkert.
|
||||
Copyright (c) 2023 Ivan Korolenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -19,22 +20,14 @@ licenses:
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
- sources: README.md
|
||||
text: |-
|
||||
Copyright © 2019, [Jon Schlinkert](https://github.com/jonschlinkert).
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
***
|
||||
|
||||
_This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.8.0, on April 28, 2019._
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
notices: []
|
||||
Generated
-58
@@ -1,58 +0,0 @@
|
||||
---
|
||||
name: lodash.isequal
|
||||
version: 4.5.0
|
||||
type: npm
|
||||
summary: The Lodash method `_.isEqual` exported as a module.
|
||||
homepage: https://lodash.com/
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
|
||||
Based on Underscore.js, copyright Jeremy Ashkenas,
|
||||
DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/lodash/lodash
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
Copyright and related rights for sample code are waived via CC0. Sample
|
||||
code is defined as all source code displayed within the prose of the
|
||||
documentation.
|
||||
|
||||
CC0: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
====
|
||||
|
||||
Files located in the node_modules and vendor directories are externally
|
||||
maintained libraries used by this software which have their own
|
||||
licenses; we recommend you read them, as their terms may differ from the
|
||||
terms above.
|
||||
notices: []
|
||||
Generated
+56
-16
@@ -1,26 +1,66 @@
|
||||
---
|
||||
name: minimatch
|
||||
version: 10.0.1
|
||||
version: 10.2.5
|
||||
type: npm
|
||||
summary: a glob matcher in javascript
|
||||
homepage:
|
||||
license: isc
|
||||
homepage:
|
||||
license: blueoak-1.0.0
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The ISC License
|
||||
# Blue Oak Model License
|
||||
|
||||
Copyright (c) 2011-2022 Isaac Z. Schlueter and Contributors
|
||||
Version 1.0.0
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
## Purpose
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
This license gives everyone as much permission to work with
|
||||
this software as possible, while protecting contributors
|
||||
from liability.
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to receive this license, you must agree to its
|
||||
rules. The rules of this license are both obligations
|
||||
under that agreement and conditions to your license.
|
||||
You must not do anything with this software that triggers
|
||||
a rule that you cannot or will not follow.
|
||||
|
||||
## Copyright
|
||||
|
||||
Each contributor licenses you to do everything with this
|
||||
software that would otherwise infringe that contributor's
|
||||
copyright in it.
|
||||
|
||||
## Notices
|
||||
|
||||
You must ensure that everyone who gets a copy of
|
||||
any part of this software from you, with or without
|
||||
changes, also gets the text of this license or a link to
|
||||
<https://blueoakcouncil.org/license/1.0.0>.
|
||||
|
||||
## Excuse
|
||||
|
||||
If anyone notifies you in writing that you have not
|
||||
complied with [Notices](#notices), you can keep your
|
||||
license by taking all practical steps to comply within 30
|
||||
days after the notice. If you do not do so, your license
|
||||
ends immediately.
|
||||
|
||||
## Patent
|
||||
|
||||
Each contributor licenses you to do everything with this
|
||||
software that would otherwise infringe any patent claims
|
||||
they can license or become able to license.
|
||||
|
||||
## Reliability
|
||||
|
||||
No contributor can revoke this license.
|
||||
|
||||
## No Liability
|
||||
|
||||
**_As far as the law allows, this software comes as is,
|
||||
without any warranty or condition, and no contributor
|
||||
will be liable to anyone for any damages related to this
|
||||
software or this license, under any kind of legal claim._**
|
||||
notices: []
|
||||
|
||||
Generated
-26
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: once
|
||||
version: 1.4.0
|
||||
type: npm
|
||||
summary: Run a function exactly one time
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: undici
|
||||
version: 5.28.4
|
||||
version: 6.28.0
|
||||
type: npm
|
||||
summary: An HTTP/1.1 client, written from scratch for Node.js
|
||||
homepage: https://undici.nodejs.org
|
||||
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
---
|
||||
name: universal-user-agent
|
||||
version: 6.0.0
|
||||
version: 7.0.3
|
||||
type: npm
|
||||
summary: Get a user agent string in both browser and node
|
||||
homepage:
|
||||
summary: Get a user agent string across all JavaScript Runtime Environments
|
||||
homepage:
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
# [ISC License](https://spdx.org/licenses/ISC)
|
||||
|
||||
Copyright (c) 2018, Gregor Martynus (https://github.com/gr2m)
|
||||
Copyright (c) 2018-2021, Gregor Martynus (https://github.com/gr2m)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: uuid
|
||||
version: 8.3.2
|
||||
type: npm
|
||||
summary: RFC4122 (v1, v4, and v5) UUIDs
|
||||
homepage:
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE.md
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
notices: []
|
||||
Generated
-26
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: wrappy
|
||||
version: 1.0.2
|
||||
type: npm
|
||||
summary: Callback wrapping utility
|
||||
homepage: https://github.com/npm/wrappy
|
||||
license: isc
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
notices: []
|
||||
@@ -1,11 +0,0 @@
|
||||
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
|
||||
module.exports = {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: false,
|
||||
arrowParens: 'avoid'
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@@ -4,6 +4,17 @@
|
||||
|
||||
Automatically label new pull requests based on the paths of files being changed or the branch name.
|
||||
|
||||
## What's changed in V7
|
||||
|
||||
- **Migrated to ESM** internally to support the latest `@actions/*` package versions. No changes to action inputs, outputs, or behavior.
|
||||
|
||||
## Breaking changes in V6
|
||||
|
||||
- Upgraded action from node20 to node24.
|
||||
> Make sure your runner is on version v2.327.1 or later to ensure compatibility with this release. [Release Notes](https://github.com/actions/runner/releases/tag/v2.327.1)
|
||||
|
||||
For more details, see the full release notes on the [release page](https://github.com/actions/labeler/releases/tag/v6.0.0)
|
||||
|
||||
## Breaking changes in V5
|
||||
1) The ability to apply labels based on the names of base and/or head branches was added ([#186](https://github.com/actions/labeler/issues/186) and [#54](https://github.com/actions/labeler/issues/54)). The match object for changed files was expanded with new combinations in order to make it more intuitive and flexible ([#423](https://github.com/actions/labeler/issues/423) and [#101](https://github.com/actions/labeler/issues/101)). As a result, the configuration file structure was significantly redesigned and is not compatible with the structure of the previous version. Please read the documentation below to find out how to adapt your configuration files for use with the new action version.
|
||||
|
||||
@@ -30,7 +41,7 @@ The match object allows control over the matching options. You can specify the l
|
||||
|
||||
The base match object is defined as:
|
||||
```yml
|
||||
- changed-files:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['list', 'of', 'globs']
|
||||
- any-glob-to-all-files: ['list', 'of', 'globs']
|
||||
- all-globs-to-any-file: ['list', 'of', 'globs']
|
||||
@@ -125,7 +136,7 @@ Documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['docs/*', 'guides/*']
|
||||
|
||||
# Add 'Documentation' label to any change to .md files within the entire repository
|
||||
# Add 'Documentation' label to any change to .md files within the entire repository
|
||||
Documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.md'
|
||||
@@ -146,6 +157,96 @@ release:
|
||||
- base-branch: 'main'
|
||||
```
|
||||
|
||||
#### Configuration Options
|
||||
|
||||
The labeler configuration file (`.github/labeler.yml`) supports the following top-level options:
|
||||
|
||||
| Name | Description |
|
||||
|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `changed-files-labels-limit` | Maximum number of new labels to apply based on changed files (must be a non-negative integer). If exceeded, no changed-files labels are applied for that run. |
|
||||
| `max-files-changed` | Maximum number of total changed files (must be a non-negative integer). If exceeded, all file-based labeling is skipped. |
|
||||
|
||||
##### Limiting changed-files labels
|
||||
|
||||
When working with large PRs (e.g., tree-wide refactors) that touch many components, you may want to prevent the labeler from adding too many labels. Set `changed-files-labels-limit` in your `.github/labeler.yml` configuration file to limit the number of labels that can be applied based on changed files patterns.
|
||||
|
||||
**Important behaviors:**
|
||||
|
||||
- The limit counts only **new** labels that would be added by changed-files rules. Labels already present on the PR are not counted toward the limit.
|
||||
- If the number of new changed-files labels **exceeds** the limit, **all** new changed-files labels are skipped for that run.
|
||||
- If the number of new changed-files labels **equals** the limit, labels are still applied normally.
|
||||
- Labels based on branch conditions (`head-branch`, `base-branch`) are **not affected** by the limit.
|
||||
- **Any label definition that includes a `changed-files` rule is considered a changed-files label** and is subject to the limit, regardless of which condition caused the label to match. For example, a label with both `head-branch` and `changed-files` rules will be subject to the limit even if it matches via the branch rule.
|
||||
- If both `max-files-changed` and `changed-files-labels-limit` are configured at the same time, `max-files-changed` is evaluated first, and if it triggers, `changed-files-labels-limit` is not evaluated.
|
||||
|
||||
##### Example
|
||||
|
||||
```yml
|
||||
# .github/labeler.yml
|
||||
|
||||
# Limit changed-files based labels to 5
|
||||
changed-files-labels-limit: 5
|
||||
|
||||
# Label definitions - these are subject to the limit
|
||||
frontend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/frontend/**'
|
||||
|
||||
backend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/backend/**'
|
||||
|
||||
docs:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'docs/**'
|
||||
|
||||
# This label has both branch and changed-files rules.
|
||||
# It is still subject to the limit because it includes changed-files.
|
||||
mixed:
|
||||
- any:
|
||||
- head-branch: '^feature/'
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/mixed/**'
|
||||
|
||||
# Branch-based labels are NOT affected by the limit
|
||||
feature:
|
||||
- head-branch: '^feature/'
|
||||
```
|
||||
|
||||
##### Skipping labeling for large PRs
|
||||
|
||||
When a PR modifies a very large number of files (e.g., tree-wide refactors, automated code formatting), you may want to skip file-based labeling entirely. Set `max-files-changed` in your `.github/labeler.yml` configuration file to skip all file-based labeling when the total number of changed files exceeds the threshold.
|
||||
|
||||
**Important behaviors:**
|
||||
|
||||
- If the total number of changed files **exceeds** the limit, all file-based labeling is skipped entirely.
|
||||
- If the total number of changed files **equals** the limit, labels are still applied normally.
|
||||
- Labels based **only** on branch conditions (`head-branch`, `base-branch`) are **not affected** by the limit.
|
||||
- **Any label definition that includes a `changed-files` rule is considered a file-based label** and will be skipped, regardless of which condition caused the label to match. For example, a label with both `head-branch` and `changed-files` rules will be skipped even if it would match via the branch rule.
|
||||
- Pre-existing labels on the PR are **preserved** — changed-files configs are not evaluated at all, so `sync-labels` will not remove them.
|
||||
|
||||
##### Example
|
||||
|
||||
```yml
|
||||
# .github/labeler.yml
|
||||
|
||||
# Skip file-based labeling if more than 100 files changed
|
||||
max-files-changed: 100
|
||||
|
||||
# These labels will be skipped if > 100 files changed
|
||||
frontend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/frontend/**'
|
||||
|
||||
backend:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/backend/**'
|
||||
|
||||
# Branch-based labels are NOT affected
|
||||
release:
|
||||
- base-branch: 'main'
|
||||
```
|
||||
|
||||
### Create Workflow
|
||||
|
||||
Create a workflow (e.g. `.github/workflows/labeler.yml` see [Creating a Workflow file](https://docs.github.com/en/actions/writing-workflows/quickstart#creating-your-first-workflow)) to utilize the labeler action with content:
|
||||
@@ -162,7 +263,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v6
|
||||
```
|
||||
|
||||
#### Inputs
|
||||
@@ -173,19 +274,33 @@ Various inputs are defined in [`action.yml`](action.yml) to let you configure th
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `repo-token` | Token to use to authorize label changes. Typically the GITHUB_TOKEN secret | `github.token` |
|
||||
| `configuration-path` | The path to the label configuration file. If the file doesn't exist at the specified path on the runner, action will read from the source repository via the Github API. | `.github/labeler.yml` |
|
||||
| `sync-labels` | Whether or not to remove labels when matching files are reverted or no longer changed by the PR | `false` |
|
||||
| `sync-labels` | Whether to remove configured labels when they no longer match. Labels not present in the labeler configuration are never removed. | `false` |
|
||||
| `dot` | Whether or not to auto-include paths starting with dot (e.g. `.github`) | `true` |
|
||||
| `pr-number` | The number(s) of pull request to update, rather than detecting from the workflow context | N/A |
|
||||
|
||||
When `sync-labels` is enabled, labeler synchronizes only labels whose names are
|
||||
present in the labeler configuration. Matching configured labels are added in
|
||||
one batch, and configured labels that no longer match are removed in one batch.
|
||||
Other labels, including labels added by users or other automation, are not
|
||||
rewritten or removed.
|
||||
|
||||
Removals and additions use separate API requests, so configured labels may be
|
||||
temporarily absent while a synchronization swaps stale labels for newly matching
|
||||
labels. Swaps can take longer than updates requiring only additions or removals
|
||||
because these requests run sequentially. At GitHub's 100-label limit, labels
|
||||
added concurrently between those requests can prevent all newly matching
|
||||
configured labels from being added; labeler does not remove unconfigured labels
|
||||
to make room.
|
||||
|
||||
##### Using `configuration-path` input together with the `@actions/checkout` action
|
||||
You might want to use action called [@actions/checkout](https://github.com/actions/checkout) to upload label configuration file onto the runner from the current or any other repositories. See usage example below:
|
||||
|
||||
```yml
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # Uploads repository content to the runner
|
||||
- uses: actions/checkout@v5 # Uploads repository content to the runner
|
||||
with:
|
||||
repository: "owner/repositoryName" # The one of the available inputs, visit https://github.com/actions/checkout#readme to find more
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
configuration-path: 'path/to/the/uploaded/configuration/file'
|
||||
|
||||
@@ -206,10 +321,10 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
|
||||
# Label PRs 1, 2, and 3
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
pr-number: |
|
||||
1
|
||||
2
|
||||
@@ -218,15 +333,20 @@ jobs:
|
||||
|
||||
**Note:** in normal usage the `pr-number` input is not required as the action will detect the PR number from the workflow context.
|
||||
|
||||
#### Outputs
|
||||
#### Outputs
|
||||
|
||||
Labeler provides the following outputs:
|
||||
Labeler provides the following outputs:
|
||||
|
||||
| Name | Description |
|
||||
|--------------|-----------------------------------------------------------|
|
||||
| `new-labels` | A comma-separated list of all new labels |
|
||||
| `all-labels` | A comma-separated list of all labels that the PR contains |
|
||||
|
||||
The outputs are calculated from the pull request label snapshot used by the
|
||||
action and the label changes it successfully applies. A label added
|
||||
concurrently by another actor is preserved, but may not appear in the outputs
|
||||
for that run.
|
||||
|
||||
The following example performs steps based on the output of labeler:
|
||||
```yml
|
||||
name: "Pull Request Labeler"
|
||||
@@ -241,14 +361,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: label-the-PR
|
||||
uses: actions/labeler@v5
|
||||
|
||||
uses: actions/labeler@v6
|
||||
|
||||
- id: run-frontend-tests
|
||||
if: contains(steps.label-the-PR.outputs.all-labels, 'frontend')
|
||||
run: |
|
||||
echo "Running frontend tests..."
|
||||
# Put your commands for running frontend tests here
|
||||
|
||||
|
||||
- id: run-backend-tests
|
||||
if: contains(steps.label-the-PR.outputs.all-labels, 'backend')
|
||||
run: |
|
||||
@@ -256,10 +376,37 @@ jobs:
|
||||
# Put your commands for running backend tests here
|
||||
```
|
||||
|
||||
## Permissions
|
||||
## Recommended Permissions
|
||||
|
||||
In order to add labels to pull requests, the GitHub labeler action requires write permissions on the pull-request. However, when the action runs on a pull request from a forked repository, GitHub only grants read access tokens for `pull_request` events, at most. If you encounter an `Error: HttpError: Resource not accessible by integration`, it's likely due to these permission constraints. To resolve this issue, you can modify the `on:` section of your workflow to use
|
||||
[`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) instead of `pull_request` (see example [above](#create-workflow)). This change allows the action to have write access, because `pull_request_target` alters the [context of the action](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) and safely grants additional permissions. Refer to the [GitHub token permissions documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) for more details about access levels and event contexts.
|
||||
To successfully add labels to pull requests using the GitHub Labeler Action, specific permissions must be granted based on your use case:
|
||||
|
||||
1. **Adding Existing Labels**:
|
||||
- Requires: `pull-requests: write`
|
||||
- Use this if all labels already exist in the repository (i.e., pre-defined in `.github/labeler.yml`).
|
||||
|
||||
2. **Creating New Labels**:
|
||||
- Requires: `issues: write`
|
||||
- This is necessary if the action needs to create labels that do not already exist in the repository.
|
||||
|
||||
However, when the action runs on a pull request from a forked repository, GitHub only grants read access tokens for `pull_request` events, at most. If you encounter an `Error: HttpError: Resource not accessible by integration`, it's likely due to these permission constraints. To resolve this issue, you can modify the `on:` section of your workflow to use
|
||||
[`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) instead of `pull_request` (see example [above](#create-workflow)). This change allows the action to have write access, because `pull_request_target` alters the [context of the action](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) and safely grants additional permissions.
|
||||
|
||||
There exists a potentially dangerous misuse of the `pull_request_target` workflow trigger that may lead to malicious PR authors (i.e. attackers) being able to obtain repository write permissions or stealing repository secrets. Hence, it is advisable that `pull_request_target` should only be used in workflows that are carefully designed to avoid executing untrusted code and to also ensure that workflows using `pull_request_target` limit access to sensitive resources. Refer to the [GitHub token permissions documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) for more details about access levels and event contexts.
|
||||
|
||||
### Example Workflow Permissions
|
||||
|
||||
To ensure the action works correctly, include the following permissions in your workflow file:
|
||||
|
||||
```yml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
```
|
||||
|
||||
### Manual Label Creation as an Alternative to Granting issues write Permission
|
||||
|
||||
If you prefer not to grant the `issues: write` permission in your workflow, you can manually create all required labels in the repository before the action runs.
|
||||
|
||||
## Notes regarding `pull_request_target` event
|
||||
|
||||
@@ -285,4 +432,4 @@ Once you confirm that the updated configuration files function as intended, you
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
|
||||
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
|
||||
@@ -1,43 +0,0 @@
|
||||
export const context = {
|
||||
payload: {
|
||||
pull_request: {
|
||||
number: 123,
|
||||
head: {
|
||||
ref: 'head-branch-name'
|
||||
},
|
||||
base: {
|
||||
ref: 'base-branch-name'
|
||||
}
|
||||
}
|
||||
},
|
||||
repo: {
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld'
|
||||
}
|
||||
};
|
||||
|
||||
const mockApi = {
|
||||
rest: {
|
||||
issues: {
|
||||
setLabels: jest.fn()
|
||||
},
|
||||
pulls: {
|
||||
get: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
labels: []
|
||||
}
|
||||
}),
|
||||
listFiles: {
|
||||
endpoint: {
|
||||
merge: jest.fn().mockReturnValue({})
|
||||
}
|
||||
}
|
||||
},
|
||||
repos: {
|
||||
getContent: jest.fn()
|
||||
}
|
||||
},
|
||||
paginate: jest.fn()
|
||||
};
|
||||
|
||||
export const getOctokit = jest.fn().mockImplementation(() => mockApi);
|
||||
@@ -0,0 +1,131 @@
|
||||
import {jest, describe, it, expect} from '@jest/globals';
|
||||
import type {ClientType} from '../src/api/types.js';
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: {
|
||||
repo: {owner: 'monalisa', repo: 'helloworld'}
|
||||
}
|
||||
}));
|
||||
|
||||
const {addLabels} = await import('../src/api/add-labels.js');
|
||||
|
||||
const createClient = () => {
|
||||
const addLabelsMock = jest.fn<any>();
|
||||
const listLabelsOnIssueMock = jest.fn<any>();
|
||||
const client = {
|
||||
rest: {
|
||||
issues: {
|
||||
addLabels: addLabelsMock,
|
||||
listLabelsOnIssue: listLabelsOnIssueMock
|
||||
}
|
||||
}
|
||||
} as ClientType;
|
||||
|
||||
return {client, addLabelsMock, listLabelsOnIssueMock};
|
||||
};
|
||||
|
||||
describe('addLabels', () => {
|
||||
it('does not verify a successful addition', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
addLabelsMock.mockResolvedValue({data: []});
|
||||
|
||||
await addLabels(client, 123, ['bug']);
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['bug'],
|
||||
request: {retries: 0}
|
||||
});
|
||||
expect(listLabelsOnIssueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts a server error when every requested label was committed', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock.mockResolvedValue({
|
||||
data: [{name: 'BUG'}, {name: 'documentation'}],
|
||||
headers: {}
|
||||
});
|
||||
|
||||
await expect(
|
||||
addLabels(client, 123, ['bug', 'documentation'])
|
||||
).resolves.toBeUndefined();
|
||||
expect(listLabelsOnIssueMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
per_page: 100,
|
||||
page: 1,
|
||||
request: {retries: 0}
|
||||
});
|
||||
});
|
||||
|
||||
it('checks subsequent pages after a committed server error', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock
|
||||
.mockResolvedValueOnce({
|
||||
data: Array.from({length: 100}, (_, index) => ({
|
||||
name: `label-${index}`
|
||||
})),
|
||||
headers: {
|
||||
link: '<https://api.github.com/issues/123/labels?page=2>; rel="next"'
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: [{name: 'documentation'}],
|
||||
headers: {}
|
||||
});
|
||||
|
||||
await expect(
|
||||
addLabels(client, 123, ['label-0', 'documentation'])
|
||||
).resolves.toBeUndefined();
|
||||
expect(listLabelsOnIssueMock).toHaveBeenNthCalledWith(2, {
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
per_page: 100,
|
||||
page: 2,
|
||||
request: {retries: 0}
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves a server error when any requested label is missing', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock.mockResolvedValue({
|
||||
data: [{name: 'bug'}],
|
||||
headers: {}
|
||||
});
|
||||
|
||||
await expect(addLabels(client, 123, ['bug', 'documentation'])).rejects.toBe(
|
||||
serverError
|
||||
);
|
||||
expect(listLabelsOnIssueMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not verify non-server errors', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const validationError = Object.assign(new Error('Validation Failed'), {
|
||||
status: 422
|
||||
});
|
||||
addLabelsMock.mockRejectedValue(validationError);
|
||||
|
||||
await expect(addLabels(client, 123, ['bug'])).rejects.toBe(validationError);
|
||||
expect(listLabelsOnIssueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves the server error when verification fails', async () => {
|
||||
const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
|
||||
const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
|
||||
addLabelsMock.mockRejectedValue(serverError);
|
||||
listLabelsOnIssueMock.mockRejectedValue(new Error('Service unavailable'));
|
||||
|
||||
await expect(addLabels(client, 123, ['bug'])).rejects.toBe(serverError);
|
||||
});
|
||||
});
|
||||
+36
-19
@@ -1,14 +1,31 @@
|
||||
import {
|
||||
getBranchName,
|
||||
checkAnyBranch,
|
||||
checkAllBranch,
|
||||
toBranchMatchConfig,
|
||||
BranchMatchConfig
|
||||
} from '../src/branch';
|
||||
import * as github from '@actions/github';
|
||||
import {jest, describe, beforeEach, it, expect} from '@jest/globals';
|
||||
import type {BranchMatchConfig} from '../src/branch.js';
|
||||
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/github');
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn()
|
||||
}));
|
||||
|
||||
const mockGithubContext = {
|
||||
payload: {
|
||||
pull_request: {
|
||||
number: 123,
|
||||
head: {ref: 'head-branch-name'},
|
||||
base: {ref: 'base-branch-name'}
|
||||
}
|
||||
},
|
||||
repo: {owner: 'monalisa', repo: 'helloworld'}
|
||||
} as any;
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: mockGithubContext,
|
||||
getOctokit: jest.fn()
|
||||
}));
|
||||
|
||||
const {getBranchName, checkAnyBranch, checkAllBranch, toBranchMatchConfig} =
|
||||
await import('../src/branch.js');
|
||||
|
||||
describe('getBranchName', () => {
|
||||
describe('when the pull requests base branch is requested', () => {
|
||||
@@ -28,10 +45,10 @@ describe('getBranchName', () => {
|
||||
|
||||
describe('checkAllBranch', () => {
|
||||
beforeEach(() => {
|
||||
github.context.payload.pull_request!.head = {
|
||||
mockGithubContext.payload.pull_request.head = {
|
||||
ref: 'test/feature/123'
|
||||
};
|
||||
github.context.payload.pull_request!.base = {
|
||||
mockGithubContext.payload.pull_request.base = {
|
||||
ref: 'main'
|
||||
};
|
||||
});
|
||||
@@ -87,10 +104,10 @@ describe('checkAllBranch', () => {
|
||||
|
||||
describe('checkAnyBranch', () => {
|
||||
beforeEach(() => {
|
||||
github.context.payload.pull_request!.head = {
|
||||
mockGithubContext.payload.pull_request.head = {
|
||||
ref: 'test/feature/123'
|
||||
};
|
||||
github.context.payload.pull_request!.base = {
|
||||
mockGithubContext.payload.pull_request.base = {
|
||||
ref: 'main'
|
||||
};
|
||||
});
|
||||
@@ -159,7 +176,7 @@ describe('toBranchMatchConfig', () => {
|
||||
|
||||
it('sets headBranch in the matchConfig', () => {
|
||||
const result = toBranchMatchConfig(config);
|
||||
expect(result).toEqual<BranchMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
headBranch: ['testing']
|
||||
});
|
||||
});
|
||||
@@ -169,7 +186,7 @@ describe('toBranchMatchConfig', () => {
|
||||
|
||||
it('sets headBranch in the matchConfig', () => {
|
||||
const result = toBranchMatchConfig(stringConfig);
|
||||
expect(result).toEqual<BranchMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
headBranch: ['testing']
|
||||
});
|
||||
});
|
||||
@@ -180,7 +197,7 @@ describe('toBranchMatchConfig', () => {
|
||||
const config = {'base-branch': ['testing']};
|
||||
it('sets baseBranch in the matchConfig', () => {
|
||||
const result = toBranchMatchConfig(config);
|
||||
expect(result).toEqual<BranchMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
baseBranch: ['testing']
|
||||
});
|
||||
});
|
||||
@@ -190,7 +207,7 @@ describe('toBranchMatchConfig', () => {
|
||||
|
||||
it('sets baseBranch in the matchConfig', () => {
|
||||
const result = toBranchMatchConfig(stringConfig);
|
||||
expect(result).toEqual<BranchMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
baseBranch: ['testing']
|
||||
});
|
||||
});
|
||||
@@ -201,7 +218,7 @@ describe('toBranchMatchConfig', () => {
|
||||
const config = {'base-branch': ['testing'], 'head-branch': ['testing']};
|
||||
it('sets headBranch and baseBranch in the matchConfig', () => {
|
||||
const result = toBranchMatchConfig(config);
|
||||
expect(result).toEqual<BranchMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
baseBranch: ['testing'],
|
||||
headBranch: ['testing']
|
||||
});
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
import {
|
||||
ChangedFilesMatchConfig,
|
||||
import {jest, describe, it, expect} from '@jest/globals';
|
||||
import type {ChangedFilesMatchConfig} from '../src/changedFiles.js';
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
error: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: {
|
||||
payload: {
|
||||
pull_request: {number: 123, head: {ref: 'head'}, base: {ref: 'base'}}
|
||||
},
|
||||
repo: {owner: 'monalisa', repo: 'helloworld'}
|
||||
},
|
||||
getOctokit: jest.fn()
|
||||
}));
|
||||
|
||||
const {
|
||||
checkAllChangedFiles,
|
||||
checkAnyChangedFiles,
|
||||
toChangedFilesMatchConfig,
|
||||
@@ -7,10 +26,7 @@ import {
|
||||
checkIfAllGlobsMatchAnyFile,
|
||||
checkIfAnyGlobMatchesAllFiles,
|
||||
checkIfAllGlobsMatchAllFiles
|
||||
} from '../src/changedFiles';
|
||||
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/github');
|
||||
} = await import('../src/changedFiles.js');
|
||||
|
||||
describe('checkAllChangedFiles', () => {
|
||||
const changedFiles = ['foo.txt', 'bar.txt'];
|
||||
@@ -92,7 +108,7 @@ describe('toChangedFilesMatchConfig', () => {
|
||||
|
||||
it('returns an empty object', () => {
|
||||
const result = toChangedFilesMatchConfig(config);
|
||||
expect(result).toEqual<ChangedFilesMatchConfig>({});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +145,7 @@ describe('toChangedFilesMatchConfig', () => {
|
||||
|
||||
it('sets the value in the config object', () => {
|
||||
const result = toChangedFilesMatchConfig(config);
|
||||
expect(result).toEqual<ChangedFilesMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
changedFiles: [{anyGlobToAnyFile: ['testing']}]
|
||||
});
|
||||
});
|
||||
@@ -140,7 +156,7 @@ describe('toChangedFilesMatchConfig', () => {
|
||||
|
||||
it(`sets the string as an array in the config object`, () => {
|
||||
const result = toChangedFilesMatchConfig(config);
|
||||
expect(result).toEqual<ChangedFilesMatchConfig>({
|
||||
expect(result).toEqual({
|
||||
changedFiles: [{anyGlobToAnyFile: ['testing']}]
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Limit to 0 changed-files labels (none allowed)
|
||||
changed-files-labels-limit: 0
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
@@ -0,0 +1,26 @@
|
||||
# Limit to 1 changed-files label
|
||||
changed-files-labels-limit: 1
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
@@ -0,0 +1,26 @@
|
||||
# Limit to 2 changed-files labels
|
||||
changed-files-labels-limit: 2
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
@@ -0,0 +1,26 @@
|
||||
# Limit to 3 changed-files labels
|
||||
changed-files-labels-limit: 3
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
@@ -0,0 +1,15 @@
|
||||
# Skip file-based labeling if more than 5 files changed
|
||||
max-files-changed: 5
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
@@ -0,0 +1,15 @@
|
||||
# Skip file-based labeling if more than 3 files changed
|
||||
max-files-changed: 3
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
# Branch-based label (should not be affected)
|
||||
test-branch:
|
||||
- head-branch: ['^test/']
|
||||
@@ -0,0 +1,23 @@
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
@@ -0,0 +1,16 @@
|
||||
# Test fixture for mixed rules behavior
|
||||
# A label with both branch and changed-files rules is considered a "changed-files label"
|
||||
# and is subject to the limit, even if it matches via the branch rule
|
||||
changed-files-labels-limit: 0
|
||||
|
||||
# This label has both branch and changed-files rules
|
||||
# It should be subject to the limit even if matched via branch
|
||||
mixed-label:
|
||||
- any:
|
||||
- head-branch: '^test/'
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
# Pure branch-based label - not subject to limit
|
||||
pure-branch-label:
|
||||
- head-branch: '^test/'
|
||||
+401
-10
@@ -1,20 +1,67 @@
|
||||
import {jest, describe, it, expect, beforeEach, beforeAll} from '@jest/globals';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as core from '@actions/core';
|
||||
import * as fs from 'fs';
|
||||
import {checkMatchConfigs} from '../src/labeler';
|
||||
import {
|
||||
import type {
|
||||
MatchConfig,
|
||||
BaseMatchConfig
|
||||
} from '../src/api/get-label-configs.js';
|
||||
|
||||
// Define API mock functions at module level
|
||||
const getPullRequestsMock = jest.fn<any>();
|
||||
const getLabelConfigsMock = jest.fn<any>();
|
||||
const addLabelsMock = jest.fn<any>();
|
||||
const removeLabelsMock = jest.fn<any>();
|
||||
const getChangedFilesMock = jest.fn<any>();
|
||||
const getContentMock = jest.fn<any>();
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
getInput: jest.fn(),
|
||||
getMultilineInput: jest.fn(),
|
||||
getBooleanInput: jest.fn(),
|
||||
setOutput: jest.fn(),
|
||||
setFailed: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
info: jest.fn(),
|
||||
debug: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: {
|
||||
payload: {
|
||||
pull_request: {
|
||||
number: 123,
|
||||
head: {ref: 'head-branch'},
|
||||
base: {ref: 'base-branch'}
|
||||
}
|
||||
},
|
||||
repo: {owner: 'monalisa', repo: 'helloworld'}
|
||||
},
|
||||
getOctokit: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../src/api/index.js', () => ({
|
||||
getPullRequests: getPullRequestsMock,
|
||||
getLabelConfigs: getLabelConfigsMock,
|
||||
addLabels: addLabelsMock,
|
||||
removeLabels: removeLabelsMock,
|
||||
getChangedFiles: getChangedFilesMock,
|
||||
getContent: getContentMock
|
||||
}));
|
||||
|
||||
const core = await import('@actions/core');
|
||||
const github = await import('@actions/github');
|
||||
const api = await import('../src/api/index.js');
|
||||
const {labeler, checkMatchConfigs} = await import('../src/labeler.js');
|
||||
const {
|
||||
toMatchConfig,
|
||||
getLabelConfigMapFromObject,
|
||||
BaseMatchConfig
|
||||
} from '../src/api/get-label-configs';
|
||||
|
||||
jest.mock('@actions/core');
|
||||
getLabelConfigResultFromObject,
|
||||
configUsesChangedFiles
|
||||
} = await import('../src/api/get-label-configs.js');
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name, options) => {
|
||||
return jest.requireActual('@actions/core').getInput(name, options);
|
||||
});
|
||||
(core.getInput as jest.Mock).mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
const loadYaml = (filepath: string) => {
|
||||
@@ -56,6 +103,204 @@ describe('getLabelConfigMapFromObject', () => {
|
||||
const result = getLabelConfigMapFromObject(yamlObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('ignores top-level options like changed-files-labels-limit and max-files-changed', () => {
|
||||
const configWithLimit = {
|
||||
'changed-files-labels-limit': 5,
|
||||
'max-files-changed': 100,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigMapFromObject(configWithLimit);
|
||||
expect(result.has('changed-files-labels-limit')).toBe(false);
|
||||
expect(result.has('max-files-changed')).toBe(false);
|
||||
expect(result.has('label1')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLabelConfigResultFromObject', () => {
|
||||
it('extracts changed-files-labels-limit as a number', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 5,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(5);
|
||||
expect(result.labelConfigs.has('label1')).toBe(true);
|
||||
});
|
||||
|
||||
it('parses changed-files-labels-limit from string', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': '10',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(10);
|
||||
});
|
||||
|
||||
it('trims whitespace when parsing string values', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': ' 5 ',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(5);
|
||||
});
|
||||
|
||||
it('returns undefined changedFilesLimit when not set', () => {
|
||||
const config = {
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws error for invalid changed-files-labels-limit value', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 'invalid',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/Invalid value for 'changed-files-labels-limit'/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for negative changed-files-labels-limit value', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': -1,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for string with trailing characters', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': '10abc',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for decimal string', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': '3.2',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for float number', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 3.2,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts zero as a valid changed-files-labels-limit', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 0,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts max-files-changed as a number', () => {
|
||||
const config = {
|
||||
'max-files-changed': 100,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBe(100);
|
||||
expect(result.labelConfigs.has('label1')).toBe(true);
|
||||
});
|
||||
|
||||
it('parses max-files-changed from string', () => {
|
||||
const config = {
|
||||
'max-files-changed': '50',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBe(50);
|
||||
});
|
||||
|
||||
it('returns undefined maxFilesChanged when not set', () => {
|
||||
const config = {
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws error for invalid max-files-changed value', () => {
|
||||
const config = {
|
||||
'max-files-changed': 'invalid',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/Invalid value for 'max-files-changed'/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for negative max-files-changed value', () => {
|
||||
const config = {
|
||||
'max-files-changed': -1,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts zero as a valid max-files-changed', () => {
|
||||
const config = {
|
||||
'max-files-changed': 0,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBe(0);
|
||||
});
|
||||
|
||||
it('supports both options together', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 5,
|
||||
'max-files-changed': 100,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(5);
|
||||
expect(result.maxFilesChanged).toBe(100);
|
||||
});
|
||||
|
||||
it('throws a clear error when max-files-changed is used as a label', () => {
|
||||
const config = {
|
||||
'max-files-changed': [
|
||||
{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}
|
||||
]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/reserved top-level option and cannot be used as a label name/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a clear error when changed-files-labels-limit is used as a label', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': [
|
||||
{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}
|
||||
]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/reserved top-level option and cannot be used as a label name/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMatchConfig', () => {
|
||||
@@ -159,3 +404,149 @@ describe('checkMatchConfigs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('configUsesChangedFiles', () => {
|
||||
it('returns true when config has changed-files in any block', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{any: [{changedFiles: [{anyGlobToAnyFile: ['*.txt']}]}]}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when config has changed-files in all block', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{all: [{changedFiles: [{allGlobsToAllFiles: ['*.txt']}]}]}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when config only has branch patterns', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{any: [{headBranch: ['^test/']}]},
|
||||
{any: [{baseBranch: ['main']}]}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when config has empty changed-files array', () => {
|
||||
const matchConfig: MatchConfig[] = [{any: [{changedFiles: []}]}];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when config has changed-files with empty objects', () => {
|
||||
const matchConfig: MatchConfig[] = [{any: [{changedFiles: [{}]}]}];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when config has mixed branch and changed-files patterns', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{
|
||||
any: [
|
||||
{changedFiles: [{anyGlobToAnyFile: ['*.txt']}]},
|
||||
{headBranch: ['^feature/']}
|
||||
]
|
||||
}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('labeler error handling', () => {
|
||||
const mockClient = {} as any;
|
||||
const mockPullRequest = {
|
||||
number: 123,
|
||||
data: {labels: []},
|
||||
changedFiles: []
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
(github.getOctokit as jest.Mock).mockReturnValue(mockClient);
|
||||
getPullRequestsMock.mockReturnValue([
|
||||
{
|
||||
...mockPullRequest,
|
||||
data: {labels: [{name: 'old-label'}]}
|
||||
}
|
||||
]);
|
||||
|
||||
getLabelConfigsMock.mockResolvedValue({
|
||||
labelConfigs: new Map([['new-label', ['dummy-config']]]),
|
||||
changedFilesLimit: undefined
|
||||
});
|
||||
|
||||
// Force match so "new-label" is always added
|
||||
jest.spyOn({checkMatchConfigs}, 'checkMatchConfigs').mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('throws a custom error for HttpError 403 with "unauthorized" message', async () => {
|
||||
addLabelsMock.mockRejectedValue({
|
||||
name: 'HttpError',
|
||||
status: 403,
|
||||
message: 'Request failed with status code 403: Unauthorized'
|
||||
});
|
||||
|
||||
await expect(labeler()).rejects.toThrow(
|
||||
/does not have permission to create labels/
|
||||
);
|
||||
});
|
||||
|
||||
it('rethrows unexpected HttpError', async () => {
|
||||
const unexpectedError = {
|
||||
name: 'HttpError',
|
||||
status: 404,
|
||||
message: 'Not Found'
|
||||
};
|
||||
addLabelsMock.mockRejectedValue(unexpectedError);
|
||||
|
||||
// NOTE: In the current implementation, labeler rethrows the raw error object (not an Error instance).
|
||||
// `rejects.toThrow` only works with real Error objects, so here we must use `rejects.toEqual`.
|
||||
// If labeler is updated to always wrap errors in `Error`, this test can be changed to use `rejects.toThrow`.
|
||||
await expect(labeler()).rejects.toEqual(unexpectedError);
|
||||
});
|
||||
|
||||
it('handles "Resource not accessible by integration" gracefully', async () => {
|
||||
const error = {
|
||||
name: 'HttpError',
|
||||
message: 'Resource not accessible by integration'
|
||||
};
|
||||
addLabelsMock.mockRejectedValue(error);
|
||||
|
||||
await labeler();
|
||||
|
||||
expect(core.warning).toHaveBeenCalledWith(
|
||||
expect.stringContaining("requires 'issues: write'"),
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(core.setFailed).toHaveBeenCalledWith(error.message);
|
||||
});
|
||||
|
||||
it('reports the configured labels when a bulk removal fails', async () => {
|
||||
(core.getBooleanInput as jest.Mock).mockReturnValue(true);
|
||||
getPullRequestsMock.mockReturnValue([
|
||||
{
|
||||
number: 123,
|
||||
data: {
|
||||
node_id: 'PR_node_id',
|
||||
labels: [{name: 'stale-label', node_id: 'label_node_id'}]
|
||||
},
|
||||
changedFiles: ['file.txt']
|
||||
}
|
||||
]);
|
||||
getLabelConfigsMock.mockResolvedValue({
|
||||
labelConfigs: new Map([
|
||||
[
|
||||
'stale-label',
|
||||
[{any: [{changedFiles: [{anyGlobToAnyFile: ['*.pdf']}]}]}]
|
||||
]
|
||||
]),
|
||||
changedFilesLimit: undefined
|
||||
});
|
||||
removeLabelsMock.mockRejectedValue(new Error('GraphQL request failed'));
|
||||
|
||||
await expect(labeler()).rejects.toThrow(
|
||||
"Failed to remove configured labels 'stale-label' from PR #123"
|
||||
);
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
+735
-65
@@ -1,23 +1,86 @@
|
||||
import {run} from '../src/labeler';
|
||||
import * as github from '@actions/github';
|
||||
import * as core from '@actions/core';
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
test,
|
||||
expect,
|
||||
afterAll,
|
||||
beforeEach
|
||||
} from '@jest/globals';
|
||||
import path from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import fs from 'fs';
|
||||
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('@actions/github');
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Define mock functions before mocking modules
|
||||
const addLabelsMock = jest.fn<any>();
|
||||
const addLabelsRequestMock = jest.fn<any>(options => {
|
||||
const params = {...options};
|
||||
delete params.request;
|
||||
return addLabelsMock(params);
|
||||
});
|
||||
const removeLabelsMock = jest.fn<any>();
|
||||
const setLabelsMock = jest.fn<any>();
|
||||
const reposMock = jest.fn<any>();
|
||||
const paginateMock = jest.fn<any>();
|
||||
const getPullMock = jest.fn<any>().mockResolvedValue({data: {labels: []}});
|
||||
const listFilesMergeMock = jest.fn<any>().mockReturnValue({});
|
||||
|
||||
const coreGetInputMock = jest.fn<any>();
|
||||
const coreGetMultilineInputMock = jest.fn<any>();
|
||||
const coreGetBooleanInputMock = jest.fn<any>();
|
||||
const coreErrorMock = jest.fn<any>();
|
||||
const coreWarningMock = jest.fn<any>();
|
||||
const coreSetFailedMock = jest.fn<any>();
|
||||
const setOutputSpy = jest.fn<any>();
|
||||
|
||||
const mockGithubContext: any = {
|
||||
payload: {
|
||||
pull_request: {
|
||||
number: 123,
|
||||
head: {ref: 'head-branch-name'},
|
||||
base: {ref: 'base-branch-name'}
|
||||
}
|
||||
},
|
||||
repo: {owner: 'monalisa', repo: 'helloworld'}
|
||||
};
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => ({
|
||||
getInput: coreGetInputMock,
|
||||
getMultilineInput: coreGetMultilineInputMock,
|
||||
getBooleanInput: coreGetBooleanInputMock,
|
||||
setOutput: setOutputSpy,
|
||||
setFailed: coreSetFailedMock,
|
||||
error: coreErrorMock,
|
||||
warning: coreWarningMock,
|
||||
info: jest.fn(),
|
||||
debug: jest.fn()
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('@actions/github', () => ({
|
||||
context: mockGithubContext,
|
||||
getOctokit: jest.fn(() => ({
|
||||
graphql: removeLabelsMock,
|
||||
rest: {
|
||||
issues: {
|
||||
addLabels: addLabelsRequestMock,
|
||||
setLabels: setLabelsMock
|
||||
},
|
||||
repos: {getContent: reposMock},
|
||||
pulls: {
|
||||
get: getPullMock,
|
||||
listFiles: {endpoint: {merge: listFilesMergeMock}}
|
||||
}
|
||||
},
|
||||
paginate: paginateMock
|
||||
}))
|
||||
}));
|
||||
|
||||
const {run} = await import('../src/labeler.js');
|
||||
|
||||
const gh = github.getOctokit('_');
|
||||
const setLabelsMock = jest.spyOn(gh.rest.issues, 'setLabels');
|
||||
const reposMock = jest.spyOn(gh.rest.repos, 'getContent');
|
||||
const paginateMock = jest.spyOn(gh, 'paginate');
|
||||
const getPullMock = jest.spyOn(gh.rest.pulls, 'get');
|
||||
const readFileSyncMock = jest.spyOn(fs, 'readFileSync');
|
||||
const existsSyncMock = jest.spyOn(fs, 'existsSync');
|
||||
const coreErrorMock = jest.spyOn(core, 'error');
|
||||
const coreWarningMock = jest.spyOn(core, 'warning');
|
||||
const coreSetFailedMock = jest.spyOn(core, 'setFailed');
|
||||
const setOutputSpy = jest.spyOn(core, 'setOutput');
|
||||
|
||||
class HttpError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -37,7 +100,17 @@ const yamlFixtures = {
|
||||
'branches.yml': fs.readFileSync('__tests__/fixtures/branches.yml'),
|
||||
'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml'),
|
||||
'not_supported.yml': fs.readFileSync('__tests__/fixtures/not_supported.yml'),
|
||||
'any_and_all.yml': fs.readFileSync('__tests__/fixtures/any_and_all.yml')
|
||||
'any_and_all.yml': fs.readFileSync('__tests__/fixtures/any_and_all.yml'),
|
||||
'mixed_labels.yml': fs.readFileSync('__tests__/fixtures/mixed_labels.yml'),
|
||||
'limit_0.yml': fs.readFileSync('__tests__/fixtures/limit_0.yml'),
|
||||
'limit_1.yml': fs.readFileSync('__tests__/fixtures/limit_1.yml'),
|
||||
'limit_2.yml': fs.readFileSync('__tests__/fixtures/limit_2.yml'),
|
||||
'limit_3.yml': fs.readFileSync('__tests__/fixtures/limit_3.yml'),
|
||||
'mixed_rules.yml': fs.readFileSync('__tests__/fixtures/mixed_rules.yml'),
|
||||
'max_files_5.yml': fs.readFileSync('__tests__/fixtures/max_files_5.yml'),
|
||||
'max_files_with_branch.yml': fs.readFileSync(
|
||||
'__tests__/fixtures/max_files_with_branch.yml'
|
||||
)
|
||||
};
|
||||
|
||||
const configureInput = (
|
||||
@@ -49,20 +122,27 @@ const configureInput = (
|
||||
'pr-number': string[];
|
||||
}>
|
||||
) => {
|
||||
jest
|
||||
.spyOn(core, 'getInput')
|
||||
.mockImplementation((name: string, ...opts) => mockInput[name]);
|
||||
jest
|
||||
.spyOn(core, 'getMultilineInput')
|
||||
.mockImplementation((name: string, ...opts) => mockInput[name]);
|
||||
jest
|
||||
.spyOn(core, 'getBooleanInput')
|
||||
.mockImplementation((name: string, ...opts) => mockInput[name]);
|
||||
coreGetInputMock.mockImplementation(
|
||||
(name: unknown) => mockInput[name as keyof typeof mockInput]
|
||||
);
|
||||
coreGetMultilineInputMock.mockImplementation(
|
||||
(name: unknown) => mockInput[name as keyof typeof mockInput]
|
||||
);
|
||||
coreGetBooleanInputMock.mockImplementation(
|
||||
(name: unknown) => mockInput[name as keyof typeof mockInput]
|
||||
);
|
||||
};
|
||||
|
||||
afterAll(() => jest.restoreAllMocks());
|
||||
afterAll(async () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
beforeEach(() => {
|
||||
getPullMock.mockResolvedValue({data: {labels: []}} as any);
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'head-branch-name'};
|
||||
mockGithubContext.payload.pull_request.base = {ref: 'base-branch-name'};
|
||||
});
|
||||
it('(with dot: false) adds labels to PRs that match our glob patterns', async () => {
|
||||
configureInput({});
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
@@ -75,14 +155,17 @@ describe('run', () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['touched-a-pdf-file']
|
||||
});
|
||||
expect(addLabelsRequestMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({request: {retries: 0}})
|
||||
);
|
||||
expect(setOutputSpy).toHaveBeenCalledWith(
|
||||
'new-labels',
|
||||
'touched-a-pdf-file'
|
||||
@@ -105,8 +188,8 @@ describe('run', () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
@@ -122,6 +205,29 @@ describe('run', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not lose a label added concurrently with matching labels', async () => {
|
||||
configureInput({'sync-labels': true});
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
mockGitHubResponseChangedFiles('foo.pdf');
|
||||
|
||||
const labelsOnPullRequest: string[] = [];
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {node_id: 'PR_node_id', labels: []}
|
||||
});
|
||||
addLabelsMock.mockImplementationOnce(({labels}: {labels: string[]}) => {
|
||||
labelsOnPullRequest.push('external-label', ...labels);
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(labelsOnPullRequest).toEqual([
|
||||
'external-label',
|
||||
'touched-a-pdf-file'
|
||||
]);
|
||||
expect(getPullMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with dot: false) does not add labels to PRs that do not match our glob patterns', async () => {
|
||||
configureInput({});
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
@@ -134,7 +240,7 @@ describe('run', () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
|
||||
expect(setOutputSpy).toHaveBeenCalledWith('all-labels', '');
|
||||
});
|
||||
@@ -146,7 +252,7 @@ describe('run', () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not add a label when the match config options are not supported', async () => {
|
||||
@@ -154,17 +260,17 @@ describe('run', () => {
|
||||
usingLabelerConfigYaml('not_supported.yml');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('adds labels based on the branch names that match the regexp pattern', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'test/testing-time'};
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'test/testing-time'};
|
||||
usingLabelerConfigYaml('branches.yml');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
@@ -177,14 +283,14 @@ describe('run', () => {
|
||||
|
||||
it('adds multiple labels based on branch names that match different regexp patterns', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {
|
||||
mockGithubContext.payload.pull_request.head = {
|
||||
ref: 'test/feature/123'
|
||||
};
|
||||
usingLabelerConfigYaml('branches.yml');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
@@ -203,12 +309,12 @@ describe('run', () => {
|
||||
|
||||
it('can support multiple branches by batching', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'fix/123'};
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'fix/123'};
|
||||
usingLabelerConfigYaml('branches.yml');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
@@ -221,12 +327,12 @@ describe('run', () => {
|
||||
|
||||
it('can support multiple branches by providing an array', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'array/123'};
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'array/123'};
|
||||
usingLabelerConfigYaml('branches.yml');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
@@ -243,8 +349,8 @@ describe('run', () => {
|
||||
mockGitHubResponseChangedFiles('tests/test.ts');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
@@ -261,7 +367,7 @@ describe('run', () => {
|
||||
mockGitHubResponseChangedFiles('tests/requirements.txt');
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern', async () => {
|
||||
@@ -275,19 +381,26 @@ describe('run', () => {
|
||||
mockGitHubResponseChangedFiles('foo.txt');
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {
|
||||
labels: [{name: 'touched-a-pdf-file'}, {name: 'manually-added'}]
|
||||
node_id: 'PR_node_id',
|
||||
labels: [
|
||||
{name: 'touched-a-pdf-file', node_id: 'stale_label_node_id'},
|
||||
{name: 'manually-added', node_id: 'manual_label_node_id'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['manually-added']
|
||||
});
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
expect(removeLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(removeLabelsMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('removeLabelsFromLabelable'),
|
||||
{
|
||||
labelableId: 'PR_node_id',
|
||||
labelIds: ['stale_label_node_id']
|
||||
}
|
||||
);
|
||||
expect(setLabelsMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
|
||||
expect(setOutputSpy).toHaveBeenCalledWith('all-labels', 'manually-added');
|
||||
@@ -310,7 +423,8 @@ describe('run', () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(removeLabelsMock).not.toHaveBeenCalled();
|
||||
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
|
||||
expect(setOutputSpy).toHaveBeenCalledWith(
|
||||
'all-labels',
|
||||
@@ -318,6 +432,36 @@ describe('run', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('removes stale configured labels in one mutation', async () => {
|
||||
configureInput({'sync-labels': true});
|
||||
usingLabelerConfigYaml('mixed_labels.yml');
|
||||
mockGitHubResponseChangedFiles('unrelated.txt');
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {
|
||||
node_id: 'PR_node_id',
|
||||
labels: [
|
||||
{name: 'component-a', node_id: 'label_a'},
|
||||
{name: 'component-b', node_id: 'label_b'},
|
||||
{name: 'external-label', node_id: 'external_label'}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(removeLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(removeLabelsMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('removeLabelsFromLabelable'),
|
||||
{
|
||||
labelableId: 'PR_node_id',
|
||||
labelIds: ['label_a', 'label_b']
|
||||
}
|
||||
);
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
expect(setLabelsMock).not.toHaveBeenCalled();
|
||||
expect(setOutputSpy).toHaveBeenCalledWith('all-labels', 'external-label');
|
||||
});
|
||||
|
||||
it('(with sync-labels: false) it only logs the excess labels', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
@@ -339,7 +483,7 @@ describe('run', () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
@@ -368,12 +512,12 @@ describe('run', () => {
|
||||
});
|
||||
|
||||
await run();
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 104,
|
||||
labels: ['manually-added', 'touched-a-pdf-file']
|
||||
labels: ['touched-a-pdf-file']
|
||||
});
|
||||
expect(setOutputSpy).toHaveBeenCalledWith(
|
||||
'new-labels',
|
||||
@@ -408,14 +552,14 @@ describe('run', () => {
|
||||
});
|
||||
|
||||
await run();
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(2);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(2);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 104,
|
||||
labels: ['manually-added', 'touched-a-pdf-file']
|
||||
labels: ['touched-a-pdf-file']
|
||||
});
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 150,
|
||||
@@ -431,13 +575,539 @@ describe('run', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('(with pr-number: non-numeric string) warns and makes no API call', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['abc']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'abc' is not a valid pull request number"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: negative number) warns and makes no API call', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['-1']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'-1' is not a valid pull request number"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: zero) warns and makes no API call', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['0']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'0' is not a valid pull request number"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: number with internal space) warns and makes no API call', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['10 4']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'10 4' is not a valid pull request number"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: number with trailing non-numeric chars) warns and makes no API call', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['104abc']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'104abc' is not a valid pull request number"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: valid number with surrounding whitespace) trims and processes correctly', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': [' 104 ']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
mockGitHubResponseChangedFiles('foo.pdf');
|
||||
getPullMock.mockResolvedValue(<any>{data: {labels: []}});
|
||||
|
||||
await run();
|
||||
|
||||
expect(getPullMock).toHaveBeenCalled();
|
||||
expect(coreWarningMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('is not a valid pull request number')
|
||||
);
|
||||
});
|
||||
|
||||
it('(with pr-number: string with carriage return) sanitizes CR as \\x0d in warning', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['abc\rdef']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'abc\\x0ddef' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: string with tab) sanitizes tab as \\x09 in warning', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['abc\tdef']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'abc\\x09def' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: string with ANSI escape sequence) sanitizes ESC byte as \\x1b in warning', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['abc\x1b[31mINJECTED\x1b[0m']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'abc\\x1b[31mINJECTED\\x1b[0m' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
|
||||
);
|
||||
expect(getPullMock).not.toHaveBeenCalled();
|
||||
expect(addLabelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('(with pr-number: mix of valid and invalid) processes valid, skips invalid with warning', async () => {
|
||||
configureInput({
|
||||
'repo-token': 'foo',
|
||||
'configuration-path': 'bar',
|
||||
'pr-number': ['104', 'abc']
|
||||
});
|
||||
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
mockGitHubResponseChangedFiles('foo.pdf');
|
||||
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(coreWarningMock).toHaveBeenCalledWith(
|
||||
"'abc' is not a valid pull request number"
|
||||
);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 104,
|
||||
labels: ['touched-a-pdf-file']
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add labels to PRs that have no changed files', async () => {
|
||||
usingLabelerConfigYaml('only_pdfs.yml');
|
||||
mockGitHubResponseChangedFiles();
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('changed-files-labels-limit', () => {
|
||||
it('applies all labels when count is within limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_3.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips changed-files labels when count exceeds limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// No labels should be applied since changed-files labels exceed limit
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('still applies branch-based labels when changed-files limit is exceeded', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('limit_1.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// Only the branch-based label should be applied
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['test-branch']
|
||||
});
|
||||
});
|
||||
|
||||
it('applies all labels when no limit is set', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('mixed_labels.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts',
|
||||
'components/d/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c', 'component-d']
|
||||
});
|
||||
});
|
||||
|
||||
it('does not count preexisting labels toward the limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts',
|
||||
'components/d/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: [{name: 'component-a'}, {name: 'component-b'}]}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// component-a and component-b are preexisting, so only 2 new labels (c, d) would be added
|
||||
// which equals the limit of 2, so labels should be applied
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-c', 'component-d']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips new labels when new count exceeds limit even with preexisting', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts',
|
||||
'components/d/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: [{name: 'component-a'}]}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// component-a is preexisting, so 3 new labels (b, c, d) would be added
|
||||
// which exceeds the limit of 2, so no new changed-files labels are applied
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('applies labels when new count equals the limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips all changed-files labels when limit is 0', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('limit_0.yml');
|
||||
mockGitHubResponseChangedFiles('components/a/file.ts');
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// With limit 0, only branch-based labels should be applied
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['test-branch']
|
||||
});
|
||||
});
|
||||
|
||||
it('treats labels with mixed rules as changed-files labels', async () => {
|
||||
// A label that has both branch and changed-files rules is considered
|
||||
// a "changed-files label" and subject to the limit, even if it matches
|
||||
// via the branch rule
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('mixed_rules.yml');
|
||||
mockGitHubResponseChangedFiles('unrelated/file.ts');
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// The mixed-label matches via branch rule but is still subject to limit
|
||||
// because it contains a changed-files rule in its definition.
|
||||
// Only pure-branch-label should be applied.
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['pure-branch-label']
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('max-files-changed', () => {
|
||||
it('applies labels when changed files count is within limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips file-based labels when changed files exceed limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file1.ts',
|
||||
'components/a/file2.ts',
|
||||
'components/b/file1.ts',
|
||||
'components/b/file2.ts',
|
||||
'components/c/file1.ts',
|
||||
'components/c/file2.ts' // 6 files > limit of 5
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// No labels should be applied since changed files exceed limit
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('applies labels when changed files count equals limit', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file1.ts',
|
||||
'components/a/file2.ts',
|
||||
'components/b/file1.ts',
|
||||
'components/b/file2.ts',
|
||||
'components/c/file.ts' // exactly 5 files = limit
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c']
|
||||
});
|
||||
});
|
||||
|
||||
it('still applies branch-based labels when max-files-changed is exceeded', async () => {
|
||||
configureInput({});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('max_files_with_branch.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file1.ts',
|
||||
'components/a/file2.ts',
|
||||
'components/b/file1.ts',
|
||||
'components/b/file2.ts' // 4 files > limit of 3
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// Only the branch-based label should be applied
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(addLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['test-branch']
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves preexisting changed-files labels with sync-labels when max-files-changed is exceeded', async () => {
|
||||
configureInput({'sync-labels': true});
|
||||
mockGithubContext.payload.pull_request.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'unrelated/file1.ts',
|
||||
'unrelated/file2.ts',
|
||||
'unrelated/file3.ts',
|
||||
'unrelated/file4.ts',
|
||||
'unrelated/file5.ts',
|
||||
'unrelated/file6.ts' // 6 files > limit of 5
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: [{name: 'component-a'}]} // preexisting label
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// No mutation because labels should remain unchanged
|
||||
// (component-a is preserved, not removed by sync-labels)
|
||||
expect(addLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use local configuration file if it exists', async () => {
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ inputs:
|
||||
default: '.github/labeler.yml'
|
||||
required: false
|
||||
sync-labels:
|
||||
description: 'Whether or not to remove labels when matching files are reverted'
|
||||
description: 'Whether to remove configured labels when they no longer match'
|
||||
default: false
|
||||
required: false
|
||||
dot:
|
||||
@@ -28,5 +28,5 @@ outputs:
|
||||
all-labels:
|
||||
description: 'A comma-separated list of all labels that the PR contains'
|
||||
runs:
|
||||
using: 'node20'
|
||||
using: 'node24'
|
||||
main: 'dist/index.js'
|
||||
|
||||
Vendored
+28603
-26531
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// This is a reusable configuration file copied from https://github.com/actions/reusable-workflows/tree/main/reusable-configurations. Please don't make changes to this file as it's the subject of an automatic update.
|
||||
import js from '@eslint/js';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import jest from 'eslint-plugin-jest';
|
||||
import n from 'eslint-plugin-n';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/*', '!src/**', '!__tests__/**']
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.es2015
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tsPlugin,
|
||||
n
|
||||
},
|
||||
rules: {
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-ignore': 'allow-with-description'
|
||||
}
|
||||
],
|
||||
'no-console': 'error',
|
||||
yoda: 'error',
|
||||
'prefer-const': [
|
||||
'error',
|
||||
{
|
||||
destructuring: 'all'
|
||||
}
|
||||
],
|
||||
'no-control-regex': 'off',
|
||||
'no-constant-condition': ['error', {checkLoops: false}],
|
||||
'no-undef': 'off',
|
||||
'no-useless-assignment': 'off',
|
||||
'n/no-extraneous-import': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*{test,spec}.ts'],
|
||||
plugins: {jest},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.jest
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
...jest.configs['flat/recommended'].rules,
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'jest/no-standalone-expect': 'off',
|
||||
'jest/no-conditional-expect': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
prettier
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
diagnostics: {
|
||||
ignoreCodes: [151002]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
transformIgnorePatterns: ['node_modules/(?!(@actions|@octokit)/)'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
Generated
+3048
-2405
File diff suppressed because it is too large
Load Diff
+30
-29
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "labeler",
|
||||
"version": "5.0.0",
|
||||
"version": "7.0.0",
|
||||
"description": "Labels pull requests by files altered",
|
||||
"type": "module",
|
||||
"main": "lib/main.js",
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && ncc build lib/main.js",
|
||||
"format": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --write \"**/*.{ts,yml,yaml}\"",
|
||||
"format-check": "prettier --no-error-on-unmatched-pattern --config ./.prettierrc.js --check \"**/*.{ts,yml,yaml}\"",
|
||||
"lint": "eslint --config ./.eslintrc.js \"**/*.ts\"",
|
||||
"lint:fix": "eslint --config ./.eslintrc.js \"**/*.ts\" --fix",
|
||||
"test": "jest"
|
||||
"format": "prettier --no-error-on-unmatched-pattern --write \"**/*.{ts,yml,yaml}\"",
|
||||
"format-check": "prettier --no-error-on-unmatched-pattern --check \"**/*.{ts,yml,yaml}\"",
|
||||
"lint": "eslint \"**/*.ts\"",
|
||||
"lint:fix": "eslint \"**/*.ts\" --fix",
|
||||
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,30 +28,27 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@octokit/plugin-retry": "^6.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimatch": "^10.0.1"
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/github": "^9.1.1",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"js-yaml": "^5.2.3",
|
||||
"minimatch": "^10.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^20.11.30",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@types/node": "^24.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.62.0",
|
||||
"@vercel/ncc": "^0.44.0",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jest": "^29.15.2",
|
||||
"eslint-plugin-n": "^18.1.0",
|
||||
"globals": "^17.7.0",
|
||||
"jest": "^30.4.2",
|
||||
"prettier": "^3.8.4",
|
||||
"ts-jest": "^29.4.11",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types.js';
|
||||
|
||||
const isServerError = (error: unknown): error is {status: number} =>
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'status' in error &&
|
||||
typeof error.status === 'number' &&
|
||||
error.status >= 500 &&
|
||||
error.status < 600;
|
||||
|
||||
export const addLabels = async (
|
||||
client: ClientType,
|
||||
prNumber: number,
|
||||
labels: string[]
|
||||
) => {
|
||||
const request = {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
issue_number: prNumber
|
||||
};
|
||||
|
||||
try {
|
||||
await client.rest.issues.addLabels({
|
||||
...request,
|
||||
labels,
|
||||
request: {retries: 0}
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (!isServerError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const currentLabelNames = new Set<string>();
|
||||
let page = 1;
|
||||
try {
|
||||
while (true) {
|
||||
const currentLabels = await client.rest.issues.listLabelsOnIssue({
|
||||
...request,
|
||||
per_page: 100,
|
||||
page,
|
||||
request: {retries: 0}
|
||||
});
|
||||
|
||||
for (const label of currentLabels.data) {
|
||||
currentLabelNames.add(label.name.toLowerCase());
|
||||
}
|
||||
|
||||
if (labels.every(label => currentLabelNames.has(label.toLowerCase()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentLabels.headers.link?.match(/;\s*rel="next"/)) {
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types';
|
||||
import {ClientType} from './types.js';
|
||||
|
||||
export const getChangedFiles = async (
|
||||
client: ClientType,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import {getChangedFiles} from './get-changed-files';
|
||||
import {ClientType} from './types';
|
||||
import {getChangedFiles} from './get-changed-files.js';
|
||||
import {ClientType} from './types.js';
|
||||
|
||||
export async function* getPullRequests(
|
||||
client: ClientType,
|
||||
@@ -17,7 +17,7 @@ export async function* getPullRequests(
|
||||
pull_number: prNumber
|
||||
});
|
||||
prData = result.data;
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
core.warning(`Could not find pull request #${prNumber}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types';
|
||||
import {ClientType} from './types.js';
|
||||
|
||||
export const getContent = async (
|
||||
client: ClientType,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as yaml from 'js-yaml';
|
||||
import fs from 'fs';
|
||||
import {ClientType} from './types';
|
||||
import {getContent} from './get-content';
|
||||
import {ClientType} from './types.js';
|
||||
import {getContent} from './get-content.js';
|
||||
|
||||
import {
|
||||
ChangedFilesMatchConfig,
|
||||
toChangedFilesMatchConfig
|
||||
} from '../changedFiles';
|
||||
} from '../changedFiles.js';
|
||||
|
||||
import {toBranchMatchConfig, BranchMatchConfig} from '../branch';
|
||||
import {toBranchMatchConfig, BranchMatchConfig} from '../branch.js';
|
||||
|
||||
export interface MatchConfig {
|
||||
all?: BaseMatchConfig[];
|
||||
@@ -18,12 +18,53 @@ export interface MatchConfig {
|
||||
|
||||
export type BaseMatchConfig = BranchMatchConfig & ChangedFilesMatchConfig;
|
||||
|
||||
export interface LabelConfigResult {
|
||||
labelConfigs: Map<string, MatchConfig[]>;
|
||||
changedFilesLimit?: number;
|
||||
maxFilesChanged?: number;
|
||||
}
|
||||
|
||||
const ALLOWED_CONFIG_KEYS = ['changed-files', 'head-branch', 'base-branch'];
|
||||
const TOP_LEVEL_OPTIONS = ['changed-files-labels-limit', 'max-files-changed'];
|
||||
|
||||
/**
|
||||
* Parses and validates a non-negative integer value from the configuration.
|
||||
*/
|
||||
function parseNonNegativeInteger(value: unknown, optionName: string): number {
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
||||
throw new Error(
|
||||
`Invalid value for '${optionName}': must be a non-negative integer (got ${value})`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (!/^\d+$/.test(trimmed)) {
|
||||
throw new Error(
|
||||
`Invalid value for '${optionName}': must be a non-negative integer (got '${value}')`
|
||||
);
|
||||
}
|
||||
return Number(trimmed);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
throw new Error(
|
||||
`'${optionName}' is a reserved top-level option and cannot be used as a label name. Please rename it.`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Invalid value for '${optionName}': expected a non-negative integer`
|
||||
);
|
||||
}
|
||||
|
||||
export const getLabelConfigs = (
|
||||
client: ClientType,
|
||||
configurationPath: string
|
||||
): Promise<Map<string, MatchConfig[]>> =>
|
||||
): Promise<LabelConfigResult> =>
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (!fs.existsSync(configurationPath)) {
|
||||
@@ -54,15 +95,49 @@ export const getLabelConfigs = (
|
||||
// loads (hopefully) a `{[label:string]: MatchConfig[]}`, but is `any`:
|
||||
const configObject: any = yaml.load(configuration);
|
||||
|
||||
// transform `any` => `Map<string,MatchConfig[]>` or throw if yaml is malformed:
|
||||
return getLabelConfigMapFromObject(configObject);
|
||||
// transform `any` => `LabelConfigResult` or throw if yaml is malformed:
|
||||
return getLabelConfigResultFromObject(configObject);
|
||||
});
|
||||
|
||||
export function getLabelConfigResultFromObject(
|
||||
configObject: any
|
||||
): LabelConfigResult {
|
||||
// Extract top-level options
|
||||
let changedFilesLimit: number | undefined;
|
||||
let maxFilesChanged: number | undefined;
|
||||
|
||||
const limitValue = configObject?.['changed-files-labels-limit'];
|
||||
if (limitValue !== undefined) {
|
||||
changedFilesLimit = parseNonNegativeInteger(
|
||||
limitValue,
|
||||
'changed-files-labels-limit'
|
||||
);
|
||||
}
|
||||
|
||||
const maxFilesValue = configObject?.['max-files-changed'];
|
||||
if (maxFilesValue !== undefined) {
|
||||
maxFilesChanged = parseNonNegativeInteger(
|
||||
maxFilesValue,
|
||||
'max-files-changed'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
labelConfigs: getLabelConfigMapFromObject(configObject),
|
||||
changedFilesLimit,
|
||||
maxFilesChanged
|
||||
};
|
||||
}
|
||||
|
||||
export function getLabelConfigMapFromObject(
|
||||
configObject: any
|
||||
): Map<string, MatchConfig[]> {
|
||||
const labelMap: Map<string, MatchConfig[]> = new Map();
|
||||
for (const label in configObject) {
|
||||
// Skip top-level options
|
||||
if (TOP_LEVEL_OPTIONS.includes(label)) {
|
||||
continue;
|
||||
}
|
||||
const configOptions = configObject[label];
|
||||
if (
|
||||
!Array.isArray(configOptions) ||
|
||||
@@ -124,3 +199,33 @@ export function toMatchConfig(config: any): BaseMatchConfig {
|
||||
...branchConfig
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any of the match configs for a label use changed-files patterns.
|
||||
* This is used to determine if a label should be counted toward the changed-files limit.
|
||||
*/
|
||||
export function configUsesChangedFiles(matchConfigs: MatchConfig[]): boolean {
|
||||
for (const config of matchConfigs) {
|
||||
if (config.all) {
|
||||
for (const baseConfig of config.all) {
|
||||
if (
|
||||
baseConfig.changedFiles &&
|
||||
baseConfig.changedFiles.some(cf => Object.keys(cf).length > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.any) {
|
||||
for (const baseConfig of config.any) {
|
||||
if (
|
||||
baseConfig.changedFiles &&
|
||||
baseConfig.changedFiles.some(cf => Object.keys(cf).length > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,6 +1,7 @@
|
||||
export * from './get-changed-files';
|
||||
export * from './get-changed-pull-requests';
|
||||
export * from './get-content';
|
||||
export * from './get-label-configs';
|
||||
export * from './set-labels';
|
||||
export * from './types';
|
||||
export * from './add-labels.js';
|
||||
export * from './get-changed-files.js';
|
||||
export * from './get-changed-pull-requests.js';
|
||||
export * from './get-content.js';
|
||||
export * from './get-label-configs.js';
|
||||
export * from './remove-labels.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {ClientType} from './types.js';
|
||||
|
||||
const REMOVE_LABELS_MUTATION = `
|
||||
mutation RemoveLabels($labelableId: ID!, $labelIds: [ID!]!) {
|
||||
removeLabelsFromLabelable(
|
||||
input: {labelableId: $labelableId, labelIds: $labelIds}
|
||||
) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const removeLabels = async (
|
||||
client: ClientType,
|
||||
labelableId: string,
|
||||
labelIds: string[]
|
||||
) => {
|
||||
await client.graphql(REMOVE_LABELS_MUTATION, {labelableId, labelIds});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as github from '@actions/github';
|
||||
import {ClientType} from './types';
|
||||
|
||||
export const setLabels = async (
|
||||
client: ClientType,
|
||||
prNumber: number,
|
||||
labels: string[]
|
||||
) => {
|
||||
await client.rest.issues.setLabels({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
labels: labels
|
||||
});
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import {Minimatch} from 'minimatch';
|
||||
import {printPattern, isObject, kebabToCamel} from './utils';
|
||||
import {printPattern, isObject, kebabToCamel} from './utils.js';
|
||||
|
||||
export interface ChangedFilesMatchConfig {
|
||||
changedFiles?: ChangedFilesGlobPatternsConfig[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as core from '@actions/core';
|
||||
import {getPrNumbers} from './get-pr-numbers';
|
||||
import {getPrNumbers} from './get-pr-numbers.js';
|
||||
|
||||
export const getInputs = () => ({
|
||||
token: core.getInput('repo-token'),
|
||||
|
||||
@@ -4,6 +4,13 @@ import * as github from '@actions/github';
|
||||
const getPrNumberFromContext = () =>
|
||||
github.context.payload.pull_request?.number;
|
||||
|
||||
const sanitizeForWarning = (value: string): string => {
|
||||
return value.replace(
|
||||
/[\x00-\x1F\x7F-\x9F]/g,
|
||||
c => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`
|
||||
);
|
||||
};
|
||||
|
||||
export const getPrNumbers = (): number[] => {
|
||||
const prInput = core.getMultilineInput('pr-number');
|
||||
|
||||
@@ -14,10 +21,16 @@ export const getPrNumbers = (): number[] => {
|
||||
const result: number[] = [];
|
||||
|
||||
for (const line of prInput) {
|
||||
const prNumber = parseInt(line, 10);
|
||||
const trimmed = line.trim();
|
||||
const prNumber = parseInt(trimmed, 10);
|
||||
|
||||
if (isNaN(prNumber) && prNumber <= 0) {
|
||||
core.warning(`'${prNumber}' is not a valid pull request number`);
|
||||
if (isNaN(prNumber) || prNumber <= 0 || String(prNumber) !== trimmed) {
|
||||
const sanitized = sanitizeForWarning(line);
|
||||
const hint =
|
||||
sanitized !== line
|
||||
? ' (non-printable characters were escaped as \\xNN)'
|
||||
: '';
|
||||
core.warning(`'${sanitized}' is not a valid pull request number${hint}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './get-inputs';
|
||||
export * from './get-inputs.js';
|
||||
|
||||
+110
-23
@@ -1,15 +1,18 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import * as pluginRetry from '@octokit/plugin-retry';
|
||||
import * as api from './api';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import {getInputs} from './get-inputs';
|
||||
import * as api from './api/index.js';
|
||||
import {getInputs} from './get-inputs/index.js';
|
||||
|
||||
import {BaseMatchConfig, MatchConfig} from './api/get-label-configs';
|
||||
import {
|
||||
BaseMatchConfig,
|
||||
MatchConfig,
|
||||
configUsesChangedFiles
|
||||
} from './api/get-label-configs.js';
|
||||
|
||||
import {checkAllChangedFiles, checkAnyChangedFiles} from './changedFiles';
|
||||
import {checkAllChangedFiles, checkAnyChangedFiles} from './changedFiles.js';
|
||||
|
||||
import {checkAnyBranch, checkAllBranch} from './branch';
|
||||
import {checkAnyBranch, checkAllBranch} from './branch.js';
|
||||
|
||||
type ClientType = ReturnType<typeof github.getOctokit>;
|
||||
|
||||
@@ -22,7 +25,7 @@ export const run = () =>
|
||||
core.setFailed(error.message);
|
||||
});
|
||||
|
||||
async function labeler() {
|
||||
export async function labeler() {
|
||||
const {token, configPath, syncLabels, dot, prNumbers} = getInputs();
|
||||
|
||||
if (!prNumbers.length) {
|
||||
@@ -35,56 +38,140 @@ async function labeler() {
|
||||
const pullRequests = api.getPullRequests(client, prNumbers);
|
||||
|
||||
for await (const pullRequest of pullRequests) {
|
||||
const labelConfigs: Map<string, MatchConfig[]> = await api.getLabelConfigs(
|
||||
client,
|
||||
configPath
|
||||
);
|
||||
const {labelConfigs, changedFilesLimit, maxFilesChanged} =
|
||||
await api.getLabelConfigs(client, configPath);
|
||||
|
||||
// Check if total changed files exceeds the max-files-changed threshold
|
||||
const skipChangedFilesLabeling =
|
||||
maxFilesChanged !== undefined &&
|
||||
pullRequest.changedFiles.length > maxFilesChanged;
|
||||
|
||||
if (skipChangedFilesLabeling) {
|
||||
core.info(
|
||||
`Total changed files (${pullRequest.changedFiles.length}) exceeds max-files-changed (${maxFilesChanged}), skipping file-based labeling`
|
||||
);
|
||||
}
|
||||
|
||||
const preexistingLabels = pullRequest.data.labels.map(l => l.name);
|
||||
const allLabels: Set<string> = new Set<string>(preexistingLabels);
|
||||
|
||||
// Track labels that would be added based on changed-files patterns
|
||||
const changedFilesLabels: Set<string> = new Set<string>();
|
||||
|
||||
for (const [label, configs] of labelConfigs.entries()) {
|
||||
core.debug(`processing ${label}`);
|
||||
|
||||
// If this config uses changed-files and we're skipping file-based labeling,
|
||||
// don't evaluate it at all (skip add/remove) to preserve preexisting labels
|
||||
const usesChangedFiles = configUsesChangedFiles(configs);
|
||||
if (skipChangedFilesLabeling && usesChangedFiles) {
|
||||
core.debug(
|
||||
`skipping ${label} (uses changed-files and max-files-changed exceeded)`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkMatchConfigs(pullRequest.changedFiles, configs, dot)) {
|
||||
allLabels.add(label);
|
||||
// Track if this label uses changed-files patterns
|
||||
if (usesChangedFiles) {
|
||||
changedFilesLabels.add(label);
|
||||
}
|
||||
} else if (syncLabels) {
|
||||
allLabels.delete(label);
|
||||
}
|
||||
}
|
||||
|
||||
const labelsToAdd = [...allLabels].slice(0, GITHUB_MAX_LABELS);
|
||||
// Check if changed-files labels should be skipped due to labels limit
|
||||
const newChangedFilesLabels = [...changedFilesLabels].filter(
|
||||
l => !preexistingLabels.includes(l)
|
||||
);
|
||||
|
||||
if (
|
||||
changedFilesLimit !== undefined &&
|
||||
newChangedFilesLabels.length > changedFilesLimit
|
||||
) {
|
||||
core.info(
|
||||
`Changed-files labels (${newChangedFilesLabels.length}) exceed limit (${changedFilesLimit}), skipping: ${newChangedFilesLabels.join(', ')}`
|
||||
);
|
||||
// Remove all new changed-files labels
|
||||
for (const label of newChangedFilesLabels) {
|
||||
allLabels.delete(label);
|
||||
}
|
||||
}
|
||||
|
||||
const labelsToApply = [...allLabels].slice(0, GITHUB_MAX_LABELS);
|
||||
const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
|
||||
|
||||
let newLabels: string[] = [];
|
||||
const finalLabels = labelsToApply;
|
||||
const newLabels = labelsToApply.filter(
|
||||
label => !preexistingLabels.includes(label)
|
||||
);
|
||||
const staleLabels = pullRequest.data.labels.filter(
|
||||
label => labelConfigs.has(label.name) && !allLabels.has(label.name)
|
||||
);
|
||||
|
||||
try {
|
||||
if (!isEqual(labelsToAdd, preexistingLabels)) {
|
||||
await api.setLabels(client, pullRequest.number, labelsToAdd);
|
||||
newLabels = labelsToAdd.filter(
|
||||
label => !preexistingLabels.includes(label)
|
||||
);
|
||||
if (staleLabels.length) {
|
||||
const labelableId = pullRequest.data.node_id;
|
||||
const missingNodeId = staleLabels.find(label => !label.node_id);
|
||||
if (!labelableId || missingNodeId) {
|
||||
throw new Error(
|
||||
`Failed to resolve node IDs while removing configured labels from PR #${pullRequest.number}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await api.removeLabels(
|
||||
client,
|
||||
labelableId,
|
||||
staleLabels.map(label => label.node_id)
|
||||
);
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Failed to remove configured labels '${staleLabels.map(label => label.name).join("', '")}' from PR #${pullRequest.number}`,
|
||||
{cause: error}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (newLabels.length) {
|
||||
await api.addLabels(client, pullRequest.number, newLabels);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const apiError = error.cause ?? error;
|
||||
if (
|
||||
error.name !== 'HttpError' ||
|
||||
error.message !== 'Resource not accessible by integration'
|
||||
apiError.name === 'HttpError' &&
|
||||
apiError.status === 403 &&
|
||||
apiError.message.toLowerCase().includes('unauthorized')
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to update labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
|
||||
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`,
|
||||
{cause: error}
|
||||
);
|
||||
} else if (
|
||||
apiError.name !== 'HttpError' ||
|
||||
apiError.message !== 'Resource not accessible by integration'
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
core.warning(
|
||||
`The action requires write permission to add labels to pull requests. For more information please refer to the action documentation: https://github.com/actions/labeler#permissions`,
|
||||
`The action requires 'issues: write' permission to create new labels or 'pull-requests: write' permission to add existing labels to pull requests. ` +
|
||||
`For more information, refer to the action documentation: https://github.com/actions/labeler#recommended-permissions`,
|
||||
{
|
||||
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github.context.eventName}' is misconfigured`
|
||||
}
|
||||
);
|
||||
|
||||
core.setFailed(error.message);
|
||||
core.setFailed(apiError.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('new-labels', newLabels.join(','));
|
||||
core.setOutput('all-labels', labelsToAdd.join(','));
|
||||
core.setOutput('all-labels', finalLabels.join(','));
|
||||
|
||||
if (excessLabels.length) {
|
||||
core.warning(
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import {run} from './labeler';
|
||||
import {run} from './labeler.js';
|
||||
|
||||
run();
|
||||
|
||||
+5
-5
@@ -2,8 +2,9 @@
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"target": "ES2022", /* Emit ES2022-compatible JavaScript. */
|
||||
"module": "NodeNext", /* Use Node.js ESM module semantics. */
|
||||
"moduleResolution": "NodeNext", /* Resolve modules using Node.js ESM rules. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
@@ -38,7 +39,7 @@
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node16' or 'NodeNext'. */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
@@ -46,7 +47,6 @@
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
@@ -59,5 +59,5 @@
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"exclude": ["node_modules", "__tests__", "__mocks__"]
|
||||
"exclude": ["node_modules", "__tests__", "__mocks__", "jest.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user