Compare commits

...
4 Commits
Author SHA1 Message Date
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>chiranjib-swain
98ce1450c7 Bump js-yaml from 5.2.2 to 5.2.3 (#967)
* Bump js-yaml from 5.2.2 to 5.2.3

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 5.2.2 to 5.2.3.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump js-yaml to version 5.2.3 and update package-lock.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: chiranjib-swain <chiranjib-swain@github.com>
2026-08-26 15:39:26 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>chiranjib-swain
2c2a2313b2 Bump js-yaml, brace-expansion, and undici to latest releases (#958)
* Bump js-yaml from 5.1.0 to 5.2.2

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 5.1.0 to 5.2.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.1.0...5.2.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: update dependency files for js-yaml, undici, and brace-expansion

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: chiranjib-swain <chiranjib-swain@github.com>
2026-08-06 15:02:16 -05:00
b7a8804475 Preserve externally managed labels during synchronization (#957)
* Preserve externally managed pull request labels

Replace whole-set label writes with batched selective additions and removals so sync-labels only manages configured labels.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182

* Reconcile ambiguous label addition failures

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182

* Handle paginated label reconciliation

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182

---------

Copilot-Session: 44452010-1cd5-4e90-8abe-f03547c04182
2026-07-30 21:51:30 -05:00
Chiranjib SwainandGitHub bf12e9b00b feat: migrate to ESM and update dependencies (#949)
* feat: migrate to ESM and update dependencies

- Migrated the project to ESM to support the latest `@actions/*` package versions.
- Updated dependencies to their latest versions, including `@actions/core`, `@actions/github`, and others.
- Replaced `.prettierrc.js` with `.prettierrc.json` for configuration.
- Updated ESLint configuration to use ESM and added new rules.
- Refactored test files to use `jest` with ESM imports.
- Removed deprecated files and updated import paths to use `.js` extensions.
- Added new files for improved configuration and removed unnecessary mocks.
- Updated README to reflect breaking changes in version 7.

* docs: update README to clarify ESM migration details and adjust TypeScript configuration
2026-07-16 11:04:03 -05:00
66 changed files with 15836 additions and 18358 deletions
-7
View File
@@ -1,7 +0,0 @@
# Ignore list
/*
# Do not ignore these folders:
!__tests__/
!__mocks__/
!src/
-51
View File
@@ -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
}
};
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/core"
version: 2.0.3
version: 3.0.1
type: npm
summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/exec"
version: 2.0.0
version: 3.0.0
type: npm
summary: Actions exec lib
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/github"
version: 6.0.1
version: 9.1.1
type: npm
summary: Actions github lib
homepage: https://github.com/actions/toolkit/tree/main/packages/github
@@ -1,6 +1,6 @@
---
name: "@actions/http-client"
version: 2.2.3
version: 4.0.1
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@actions/io"
version: 2.0.0
version: 3.0.2
type: npm
summary: Actions io lib
homepage: https://github.com/actions/toolkit/tree/main/packages/io
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@octokit/endpoint"
version: 9.0.6
version: 11.0.3
type: npm
summary: Turns REST API endpoints into generic request options
homepage:
+2 -2
View File
@@ -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
-20
View File
@@ -1,20 +0,0 @@
---
name: "@octokit/openapi-types"
version: 19.0.1
type: npm
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
homepage:
license: mit
licenses:
- sources: LICENSE
text: |-
Copyright 2020 Gregor Martynus
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)"
notices: []
-20
View File
@@ -1,20 +0,0 @@
---
name: "@octokit/openapi-types"
version: 23.0.1
type: npm
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
homepage:
license: mit
licenses:
- sources: LICENSE
text: |-
Copyright 2020 Gregor Martynus
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)"
notices: []
-20
View File
@@ -1,20 +0,0 @@
---
name: "@octokit/openapi-types"
version: 24.2.0
type: npm
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
homepage:
license: mit
licenses:
- sources: LICENSE
text: |-
Copyright 2020 Gregor Martynus
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)"
notices: []
@@ -1,14 +1,14 @@
---
name: "@octokit/openapi-types"
version: 20.0.0
version: 27.0.0
type: npm
summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com
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:
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@octokit/plugin-paginate-rest"
version: 9.2.2
version: 14.0.0
type: npm
summary: Octokit plugin to paginate REST API endpoint responses
homepage:
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@octokit/plugin-rest-endpoint-methods"
version: 10.4.1
version: 17.0.0
type: npm
summary: Octokit plugin adding one method for all of api.github.com REST API endpoints
homepage:
+2 -2
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@octokit/request-error"
version: 5.1.1
version: 7.1.0
type: npm
summary: Error class for Octokit request errors
homepage:
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: "@octokit/request"
version: 8.4.1
version: 10.0.10
type: npm
summary: Send parameterized requests to GitHub's APIs with sensible defaults in browsers
and Node
-20
View File
@@ -1,20 +0,0 @@
---
name: "@octokit/types"
version: 12.6.0
type: npm
summary: Shared TypeScript definitions for Octokit projects
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
MIT License Copyright (c) 2019 Octokit 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 (including the next paragraph) 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)"
notices: []
-20
View File
@@ -1,20 +0,0 @@
---
name: "@octokit/types"
version: 13.10.0
type: npm
summary: Shared TypeScript definitions for Octokit projects
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
MIT License Copyright (c) 2019 Octokit 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 (including the next paragraph) 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)"
notices: []
-20
View File
@@ -1,20 +0,0 @@
---
name: "@octokit/types"
version: 13.8.0
type: npm
summary: Shared TypeScript definitions for Octokit projects
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
MIT License Copyright (c) 2019 Octokit 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 (including the next paragraph) 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)"
notices: []
@@ -1,6 +1,6 @@
---
name: "@octokit/types"
version: 12.1.0
version: 16.0.0
type: npm
summary: Shared TypeScript definitions for Octokit projects
homepage:
+2 -2
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: brace-expansion
version: 5.0.7
version: 5.0.9
type: npm
summary: Brace expansion as known from sh/bash
homepage:
+47
View File
@@ -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: []
-28
View File
@@ -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: []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: js-yaml
version: 4.2.0
version: 5.2.3
type: npm
summary: YAML 1.2 parser and serializer
homepage:
+33
View File
@@ -0,0 +1,33 @@
---
name: json-with-bigint
version: 3.5.8
type: npm
summary: JS library that allows you to easily serialize and deserialize data with
BigInt values
homepage:
license: mit
licenses:
- sources: LICENSE
text: |
MIT License
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
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: []
-58
View File
@@ -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: []
-26
View File
@@ -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: []
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: undici
version: 6.27.0
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
View File
@@ -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.
-26
View File
@@ -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: []
-11
View File
@@ -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'
};
+10
View File
@@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid"
}
+24 -1
View File
@@ -4,6 +4,10 @@
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.
@@ -270,10 +274,24 @@ 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:
@@ -324,6 +342,11 @@ Labeler provides the following outputs:
| `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"
-43
View File
@@ -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);
+131
View File
@@ -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
View File
@@ -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']
});
+25 -9
View File
@@ -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']}]
});
});
+89 -19
View File
@@ -1,26 +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 api from '../src/api';
import {labeler} from '../src/labeler';
import * as github from '@actions/github';
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,
getLabelConfigResultFromObject,
BaseMatchConfig,
configUsesChangedFiles
} from '../src/api/get-label-configs';
jest.mock('@actions/core');
jest.mock('../src/api');
} = 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) => {
@@ -422,14 +463,14 @@ describe('labeler error handling', () => {
jest.resetAllMocks();
(github.getOctokit as jest.Mock).mockReturnValue(mockClient);
(api.getPullRequests as jest.Mock).mockReturnValue([
getPullRequestsMock.mockReturnValue([
{
...mockPullRequest,
data: {labels: [{name: 'old-label'}]}
}
]);
(api.getLabelConfigs as jest.Mock).mockResolvedValue({
getLabelConfigsMock.mockResolvedValue({
labelConfigs: new Map([['new-label', ['dummy-config']]]),
changedFilesLimit: undefined
});
@@ -439,7 +480,7 @@ describe('labeler error handling', () => {
});
it('throws a custom error for HttpError 403 with "unauthorized" message', async () => {
(api.setLabels as jest.Mock).mockRejectedValue({
addLabelsMock.mockRejectedValue({
name: 'HttpError',
status: 403,
message: 'Request failed with status code 403: Unauthorized'
@@ -456,7 +497,7 @@ describe('labeler error handling', () => {
status: 404,
message: 'Not Found'
};
(api.setLabels as jest.Mock).mockRejectedValue(unexpectedError);
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`.
@@ -469,7 +510,7 @@ describe('labeler error handling', () => {
name: 'HttpError',
message: 'Resource not accessible by integration'
};
(api.setLabels as jest.Mock).mockRejectedValue(error);
addLabelsMock.mockRejectedValue(error);
await labeler();
@@ -479,4 +520,33 @@ describe('labeler error handling', () => {
);
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();
});
});
+248 -114
View File
@@ -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) {
@@ -59,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');
@@ -85,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'
@@ -115,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,
@@ -132,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');
@@ -144,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', '');
});
@@ -156,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 () => {
@@ -164,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,
@@ -187,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,
@@ -213,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,
@@ -231,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,
@@ -253,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,
@@ -271,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 () => {
@@ -285,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');
@@ -320,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',
@@ -328,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',
@@ -349,7 +483,7 @@ describe('run', () => {
await run();
expect(setLabelsMock).toHaveBeenCalledTimes(0);
expect(addLabelsMock).toHaveBeenCalledTimes(0);
expect(coreWarningMock).toHaveBeenCalledTimes(1);
expect(coreWarningMock).toHaveBeenCalledWith(
@@ -378,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',
@@ -418,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,
@@ -456,7 +590,7 @@ describe('run', () => {
"'abc' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: negative number) warns and makes no API call', async () => {
@@ -474,7 +608,7 @@ describe('run', () => {
"'-1' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: zero) warns and makes no API call', async () => {
@@ -492,7 +626,7 @@ describe('run', () => {
"'0' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: number with internal space) warns and makes no API call', async () => {
@@ -510,7 +644,7 @@ describe('run', () => {
"'10 4' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: number with trailing non-numeric chars) warns and makes no API call', async () => {
@@ -528,7 +662,7 @@ describe('run', () => {
"'104abc' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: valid number with surrounding whitespace) trims and processes correctly', async () => {
@@ -565,7 +699,7 @@ describe('run', () => {
"'abc\\x0ddef' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: string with tab) sanitizes tab as \\x09 in warning', async () => {
@@ -583,7 +717,7 @@ describe('run', () => {
"'abc\\x09def' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: string with ANSI escape sequence) sanitizes ESC byte as \\x1b in warning', async () => {
@@ -601,7 +735,7 @@ describe('run', () => {
"'abc\\x1b[31mINJECTED\\x1b[0m' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
);
expect(getPullMock).not.toHaveBeenCalled();
expect(setLabelsMock).not.toHaveBeenCalled();
expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: mix of valid and invalid) processes valid, skips invalid with warning', async () => {
@@ -623,8 +757,8 @@ describe('run', () => {
expect(coreWarningMock).toHaveBeenCalledWith(
"'abc' is not a valid pull request number"
);
expect(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
expect(addLabelsMock).toHaveBeenCalledTimes(1);
expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 104,
@@ -638,13 +772,13 @@ describe('run', () => {
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({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('limit_3.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -656,8 +790,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,
@@ -667,7 +801,7 @@ describe('run', () => {
it('skips changed-files labels when count exceeds limit', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('limit_2.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -681,12 +815,12 @@ describe('run', () => {
await run();
// No labels should be applied since changed-files labels exceed limit
expect(setLabelsMock).toHaveBeenCalledTimes(0);
expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('still applies branch-based labels when changed-files limit is exceeded', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
usingLabelerConfigYaml('limit_1.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -700,8 +834,8 @@ describe('run', () => {
await run();
// Only the branch-based label should be applied
expect(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
expect(addLabelsMock).toHaveBeenCalledTimes(1);
expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -711,7 +845,7 @@ describe('run', () => {
it('applies all labels when no limit is set', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('mixed_labels.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -725,8 +859,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,
@@ -736,7 +870,7 @@ describe('run', () => {
it('does not count preexisting labels toward the limit', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('limit_2.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -752,18 +886,18 @@ describe('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(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
expect(addLabelsMock).toHaveBeenCalledTimes(1);
expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
labels: ['component-a', 'component-b', 'component-c', 'component-d']
labels: ['component-c', 'component-d']
});
});
it('skips new labels when new count exceeds limit even with preexisting', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('limit_2.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -779,12 +913,12 @@ describe('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(setLabelsMock).toHaveBeenCalledTimes(0);
expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('applies labels when new count equals the limit', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('limit_2.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -796,8 +930,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,
@@ -807,7 +941,7 @@ describe('run', () => {
it('skips all changed-files labels when limit is 0', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
usingLabelerConfigYaml('limit_0.yml');
mockGitHubResponseChangedFiles('components/a/file.ts');
getPullMock.mockResolvedValue(<any>{
@@ -817,8 +951,8 @@ describe('run', () => {
await run();
// With limit 0, only branch-based labels should be applied
expect(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
expect(addLabelsMock).toHaveBeenCalledTimes(1);
expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -831,7 +965,7 @@ describe('run', () => {
// a "changed-files label" and subject to the limit, even if it matches
// via the branch rule
configureInput({});
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
usingLabelerConfigYaml('mixed_rules.yml');
mockGitHubResponseChangedFiles('unrelated/file.ts');
getPullMock.mockResolvedValue(<any>{
@@ -843,8 +977,8 @@ describe('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(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
expect(addLabelsMock).toHaveBeenCalledTimes(1);
expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -856,7 +990,7 @@ describe('run', () => {
describe('max-files-changed', () => {
it('applies labels when changed files count is within limit', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('max_files_5.yml');
mockGitHubResponseChangedFiles(
'components/a/file.ts',
@@ -869,8 +1003,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,
@@ -880,7 +1014,7 @@ describe('run', () => {
it('skips file-based labels when changed files exceed limit', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('max_files_5.yml');
mockGitHubResponseChangedFiles(
'components/a/file1.ts',
@@ -897,12 +1031,12 @@ describe('run', () => {
await run();
// No labels should be applied since changed files exceed limit
expect(setLabelsMock).toHaveBeenCalledTimes(0);
expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('applies labels when changed files count equals limit', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('max_files_5.yml');
mockGitHubResponseChangedFiles(
'components/a/file1.ts',
@@ -917,8 +1051,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,
@@ -928,7 +1062,7 @@ describe('run', () => {
it('still applies branch-based labels when max-files-changed is exceeded', async () => {
configureInput({});
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
mockGithubContext.payload.pull_request.head = {ref: 'test/some-feature'};
usingLabelerConfigYaml('max_files_with_branch.yml');
mockGitHubResponseChangedFiles(
'components/a/file1.ts',
@@ -943,8 +1077,8 @@ describe('run', () => {
await run();
// Only the branch-based label should be applied
expect(setLabelsMock).toHaveBeenCalledTimes(1);
expect(setLabelsMock).toHaveBeenCalledWith({
expect(addLabelsMock).toHaveBeenCalledTimes(1);
expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -954,7 +1088,7 @@ describe('run', () => {
it('preserves preexisting changed-files labels with sync-labels when max-files-changed is exceeded', async () => {
configureInput({'sync-labels': true});
github.context.payload.pull_request!.head = {ref: 'main'};
mockGithubContext.payload.pull_request.head = {ref: 'main'};
usingLabelerConfigYaml('max_files_5.yml');
mockGitHubResponseChangedFiles(
'unrelated/file1.ts',
@@ -970,9 +1104,9 @@ describe('run', () => {
await run();
// No setLabels call because labels should remain unchanged
// No mutation because labels should remain unchanged
// (component-a is preserved, not removed by sync-labels)
expect(setLabelsMock).toHaveBeenCalledTimes(0);
expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
});
+1 -1
View File
@@ -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:
+12256 -15896
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+74
View File
@@ -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
];
-10
View File
@@ -1,10 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
+23
View File
@@ -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
}
+2627 -1782
View File
File diff suppressed because it is too large Load Diff
+26 -30
View File
@@ -1,18 +1,19 @@
{
"name": "labeler",
"version": "6.2.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",
@@ -27,32 +28,27 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/core": "^2.0.3",
"@actions/github": "^6.0.1",
"@octokit/plugin-retry": "^6.0.0",
"js-yaml": "^4.2.0",
"lodash.isequal": "^4.5.0",
"@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"
},
"overrides": {
"undici": "6.27.0"
},
"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": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.59.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
"@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"
}
}
+65
View File
@@ -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 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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[];
+7 -6
View File
@@ -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';
+19
View File
@@ -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});
};
-15
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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'),
+1 -1
View File
@@ -1 +1 @@
export * from './get-inputs';
export * from './get-inputs.js';
+44 -38
View File
@@ -1,19 +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,
configUsesChangedFiles
} from './api/get-label-configs';
} 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>;
@@ -104,49 +103,56 @@ export async function labeler() {
const labelsToApply = [...allLabels].slice(0, GITHUB_MAX_LABELS);
const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
let finalLabels = labelsToApply;
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(labelsToApply, preexistingLabels)) {
// Fetch the latest labels for the PR
const latestLabels: string[] = [];
// Skip fetching real labels when running tests (uses mock data instead)
if (process.env.NODE_ENV !== 'test') {
const pr = await client.rest.pulls.get({
...github.context.repo,
pull_number: pullRequest.number
});
latestLabels.push(...pr.data.labels.map(l => l.name).filter(Boolean));
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}`
);
}
// Labels added manually during the run (not in first snapshot)
const manualAddedDuringRun = latestLabels.filter(
l => !preexistingLabels.includes(l)
);
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}
);
}
}
// Preserve manual labels first, then apply config-based labels, respecting GitHub's 100-label limit
finalLabels = [
...new Set([...manualAddedDuringRun, ...labelsToApply])
].slice(0, GITHUB_MAX_LABELS);
await api.setLabels(client, pullRequest.number, finalLabels);
newLabels = finalLabels.filter(l => !preexistingLabels.includes(l));
if (newLabels.length) {
await api.addLabels(client, pullRequest.number, newLabels);
}
} catch (error: any) {
const apiError = error.cause ?? error;
if (
error.name === 'HttpError' &&
error.status === 403 &&
error.message.toLowerCase().includes('unauthorized')
apiError.name === 'HttpError' &&
apiError.status === 403 &&
apiError.message.toLowerCase().includes('unauthorized')
) {
throw new Error(
`Failed to set 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.`
`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 (
error.name !== 'HttpError' ||
error.message !== 'Resource not accessible by integration'
apiError.name !== 'HttpError' ||
apiError.message !== 'Resource not accessible by integration'
) {
throw error;
}
@@ -159,7 +165,7 @@ export async function labeler() {
}
);
core.setFailed(error.message);
core.setFailed(apiError.message);
return;
}
+1 -1
View File
@@ -1,3 +1,3 @@
import {run} from './labeler';
import {run} from './labeler.js';
run();
+5 -5
View File
@@ -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"]
}