19 Commits
v2.2 ... main

Author SHA1 Message Date
dependabot[bot]
f709bf4521 chore(deps-dev): bump the npm-development group with 6 updates (#92) 2026-01-19 00:05:11 +00:00
dependabot[bot]
713ef57d6a chore(deps): bump the npm-production group with 2 updates (#93) 2026-01-18 23:51:40 +00:00
dependabot[bot]
9faf3c9e39 chore(deps): bump the actions group with 2 updates (#94) 2026-01-18 23:50:34 +00:00
dependabot[bot]
2a6efacda6 chore(deps): bump ruby/setup-ruby from 1.278.0 to 1.280.0 in the actions group (#91) 2026-01-11 16:05:41 +00:00
dependabot[bot]
00f9f5b105 chore(deps-dev): bump the npm-development group with 4 updates (#90) 2026-01-11 16:05:18 +00:00
dependabot[bot]
b159afa635 chore(deps-dev): bump the npm-development group with 3 updates (#87)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 00:22:50 +00:00
dependabot[bot]
e9e6aa35a8 chore(deps): bump ruby/setup-ruby from 1.275.0 to 1.278.0 in the actions group (#88)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 00:22:15 +00:00
d92b2d6009 ci(release/tag): use new derive_from input (#89) 2026-01-02 00:21:59 +00:00
dependabot[bot]
9debb9deac chore(deps-dev): bump the npm-development group with 3 updates (#85) 2025-12-25 13:11:35 +00:00
dependabot[bot]
36243fb9d3 chore(deps): bump jimeh/update-tags-action from 2.2.0 to 2.3.0 in the actions group (#86) 2025-12-25 13:00:31 +00:00
jimehbot[bot]
98d6d448de chore(main): release 2.3.0 (#78)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-12-24 00:59:00 +00:00
fee5cc2ff2 chore(readme): strip out all release-please update tags (#84) 2025-12-24 00:54:17 +00:00
facf303096 chore(readme): fix version string bumps done by release-please (#83) 2025-12-24 00:33:14 +00:00
5dc277d712 chore(readme): tweak version numbering in examples (#82) 2025-12-24 00:23:39 +00:00
32e66b04be feat(tags): support per-tag annotation overrides (#81) 2025-12-24 00:12:13 +00:00
1d171e9f3c feat(tags/derive): add semver tag derivation functionality (#79) 2025-12-23 23:33:54 +00:00
6adab3a060 chore(AGENTS.md): add license compliance instructions for npm dependencies (#80) 2025-12-23 22:45:43 +00:00
ef41989077 feat(inputs): add dry run support (#76) 2025-12-23 21:31:53 +00:00
bb3e44be2f style(ci/dependabot-rebuild): apply prettier formatting to workflow YAML (#77) 2025-12-23 21:28:52 +00:00
34 changed files with 14303 additions and 1493 deletions

View File

@@ -1,3 +1,3 @@
{
".": "2.2.1"
".": "2.3.0"
}

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .node-version
cache: npm
@@ -54,7 +54,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .node-version
cache: npm
@@ -67,7 +67,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .node-version
cache: npm
@@ -79,8 +79,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
outputs:
release_created: ${{ steps.release-please.outputs.release_created }}
major: ${{ steps.release-please.outputs.major }}
minor: ${{ steps.release-please.outputs.minor }}
tag_name: ${{ steps.release-please.outputs.tag_name }}
steps:
- uses: jimeh/release-please-manifest-action@84f33fd2828210488c36f3e0a7e3209252d2ae7d # v3.0.0
id: release-please
@@ -95,8 +94,6 @@ jobs:
permissions:
contents: write
steps:
- uses: jimeh/update-tags-action@eecd8caae9a536ed536cff9b2b7f0bd187f67c13 # v2.2.0
- uses: jimeh/update-tags-action@98d6d448de7488acb52bd8db115673541d019521 # v2.3.0
with:
tags: |
v${{ needs.release-please.outputs.major }}
v${{ needs.release-please.outputs.major }}.${{ needs.release-please.outputs.minor }}
derive_from: ${{ needs.release-please.outputs.tag_name }}

View File

@@ -16,12 +16,12 @@ jobs:
contents: read
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .node-version
cache: npm
- run: npm ci
- uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0
- uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
with:
ruby-version: ruby
bundler-cache: true

View File

@@ -12,7 +12,8 @@ permissions:
jobs:
rebuild:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' && github.event.sender.login == 'dependabot[bot]' }}
if: |-
${{ github.actor == 'dependabot[bot]' && github.event.sender.login == 'dependabot[bot]' }}
steps:
- name: Generate app token
id: app-token
@@ -25,7 +26,7 @@ jobs:
token: ${{ steps.app-token.outputs.token }}
# Update and push dist if changed.
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .node-version
cache: npm
@@ -43,7 +44,7 @@ jobs:
chore: rebuild dist
# Update and push .licenses if changed.
- uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0
- uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
with:
ruby-version: ruby
bundler-cache: true

View File

@@ -32,12 +32,12 @@ jobs:
token: ${{ steps.app-token.outputs.token }}
# Setup Node and Ruby runtimes.
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .node-version
cache: npm
- run: npm ci
- uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0
- uses: ruby/setup-ruby@708024e6c902387ab41de36e1669e43b5ee7085e # v1.283.0
with:
ruby-version: ruby
bundler-cache: true

View File

@@ -1,6 +1,6 @@
---
name: "@actions/core"
version: 2.0.1
version: 2.0.2
type: npm
summary: Actions core lib
homepage: https://github.com/actions/toolkit/tree/main/packages/core

View File

@@ -1,6 +1,6 @@
---
name: "@actions/github"
version: 6.0.1
version: 7.0.0
type: npm
summary: Actions github lib
homepage: https://github.com/actions/toolkit/tree/main/packages/github

View File

@@ -1,32 +0,0 @@
---
name: "@actions/http-client"
version: 3.0.0
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: []

View File

@@ -1,6 +1,6 @@
---
name: "@actions/http-client"
version: 2.2.3
version: 3.0.1
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client

View File

@@ -0,0 +1,36 @@
---
name: handlebars
version: 4.7.8
type: npm
summary: Handlebars provides the power necessary to let you build semantic templates
effectively with no frustration
homepage: https://www.handlebarsjs.com/
license: mit
licenses:
- sources: LICENSE
text: |
Copyright (C) 2011-2019 by Yehuda Katz
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.markdown
text: |-
Handlebars.js is released under the MIT license.
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
notices: []

View File

@@ -0,0 +1,44 @@
---
name: minimist
version: 1.2.8
type: npm
summary: parse argument options
homepage: https://github.com/minimistjs/minimist
license: other
licenses:
- sources: LICENSE
text: |
This software is released under the 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.
- sources: README.md
text: |-
MIT
[package-url]: https://npmjs.org/package/minimist
[npm-version-svg]: https://versionbadg.es/minimistjs/minimist.svg
[npm-badge-png]: https://nodei.co/npm/minimist.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/minimist.svg
[license-url]: LICENSE
[downloads-image]: https://img.shields.io/npm/dm/minimist.svg
[downloads-url]: https://npm-stat.com/charts.html?package=minimist
[codecov-image]: https://codecov.io/gh/minimistjs/minimist/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/minimistjs/minimist/
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/minimistjs/minimist
[actions-url]: https://github.com/minimistjs/minimist/actions
notices: []

View File

@@ -0,0 +1,36 @@
---
name: neo-async
version: 2.6.2
type: npm
summary: 'Neo-Async is a drop-in replacement for Async, it almost fully covers its
functionality and runs faster '
homepage: https://github.com/suguru03/neo-async
license: other
licenses:
- sources: LICENSE
text: |
MIT License
Copyright (c) 2014-2018 Suguru Motegi
Based on Async.js, Copyright Caolan McMahon
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: "[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fsuguru03%2Fneo-async.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fsuguru03%2Fneo-async?ref=badge_large)"
notices: []

View File

@@ -0,0 +1,26 @@
---
name: semver
version: 7.7.3
type: npm
summary: The semantic version parser used by npm.
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: []

View File

@@ -0,0 +1,39 @@
---
name: source-map
version: 0.6.1
type: npm
summary: Generates and consumes source maps
homepage: https://github.com/mozilla/source-map
license: other
licenses:
- sources: LICENSE
text: |2
Copyright (c) 2009-2011, Mozilla Foundation and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the names of the Mozilla Foundation nor the names of project
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
notices: []

View File

@@ -0,0 +1,40 @@
---
name: uglify-js
version: 3.19.3
type: npm
summary: JavaScript parser, mangler/compressor and beautifier toolkit
homepage:
license: other
licenses:
- sources: LICENSE
text: |
UglifyJS is released under the BSD license:
Copyright 2012-2024 (c) Mihai Bazon <mihai.bazon@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the following
disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
notices: []

View File

@@ -0,0 +1,29 @@
---
name: wordwrap
version: 1.0.0
type: npm
summary: Wrap those words. Show them at what columns to start and stop.
homepage:
license: other
licenses:
- sources: LICENSE
text: |
This software is released under the 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: []

View File

@@ -19,6 +19,10 @@ tags.
up-to-date. Always run `npm run package` (or `npm run bundle`) after modifying
`src/` files.
**License Compliance**: After modifying NPM dependencies, check license status
and re-cache if needed via `npm run licensed:status` and
`npm run licensed:cache`.
## Development Commands
Package manager: npm
@@ -35,7 +39,7 @@ npm run package # Build src/index.ts -> dist/index.js via Rollup
npm run bundle # Alias: format + package
# Run a single test file
NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest tests/main.test.ts
NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest tests/main.test.ts
# CI variants (suppress warnings)
npm run ci-test # Run tests in CI mode
@@ -48,6 +52,10 @@ npm run update-readme
# Watch mode
npm run package:watch # Auto-rebuild on changes
# License compliance (after modifying dependencies)
npm run licensed:status # Check license status of dependencies
npm run licensed:cache # Re-cache licenses if needed
```
## Code Architecture
@@ -59,6 +67,11 @@ npm run package:watch # Auto-rebuild on changes
function that coordinates input parsing, tag processing, and output setting
- **[src/inputs.ts](src/inputs.ts)**: Input parsing and validation. Exports
`getInputs()` that reads action inputs and `Inputs` interface
- **[src/derive.ts](src/derive.ts)**: Semver parsing and tag derivation:
- `parseSemver()`: Parses version strings into components (prefix, major,
minor, patch, prerelease, build)
- `renderTemplate()`: Renders Handlebars templates with semver context
- `deriveTags()`: Derives tags from a version string using a template
- **[src/tags.ts](src/tags.ts)**: Tag planning and execution logic:
- `planTagOperations()`: Parses tags, pre-resolves refs to SHAs in parallel,
plans create/update/skip operations
@@ -72,13 +85,36 @@ npm run package:watch # Auto-rebuild on changes
### Tag Input Parsing
Uses `csv-parse/sync` to handle both CSV and newline-delimited formats. Supports
per-tag ref overrides: `v1:main` tags `v1` to `main` branch.
per-tag ref and annotation overrides using the format `tag:ref:annotation`:
- `tag` — Use default `ref` and `annotation` inputs
- `tag:ref` — Override ref for this tag (e.g., `v1:main` tags `v1` to `main`)
- `tag:ref:annotation` — Override both ref and annotation
- `tag::annotation` — Override annotation only (empty ref uses default)
Annotations can contain colons; everything after the second colon is treated as
the annotation text. Per-tag values override the global `ref` and `annotation`
inputs.
### Tag Derivation
The `derive_from` input allows automatic generation of tags from a semver
version string. Uses Handlebars templates with these placeholders:
- `{{prefix}}`: "v" or "V" if input had prefix, empty otherwise
- `{{major}}`, `{{minor}}`, `{{patch}}`: Version numbers
- `{{prerelease}}`, `{{build}}`: Optional semver components
- `{{version}}`: Full version without prefix
Default template `{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}` generates
major and minor tags (e.g., `v1.2.3``v1`, `v1.2`). Supports Handlebars
conditionals like `{{#if prerelease}}...{{/if}}`.
### Tag Update Logic
1. Parse and validate inputs ([inputs.ts](src/inputs.ts))
2. Plan all tag operations ([tags.ts](src/tags.ts):planTagOperations):
- Parse `tag:ref` syntax and extract per-tag refs
- Parse `tag:ref:annotation` syntax and extract per-tag refs/annotations
- Pre-resolve all unique refs to SHAs in parallel (optimization)
- For each tag, check existence and determine operation:
- If exists + fail mode: Fail action immediately
@@ -188,12 +224,19 @@ chore(deps): bump @actions/core to v1.10.0
**Inputs:**
- `tags`: CSV/newline list, supports `tag:ref` syntax
- `ref`: SHA/ref to tag (default: current commit)
- `tags`: CSV/newline list, supports `tag:ref:annotation` syntax for per-tag
overrides
- `derive_from`: Semver version string to derive tags from (e.g., "v1.2.3")
- `derive_from_template`: Handlebars template for tag derivation (default:
`{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}`)
- `ref`: Default SHA/ref to tag (default: current commit)
- `when_exists`: update|skip|fail (default: update)
- `annotation`: Optional message for annotated tags (default: lightweight)
- `annotation`: Default annotation message for tags (default: lightweight/none)
- `dry_run`: Log planned operations without executing (default: false)
- `github_token`: Auth token (default: github.token)
Either `tags` or `derive_from` (or both) must be provided.
**Outputs:**
- `tags`: All created/updated tags

View File

@@ -1,5 +1,14 @@
# Changelog
## [2.3.0](https://github.com/jimeh/update-tags-action/compare/v2.2.1...v2.3.0) (2025-12-24)
### Features
* **inputs/derive_from:** derive tags from provided semver string ([#79](https://github.com/jimeh/update-tags-action/issues/79)) ([1d171e9](https://github.com/jimeh/update-tags-action/commit/1d171e9f3c63639b8d0145a881a6df941200742d))
* **inputs/dry_run:** add dry run support ([#76](https://github.com/jimeh/update-tags-action/issues/76)) ([ef41989](https://github.com/jimeh/update-tags-action/commit/ef4198907727f6ff9e5813deb5ab5a4307ab4691))
* **inputs/tags:** support per-tag annotation overrides ([#81](https://github.com/jimeh/update-tags-action/issues/81)) ([32e66b0](https://github.com/jimeh/update-tags-action/commit/32e66b04be37383e2450d698007faa436c706ffd))
## [2.2.1](https://github.com/jimeh/update-tags-action/compare/v2.2.0...v2.2.1) (2025-12-22)

195
README.md
View File

@@ -22,23 +22,50 @@ to move its own major and minor tags.
### Basic
<!-- x-release-please-start-minor -->
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: v2,v2.2
tags: v1,v1.2
```
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: |
v2
v2.2
v1
v1.2
```
<!-- x-release-please-end -->
### Deriving Tags from Version
Automatically derive major and minor tags from a semver version string:
```yaml
- uses: jimeh/update-tags-action@v2
with:
derive_from: v1.2.3
# Creates tags: v1, v1.2
```
With a custom template (major tag only):
```yaml
- uses: jimeh/update-tags-action@v2
with:
derive_from: v1.2.3
derive_from_template: '{{prefix}}{{major}}'
# Creates tag: v1
```
Combine derived tags with explicit tags:
```yaml
- uses: jimeh/update-tags-action@v2
with:
derive_from: v1.2.3
tags: latest
# Creates tags: latest, v1, v1.2
```
### With Release Please
@@ -65,8 +92,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
outputs:
release_created: ${{ steps.release-please.outputs.release_created }}
major: ${{ steps.release-please.outputs.major }}
minor: ${{ steps.release-please.outputs.minor }}
tag_name: ${{ steps.release-please.outputs.tag_name }}
permissions:
contents: write
issues: write
@@ -79,8 +105,6 @@ jobs:
And finally a job to create MAJOR and MINOR release tags, which only runs when
release-please reports having created a release:
<!-- x-release-please-start-major -->
```yaml
jobs:
# [...]
@@ -93,26 +117,153 @@ jobs:
steps:
- uses: jimeh/update-tags-action@v2
with:
tags: |
v${{ needs.release-please.outputs.major }}
v${{ needs.release-please.outputs.major }}.${{ needs.release-please.outputs.minor }}
derive_from: ${{ needs.release-please.outputs.tag_name }}
# Creates tags: v1, v1.2 (for tag_name v1.2.3)
```
<!-- x-release-please-end -->
<!-- action-docs-inputs source="action.yml" -->
## Inputs
| name | description | required | default |
| -------------- | --------------------------------------------------------------------------------------------------------------------- | -------- | --------------------- |
| `tags` | <p>List/CSV of tags to create/update.</p> | `true` | `""` |
| `ref` | <p>The SHA or ref to tag. Defaults to SHA of current commit.</p> | `false` | `${{ github.sha }}` |
| `when_exists` | <p>What to do if the tag already exists. Must be one of 'update', 'skip', or 'fail'.</p> | `false` | `update` |
| `annotation` | <p>Optional annotation message for tags. If provided, creates annotated tags. If empty, creates lightweight tags.</p> | `false` | `""` |
| `github_token` | <p>The GitHub token to use for authentication.</p> | `false` | `${{ github.token }}` |
| name | description | required | default |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------------------------------------------------- |
| `tags` | <p>List/CSV of tags to create/update. Supports per-tag ref and annotation overrides using the format 'tag:ref:annotation'. Use 'tag::annotation' to specify an annotation with the default ref.</p> | `false` | `""` |
| `derive_from` | <p>Semver version string to derive tags from (e.g., 'v1.2.3'). When provided, generates tags using <code>derive_from_template</code> input. Default template will produce major and minor tags. (e.g., 'v1', 'v1.2')</p> | `false` | `""` |
| `derive_from_template` | <p>Handlebars template for deriving tags from the <code>derive_from</code> input. Uses the same format as the <code>tags</code> input, and supports the following handlebars placeholders: <code>{{prefix}}</code>, <code>{{major}}</code>, <code>{{minor}}</code>, <code>{{patch}}</code>, <code>{{prerelease}}</code>, <code>{{build}}</code>, <code>{{version}}</code>.</p> | `false` | `{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}` |
| `ref` | <p>The SHA or ref to tag. Defaults to SHA of current commit.</p> | `false` | `${{ github.sha }}` |
| `when_exists` | <p>What to do if the tag already exists. Must be one of 'update', 'skip', or 'fail'.</p> | `false` | `update` |
| `annotation` | <p>Optional default annotation message for tags. If provided, creates annotated tags. If empty, creates lightweight tags. Can be overridden per-tag using the 'tag:ref:annotation' syntax in the tags input.</p> | `false` | `""` |
| `dry_run` | <p>If true, logs planned operations without executing them.</p> | `false` | `false` |
| `github_token` | <p>The GitHub token to use for authentication.</p> | `false` | `${{ github.token }}` |
<!-- action-docs-inputs source="action.yml" -->
### Tag Input Syntax
The `tags` input accepts a comma or newline-delimited list of tags. Each tag
specification supports optional per-tag ref and annotation overrides using the
format:
```
tag[:ref[:annotation]]
```
| Format | Description |
| -------------------- | ----------------------------------------------- |
| `tag` | Tag using default `ref` and `annotation` inputs |
| `tag:ref` | Tag using specified ref, default annotation |
| `tag:ref:annotation` | Tag using specified ref and annotation |
| `tag::annotation` | Tag using default ref with specified annotation |
**Per-tag refs** allow different tags to point to different commits:
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: |
v1:main
v2:develop
```
**Per-tag annotations** allow different annotation messages for each tag:
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: |
v1:main:Latest v1.x release
v1.2:main:Latest v1.2.x release
```
Use `tag::annotation` to specify an annotation while using the default ref:
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: |
v1::This is the v1 tag annotation
v1.2::This is the v1.2 tag annotation
```
Per-tag values override the global `ref` and `annotation` inputs:
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: |
v1:main:Custom annotation for v1
v2
ref: develop
annotation: Default annotation for tags without per-tag override
# v1 -> main with "Custom annotation for v1"
# v2 -> develop with "Default annotation..."
```
Annotations can contain colons (everything after the second colon is the
annotation):
```yaml
- uses: jimeh/update-tags-action@v2
with:
tags: |
v1:main:Release: version 1.0.0
# Annotation will be "Release: version 1.0.0"
```
### Derive Template Syntax
The `derive_from_template` input uses [Handlebars](https://handlebarsjs.com/)
for template rendering. Splitting the template into separate tags by comma or
newline is done after the template is rendered.
Available placeholders:
| Placeholder | Description |
| ---------------- | ----------------------------------------------------- |
| `{{prefix}}` | `v` or `V` if input had a prefix, empty otherwise |
| `{{major}}` | Major version number |
| `{{minor}}` | Minor version number |
| `{{patch}}` | Patch version number |
| `{{prerelease}}` | Prerelease identifier (e.g., `beta.1`), empty if none |
| `{{build}}` | Build metadata (e.g., `build.123`), empty if none |
| `{{version}}` | Full version string without prefix |
#### Conditional Sections
Use Handlebars `{{#if}}` blocks to include content only when a variable has a
value. This is useful for optional components like prerelease or build metadata:
```yaml
- uses: jimeh/update-tags-action@v2
with:
derive_from: v1.2.3-beta.1
derive_from_template: |
{{prefix}}{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}
# Creates tag: v1-beta.1
```
For a stable release without prerelease:
```yaml
- uses: jimeh/update-tags-action@v2
with:
derive_from: v1.2.3
derive_from_template: |
{{prefix}}{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}
# Creates tag: v1 (prerelease section omitted)
```
You can also use `{{#unless}}` for inverse logic:
```yaml
- uses: jimeh/update-tags-action@v2
with:
derive_from: v1.2.3
derive_from_template: |
{{prefix}}{{major}}{{#unless prerelease}}-stable{{/unless}}
# Creates tag: v1-stable (only for non-prerelease versions)
```
<!-- action-docs-outputs source="action.yml" -->
## Outputs

View File

@@ -8,8 +8,25 @@ branding:
inputs:
tags:
description: "List/CSV of tags to create/update."
required: true
description: >-
List/CSV of tags to create/update. Supports per-tag ref and annotation
overrides using the format 'tag:ref:annotation'. Use 'tag::annotation' to
specify an annotation with the default ref.
required: false
derive_from:
description: >-
Semver version string to derive tags from (e.g., 'v1.2.3'). When provided,
generates tags using `derive_from_template` input. Default template will
produce major and minor tags. (e.g., 'v1', 'v1.2')
required: false
derive_from_template:
description: >-
Handlebars template for deriving tags from the `derive_from` input. Uses
the same format as the `tags` input, and supports the following handlebars
placeholders: `{{prefix}}`, `{{major}}`, `{{minor}}`, `{{patch}}`,
`{{prerelease}}`, `{{build}}`, `{{version}}`.
required: false
default: "{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}"
ref:
description: "The SHA or ref to tag. Defaults to SHA of current commit."
required: false
@@ -22,10 +39,16 @@ inputs:
default: "update"
annotation:
description: >-
Optional annotation message for tags. If provided, creates annotated tags.
If empty, creates lightweight tags.
Optional default annotation message for tags. If provided, creates
annotated tags. If empty, creates lightweight tags. Can be overridden
per-tag using the 'tag:ref:annotation' syntax in the tags input.
required: false
default: ""
dry_run:
description: >-
If true, logs planned operations without executing them.
required: false
default: "false"
github_token:
description: "The GitHub token to use for authentication."
required: false

12750
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

518
package-lock.json generated
View File

@@ -1,40 +1,43 @@
{
"name": "update-tags-action",
"version": "2.2.1",
"version": "2.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "update-tags-action",
"version": "2.2.1",
"version": "2.3.0",
"dependencies": {
"@actions/core": "^2.0.1",
"@actions/github": "^6.0.1",
"csv-parse": "^6.1.0"
"@actions/core": "^2.0.2",
"@actions/github": "^7.0.0",
"csv-parse": "^6.1.0",
"handlebars": "^4.7.8",
"semver": "^7.7.3"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/compat": "^2.0.1",
"@jest/globals": "^30.2.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@types/node": "^25.0.9",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.46.2",
"action-docs": "^2.5.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.9.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-jest": "^29.12.1",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.2.0",
"make-coverage-badge": "^1.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.7.4",
"prettier": "^3.8.0",
"prettier-eslint": "^16.4.2",
"rollup": "^4.54.0",
"rollup": "^4.55.1",
"ts-jest": "^29.4.6",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.9.3"
@@ -44,23 +47,13 @@
}
},
"node_modules/@actions/core": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.1.tgz",
"integrity": "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-2.0.2.tgz",
"integrity": "sha512-Ast1V7yHbGAhplAsuVlnb/5J8Mtr/Zl6byPPL+Qjq3lmfIgWF1ak1iYfF/079cRERiuTALTXkSuEUdZeDCfGtA==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^2.0.0",
"@actions/http-client": "^3.0.0"
}
},
"node_modules/@actions/core/node_modules/@actions/http-client": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
"integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.28.5"
"@actions/http-client": "^3.0.1"
}
},
"node_modules/@actions/exec": {
@@ -73,12 +66,12 @@
}
},
"node_modules/@actions/github": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz",
"integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-7.0.0.tgz",
"integrity": "sha512-PyGODO938aoBTZd/IfN/+e+Pd5hUcVpyf+thm4CPESLeqhdSkq5QwMTGX9v84XHE1ifmHWBQ60KB8kIgm96opw==",
"license": "MIT",
"dependencies": {
"@actions/http-client": "^2.2.0",
"@actions/http-client": "^3.0.1",
"@octokit/core": "^5.0.1",
"@octokit/plugin-paginate-rest": "^9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^10.4.0",
@@ -88,13 +81,13 @@
}
},
"node_modules/@actions/http-client": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.1.tgz",
"integrity": "sha512-SbGS8c/vySbNO3kjFgSW77n83C4MQx/Yoe+b1hAdpuvfHxnkHzDq2pWljUpAA56Si1Gae/7zjeZsV0CYjmLo/w==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
"undici": "^5.28.5"
}
},
"node_modules/@actions/io": {
@@ -134,7 +127,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -655,9 +647,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -684,13 +676,13 @@
}
},
"node_modules/@eslint/compat": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.0.tgz",
"integrity": "sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.1.tgz",
"integrity": "sha512-yl/JsgplclzuvGFNqwNYV4XNPhP3l62ZOP9w/47atNAdmDtIFCx6X7CSk/SlWUuBGkT4Et/5+UD+WyvX2iiIWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.0.0"
"@eslint/core": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
@@ -705,9 +697,9 @@
}
},
"node_modules/@eslint/compat/node_modules/@eslint/core": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz",
"integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.1.tgz",
"integrity": "sha512-r18fEAj9uCk+VjzGt2thsbOmychS+4kxI14spVNibUO2vqKX7obOG+ymZljAwuPZl+S3clPGwCwTDtrdqTiY6Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1578,7 +1570,6 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz",
"integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
@@ -1850,9 +1841,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
"cpu": [
"arm"
],
@@ -1864,9 +1855,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
"cpu": [
"arm64"
],
@@ -1878,9 +1869,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
"cpu": [
"arm64"
],
@@ -1892,9 +1883,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
"cpu": [
"x64"
],
@@ -1906,9 +1897,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
"cpu": [
"arm64"
],
@@ -1920,9 +1911,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
"cpu": [
"x64"
],
@@ -1934,9 +1925,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
"cpu": [
"arm"
],
@@ -1948,9 +1939,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
"cpu": [
"arm"
],
@@ -1962,9 +1953,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
"cpu": [
"arm64"
],
@@ -1976,9 +1967,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
"cpu": [
"arm64"
],
@@ -1990,9 +1981,23 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
"cpu": [
"loong64"
],
@@ -2004,9 +2009,23 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
"cpu": [
"ppc64"
],
@@ -2018,9 +2037,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
"cpu": [
"riscv64"
],
@@ -2032,9 +2051,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
"cpu": [
"riscv64"
],
@@ -2046,9 +2065,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
"cpu": [
"s390x"
],
@@ -2060,9 +2079,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
"cpu": [
"x64"
],
@@ -2074,9 +2093,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
"cpu": [
"x64"
],
@@ -2087,10 +2106,24 @@
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
"cpu": [
"arm64"
],
@@ -2102,9 +2135,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
"cpu": [
"arm64"
],
@@ -2116,9 +2149,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
"cpu": [
"ia32"
],
@@ -2130,9 +2163,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
"cpu": [
"x64"
],
@@ -2144,9 +2177,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
"cpu": [
"x64"
],
@@ -2317,9 +2350,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"version": "25.0.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz",
"integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2333,6 +2366,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -2358,21 +2398,20 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
"integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/type-utils": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"ignore": "^7.0.0",
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/type-utils": "8.53.0",
"@typescript-eslint/utils": "8.53.0",
"@typescript-eslint/visitor-keys": "8.53.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2382,24 +2421,23 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.50.0",
"@typescript-eslint/parser": "^8.53.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz",
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4"
"@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.53.0",
"@typescript-eslint/typescript-estree": "8.53.0",
"@typescript-eslint/visitor-keys": "8.53.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2414,15 +2452,15 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz",
"integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.50.0",
"@typescript-eslint/types": "^8.50.0",
"debug": "^4.3.4"
"@typescript-eslint/tsconfig-utils": "^8.53.0",
"@typescript-eslint/types": "^8.53.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2436,14 +2474,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz",
"integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0"
"@typescript-eslint/types": "8.53.0",
"@typescript-eslint/visitor-keys": "8.53.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2454,9 +2492,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz",
"integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2471,17 +2509,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz",
"integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0",
"@typescript-eslint/utils": "8.50.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
"@typescript-eslint/types": "8.53.0",
"@typescript-eslint/typescript-estree": "8.53.0",
"@typescript-eslint/utils": "8.53.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2496,9 +2534,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz",
"integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2510,21 +2548,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz",
"integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.50.0",
"@typescript-eslint/tsconfig-utils": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/visitor-keys": "8.50.0",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"@typescript-eslint/project-service": "8.53.0",
"@typescript-eslint/tsconfig-utils": "8.53.0",
"@typescript-eslint/types": "8.53.0",
"@typescript-eslint/visitor-keys": "8.53.0",
"debug": "^4.4.3",
"minimatch": "^9.0.5",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.4.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2538,16 +2576,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz",
"integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/typescript-estree": "8.50.0"
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.53.0",
"@typescript-eslint/typescript-estree": "8.53.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2562,13 +2600,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz",
"integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.50.0",
"@typescript-eslint/types": "8.53.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -2874,7 +2912,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3342,7 +3379,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -4142,7 +4178,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4203,7 +4238,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -4330,7 +4364,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4404,9 +4437,9 @@
}
},
"node_modules/eslint-plugin-jest": {
"version": "29.9.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.9.0.tgz",
"integrity": "sha512-1H2VNoEkJEO3CzfoSvAIBRaiywXmLU7FesYct/yI4YPnzkM91bNFBMVKqNjT2C7mPGvKAkCQV14oi5rgFCqPdQ==",
"version": "29.12.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.12.1.tgz",
"integrity": "sha512-Rxo7r4jSANMBkXLICJKS0gjacgyopfNAsoS0e3R9AHnjoKuQOaaPfmsDJPi8UWwygI099OV/K/JhpYRVkxD4AA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4430,14 +4463,14 @@
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
"integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
"version": "5.5.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
"integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.0",
"synckit": "^0.11.7"
"prettier-linter-helpers": "^1.0.1",
"synckit": "^0.11.12"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -5215,7 +5248,6 @@
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
@@ -6051,7 +6083,6 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -7093,7 +7124,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -7153,7 +7183,6 @@
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/nice-try": {
@@ -7880,12 +7909,11 @@
}
},
"node_modules/prettier": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
"integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -8164,7 +8192,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -8399,9 +8426,9 @@
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8771,12 +8798,11 @@
}
},
"node_modules/rollup": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -8788,28 +8814,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"@rollup/rollup-android-arm-eabi": "4.55.1",
"@rollup/rollup-android-arm64": "4.55.1",
"@rollup/rollup-darwin-arm64": "4.55.1",
"@rollup/rollup-darwin-x64": "4.55.1",
"@rollup/rollup-freebsd-arm64": "4.55.1",
"@rollup/rollup-freebsd-x64": "4.55.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
"@rollup/rollup-linux-arm-musleabihf": "4.55.1",
"@rollup/rollup-linux-arm64-gnu": "4.55.1",
"@rollup/rollup-linux-arm64-musl": "4.55.1",
"@rollup/rollup-linux-loong64-gnu": "4.55.1",
"@rollup/rollup-linux-loong64-musl": "4.55.1",
"@rollup/rollup-linux-ppc64-gnu": "4.55.1",
"@rollup/rollup-linux-ppc64-musl": "4.55.1",
"@rollup/rollup-linux-riscv64-gnu": "4.55.1",
"@rollup/rollup-linux-riscv64-musl": "4.55.1",
"@rollup/rollup-linux-s390x-gnu": "4.55.1",
"@rollup/rollup-linux-x64-gnu": "4.55.1",
"@rollup/rollup-linux-x64-musl": "4.55.1",
"@rollup/rollup-openbsd-x64": "4.55.1",
"@rollup/rollup-openharmony-arm64": "4.55.1",
"@rollup/rollup-win32-arm64-msvc": "4.55.1",
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
"@rollup/rollup-win32-x64-gnu": "4.55.1",
"@rollup/rollup-win32-x64-msvc": "4.55.1",
"fsevents": "~2.3.2"
}
},
@@ -8896,7 +8925,6 @@
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -9120,7 +9148,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -9506,9 +9533,9 @@
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"version": "0.11.12",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
"integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9627,9 +9654,9 @@
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9989,8 +10016,7 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tunnel": {
"version": "0.0.6",
@@ -10121,7 +10147,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10134,7 +10159,6 @@
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
@@ -10195,7 +10219,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -10480,7 +10503,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi": {

View File

@@ -1,6 +1,6 @@
{
"name": "update-tags-action",
"version": "2.2.1",
"version": "2.3.0",
"author": "jimeh",
"type": "module",
"private": true,
@@ -41,33 +41,36 @@
"update-readme": "npx action-docs --update-readme && npx prettier --write README.md"
},
"dependencies": {
"@actions/core": "^2.0.1",
"@actions/github": "^6.0.1",
"csv-parse": "^6.1.0"
"@actions/core": "^2.0.2",
"@actions/github": "^7.0.0",
"csv-parse": "^6.1.0",
"handlebars": "^4.7.8",
"semver": "^7.7.3"
},
"devDependencies": {
"@eslint/compat": "^2.0.0",
"@eslint/compat": "^2.0.1",
"@jest/globals": "^30.2.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"@typescript-eslint/eslint-plugin": "^8.50.0",
"@types/node": "^25.0.9",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.46.2",
"action-docs": "^2.5.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.9.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-jest": "^29.12.1",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.2.0",
"make-coverage-badge": "^1.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.7.4",
"prettier": "^3.8.0",
"prettier-eslint": "^16.4.2",
"rollup": "^4.54.0",
"rollup": "^4.55.1",
"ts-jest": "^29.4.6",
"ts-jest-resolver": "^2.0.1",
"typescript": "^5.9.3"

105
src/derive.ts Normal file
View File

@@ -0,0 +1,105 @@
import { parse } from 'csv-parse/sync'
import Handlebars from 'handlebars'
import * as semver from 'semver'
/**
* Context containing parsed semver components for template rendering.
*/
export interface SemverContext {
/** "v" or "V" if input had a prefix, empty string otherwise */
prefix: string
/** Major version number */
major: number
/** Minor version number */
minor: number
/** Patch version number */
patch: number
/** Prerelease identifier (e.g., "beta.1"), empty if none */
prerelease: string
/** Build metadata (e.g., "build.123"), empty if none */
build: string
/** Full version string without prefix */
version: string
}
/**
* Parse a version string into semver components.
*
* @param input - Version string (e.g., "v1.2.3", "1.2.3-beta.1+build.456")
* @returns Parsed semver context
* @throws Error if the version string is not valid semver
*/
export function parseSemver(input: string): SemverContext {
const trimmed = input.trim()
if (!trimmed) {
throw new Error('Invalid semver: empty string')
}
// Check for v/V prefix and preserve original case
const firstChar = trimmed[0]
const hasPrefix = firstChar === 'v' || firstChar === 'V'
const prefix = hasPrefix ? firstChar : ''
const versionStr = hasPrefix ? trimmed.slice(1) : trimmed
// Parse with semver library
const parsed = semver.parse(versionStr)
if (!parsed) {
throw new Error(`Invalid semver: '${input}'`)
}
return {
prefix,
major: parsed.major,
minor: parsed.minor,
patch: parsed.patch,
prerelease: parsed.prerelease.join('.'),
build: parsed.build.join('.'),
version: parsed.version
}
}
/**
* Render a single template string with semver context using Handlebars.
* Supports {{#if variable}}...{{/if}} conditionals for optional sections.
*
* @param template - Handlebars template string
* @param ctx - Semver context for substitution
* @returns Rendered string
*/
export function renderTemplate(template: string, ctx: SemverContext): string {
const compiled = Handlebars.compile(template, { noEscape: true })
// Convert numbers to strings so 0 is truthy in conditionals
const stringCtx = {
...ctx,
major: String(ctx.major),
minor: String(ctx.minor),
patch: String(ctx.patch)
}
return compiled(stringCtx)
}
/**
* Derive tags from a semver version string using a template.
*
* @param deriveFrom - Semver version string (e.g., "v1.2.3")
* @param template - CSV/newline-delimited Handlebars template string
* @returns Array of derived tag strings
*/
export function deriveTags(deriveFrom: string, template: string): string[] {
const ctx = parseSemver(deriveFrom)
// Render template with Handlebars first, enabling conditional tag inclusion
const rendered = renderTemplate(template, ctx)
// Parse rendered result as CSV/newline delimited
const tags = (
parse(rendered, {
delimiter: ',',
trim: true,
relax_column_count: true
}) as string[][]
).flat()
// Exclude empty tags and tags that are just the prefix with no version data
return tags.filter((tag) => tag.length > 0 && tag !== ctx.prefix)
}

View File

@@ -1,6 +1,7 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { parse } from 'csv-parse/sync'
import { deriveTags } from './derive.js'
const WHEN_EXISTS_MODES = ['update', 'skip', 'fail'] as const
export type WhenExistsMode = (typeof WHEN_EXISTS_MODES)[number]
@@ -10,6 +11,7 @@ export interface Inputs {
defaultRef: string
whenExists: WhenExistsMode
annotation: string
dryRun: boolean
owner: string
repo: string
token: string
@@ -32,37 +34,62 @@ function validateWhenExists(input: string): WhenExistsMode {
return input as WhenExistsMode
}
const DEFAULT_DERIVE_FROM_TEMPLATE =
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}'
/**
* Get and validate action inputs.
*
* @returns Parsed and validated inputs
*/
export function getInputs(): Inputs {
const tagsInput: string = core.getInput('tags', { required: true })
const tagsInput: string = core.getInput('tags')
const deriveFrom: string = core.getInput('derive_from')
const deriveFromTemplate: string =
core.getInput('derive_from_template') || DEFAULT_DERIVE_FROM_TEMPLATE
const defaultRef: string = core.getInput('ref')
const whenExistsInput = core.getInput('when_exists') || 'update'
const whenExists = validateWhenExists(whenExistsInput)
const annotation: string = core.getInput('annotation')
const dryRun: boolean = core.getBooleanInput('dry_run')
const token: string = core.getInput('github_token', {
required: true
})
const { owner, repo } = github.context.repo
// Parse tags as CSV/newline delimited strings
const tags = (
parse(tagsInput, {
delimiter: ',',
trim: true,
relax_column_count: true
}) as string[][]
).flat()
// Parse explicit tags as CSV/newline delimited strings
const explicitTags: string[] = tagsInput
? (
parse(tagsInput, {
delimiter: ',',
trim: true,
relax_column_count: true
}) as string[][]
).flat()
: []
// Derive tags from semver version string if provided
const derivedTags: string[] = deriveFrom
? deriveTags(deriveFrom, deriveFromTemplate)
: []
// Combine explicit and derived tags
const tags = [...explicitTags, ...derivedTags]
// Validate that at least one tag source is provided
if (tags.length === 0) {
throw new Error(
"No tags specified. Provide 'tags' input, 'derive_from' input, or both."
)
}
return {
tags,
defaultRef,
whenExists,
annotation,
dryRun,
owner,
repo,
token

47
src/logger.ts Normal file
View File

@@ -0,0 +1,47 @@
import * as core from '@actions/core'
import type { AnnotationProperties } from '@actions/core'
/**
* Logger interface that mirrors @actions/core logging functions.
*/
export interface Logger {
debug: (message: string) => void
info: (message: string) => void
notice: (message: string | Error, properties?: AnnotationProperties) => void
warning: (message: string | Error, properties?: AnnotationProperties) => void
error: (message: string | Error, properties?: AnnotationProperties) => void
}
/**
* Create a logger that optionally prefixes all messages.
*
* @param prefix - Optional prefix to prepend to all log messages
* @returns Logger instance with prefixed messages
*/
export function createLogger(prefix: string = ''): Logger {
const prefixMessage = (msg: string | Error): string | Error => {
if (!prefix) {
return msg
}
if (typeof msg === 'string') {
return `${prefix}${msg}`
}
// Wrap Error with prefixed message, preserving the original as cause
const wrapped = new Error(`${prefix}${msg.message}`, { cause: msg })
if (msg.stack) {
wrapped.stack = msg.stack
}
return wrapped
}
return {
debug: (message: string) => core.debug(`${prefix}${message}`),
info: (message: string) => core.info(`${prefix}${message}`),
notice: (message: string | Error, properties?: AnnotationProperties) =>
core.notice(prefixMessage(message), properties),
warning: (message: string | Error, properties?: AnnotationProperties) =>
core.warning(prefixMessage(message), properties),
error: (message: string | Error, properties?: AnnotationProperties) =>
core.error(prefixMessage(message), properties)
}
}

View File

@@ -14,20 +14,27 @@ export async function run(): Promise<void> {
const octokit = github.getOctokit(inputs.token)
const operations = await planTagOperations(inputs, octokit)
if (inputs.dryRun) {
core.info('[dry-run] Dry-run mode enabled, no changes will be made.')
}
const created: string[] = []
const updated: string[] = []
const skipped: string[] = []
// Execute all planned operations.
for (const operation of operations) {
await executeTagOperation(operation, octokit)
await executeTagOperation(operation, octokit, { dryRun: inputs.dryRun })
if (operation.operation === 'create') {
created.push(operation.name)
} else if (operation.operation === 'update') {
updated.push(operation.name)
} else if (operation.operation === 'skip') {
skipped.push(operation.name)
// Only track actual changes when not in dry-run mode
if (!inputs.dryRun) {
if (operation.operation === 'create') {
created.push(operation.name)
} else if (operation.operation === 'update') {
updated.push(operation.name)
} else if (operation.operation === 'skip') {
skipped.push(operation.name)
}
}
}

View File

@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import type { Inputs } from './inputs.js'
import { createLogger, type Logger } from './logger.js'
export interface ExistingTagInfo {
commitSHA: string
@@ -37,12 +37,21 @@ export interface SkipOperation extends BaseOperation {
export type TagOperation = CreateOperation | UpdateOperation | SkipOperation
interface Context {
export interface ExecuteOptions {
dryRun?: boolean
}
interface ReadContext {
owner: string
repo: string
octokit: ReturnType<typeof github.getOctokit>
}
interface Context extends ReadContext {
dryRun: boolean
log: Logger
}
/**
* Plan tag operations based on inputs.
*
@@ -56,19 +65,18 @@ export async function planTagOperations(
): Promise<TagOperation[]> {
const uniqueRefs = new Set<string>()
const tagRefs: Record<string, string> = {}
const tagAnnotations: Record<string, string> = {}
for (const tag of inputs.tags) {
const parts = tag.split(':').map((s) => s.trim())
if (parts.length > 2) {
throw new Error(
`Invalid tag specification '${tag}': too many colons. ` +
`Format should be 'tag' or 'tag:ref'.`
)
}
const [tagName, tagRef] = parts
const parts = tag.split(':')
const tagName = (parts[0] || '').trim()
const tagRef = (parts[1] || '').trim()
// Join remaining parts back with colons to preserve annotation content
const tagAnnotation = parts.slice(2).join(':').trim()
if (!tagName) {
// Skip completely empty tags, but fail on invalid ones like ":main"
if (tagRef) {
if (tagRef || tagAnnotation) {
throw new Error(`Invalid tag: '${tag}'`)
}
continue
@@ -88,11 +96,14 @@ export async function planTagOperations(
}
tagRefs[tagName] = ref
if (tagAnnotation) {
tagAnnotations[tagName] = tagAnnotation
}
uniqueRefs.add(ref)
}
// Pre-resolve all unique refs in parallel.
const ctx: Context = { owner: inputs.owner, repo: inputs.repo, octokit }
const ctx: ReadContext = { owner: inputs.owner, repo: inputs.repo, octokit }
const refSHAs: Record<string, string> = {}
await Promise.all(
Array.from(uniqueRefs).map(async (ref) => {
@@ -106,6 +117,7 @@ export async function planTagOperations(
tagNames.map(async (tagName) => {
const tagRef = tagRefs[tagName]
const sha = refSHAs[tagRef]
const annotation = tagAnnotations[tagName] || inputs.annotation
// Check if tag already exists
let existing: ExistingTagInfo | undefined
@@ -146,7 +158,7 @@ export async function planTagOperations(
return {
...baseOp,
operation: 'create',
annotation: inputs.annotation
annotation
} as CreateOperation
}
@@ -163,7 +175,7 @@ export async function planTagOperations(
// whenExists === 'update' - check if update is needed
const { commitMatches, annotationMatches } = compareTagState(
sha,
inputs.annotation,
annotation,
existing
)
@@ -177,11 +189,11 @@ export async function planTagOperations(
}
// Plan update with reasons
const reasons = getUpdateReasons(sha, inputs.annotation, existing)
const reasons = getUpdateReasons(sha, annotation, existing)
return {
...baseOp,
operation: 'update',
annotation: inputs.annotation,
annotation,
existingSHA: existing.commitSHA,
existingIsAnnotated: existing.isAnnotated,
reasons
@@ -197,26 +209,24 @@ export async function planTagOperations(
*
* @param operation - The planned tag operation to execute
* @param octokit - GitHub API client
* @param options - Execution options (e.g., dryRun)
*/
export async function executeTagOperation(
operation: TagOperation,
octokit: ReturnType<typeof github.getOctokit>
octokit: ReturnType<typeof github.getOctokit>,
options: ExecuteOptions = {}
): Promise<void> {
const dryRun = options.dryRun ?? false
const ctx: Context = {
owner: operation.owner,
repo: operation.repo,
octokit
octokit,
dryRun,
log: createLogger(dryRun ? '[dry-run] ' : '')
}
if (operation.operation === 'skip') {
if (operation.reason === 'when_exists_skip') {
core.info(`Tag '${operation.name}' exists, skipping.`)
} else {
core.info(
`Tag '${operation.name}' already exists with desired commit SHA ${operation.sha}` +
(operation.existingIsAnnotated ? ' (annotated).' : '.')
)
}
logSkipOperation(ctx, operation)
return
}
@@ -235,15 +245,30 @@ export async function executeTagOperation(
)
}
/**
* Log a skip operation.
*/
function logSkipOperation(ctx: Context, operation: SkipOperation): void {
if (operation.reason === 'when_exists_skip') {
ctx.log.info(`Tag '${operation.name}' exists, skipping.`)
} else {
ctx.log.info(
`Tag '${operation.name}' already exists with desired ` +
`commit SHA ${operation.sha}` +
(operation.existingIsAnnotated ? ' (annotated).' : '.')
)
}
}
/**
* Fetch information about an existing tag, dereferencing if annotated.
*
* @param ctx - Operation context
* @param ctx - Read-only operation context
* @param tagName - The name of the tag to fetch
* @returns Information about the existing tag
*/
async function fetchTagInfo(
ctx: Context,
ctx: ReadContext,
tagName: string
): Promise<ExistingTagInfo> {
const ref = await ctx.octokit.rest.git.getRef({
@@ -278,11 +303,11 @@ async function fetchTagInfo(
/**
* Resolve a ref to a SHA.
*
* @param ctx - Operation context
* @param ctx - Read-only operation context
* @param ref - The ref to resolve
* @returns The SHA
*/
async function resolveRefToSha(ctx: Context, ref: string): Promise<string> {
async function resolveRefToSha(ctx: ReadContext, ref: string): Promise<string> {
try {
const {
data: { sha }
@@ -306,19 +331,24 @@ async function updateExistingTag(
operation: UpdateOperation
): Promise<void> {
const commitMatches = operation.existingSHA === operation.sha
const verb = ctx.dryRun ? 'Would update' : 'Updating'
if (commitMatches) {
core.info(
`Tag '${operation.name}' exists with same commit but ${operation.reasons.join(', ')}.`
ctx.log.info(
`${verb} tag '${operation.name}', ${operation.reasons.join(', ')}.`
)
} else {
core.info(
`Tag '${operation.name}' exists` +
`${operation.existingIsAnnotated ? ' (annotated)' : ''}` +
`, updating to ${operation.reasons.join(', ')}.`
ctx.log.info(
`${verb} tag '${operation.name}'` +
`${operation.existingIsAnnotated ? ' (annotated)' : ''} ` +
`to ${operation.reasons.join(', ')}.`
)
}
if (ctx.dryRun) {
return
}
const targetSha = await resolveTargetSHA(
ctx,
operation.name,
@@ -342,10 +372,16 @@ async function createTag(
ctx: Context,
operation: CreateOperation
): Promise<void> {
core.info(
`Tag '${operation.name}' does not exist, creating with commit SHA ${operation.sha}.`
const verb = ctx.dryRun ? 'Would create' : 'Creating'
ctx.log.info(
`${verb} tag '${operation.name}' at commit SHA ${operation.sha}` +
(operation.annotation ? ' (annotated).' : '.')
)
if (ctx.dryRun) {
return
}
const targetSha = await resolveTargetSHA(
ctx,
operation.name,
@@ -364,14 +400,14 @@ async function createTag(
/**
* Resolve the target SHA for a tag (creates annotated tag object if needed).
*
* @param ctx - Operation context
* @param ctx - Read-only operation context
* @param tagName - The tag name
* @param commitSha - The commit SHA
* @param annotation - The annotation message (if any)
* @returns The SHA to use (tag object SHA if annotated, commit SHA otherwise)
*/
async function resolveTargetSHA(
ctx: Context,
ctx: ReadContext,
tagName: string,
commitSha: string,
annotation: string

420
tests/derive.test.ts Normal file
View File

@@ -0,0 +1,420 @@
/**
* Unit tests for the derive module, src/derive.ts
*/
import { parseSemver, renderTemplate, deriveTags } from '../src/derive.js'
describe('parseSemver', () => {
it('parses simple version without prefix', () => {
const result = parseSemver('1.2.3')
expect(result).toEqual({
prefix: '',
major: 1,
minor: 2,
patch: 3,
prerelease: '',
build: '',
version: '1.2.3'
})
})
it('parses version with v prefix', () => {
const result = parseSemver('v1.2.3')
expect(result).toEqual({
prefix: 'v',
major: 1,
minor: 2,
patch: 3,
prerelease: '',
build: '',
version: '1.2.3'
})
})
it('parses version with uppercase V prefix', () => {
const result = parseSemver('V1.2.3')
expect(result).toEqual({
prefix: 'V',
major: 1,
minor: 2,
patch: 3,
prerelease: '',
build: '',
version: '1.2.3'
})
})
it('parses version with prerelease', () => {
const result = parseSemver('v1.0.0-beta.1')
expect(result).toEqual({
prefix: 'v',
major: 1,
minor: 0,
patch: 0,
prerelease: 'beta.1',
build: '',
version: '1.0.0-beta.1'
})
})
it('parses version with build metadata', () => {
const result = parseSemver('v1.0.0+build.456')
expect(result).toEqual({
prefix: 'v',
major: 1,
minor: 0,
patch: 0,
prerelease: '',
build: 'build.456',
version: '1.0.0'
})
})
it('parses version with both prerelease and build metadata', () => {
const result = parseSemver('v1.2.3-alpha.1+build.789')
expect(result).toEqual({
prefix: 'v',
major: 1,
minor: 2,
patch: 3,
prerelease: 'alpha.1',
build: 'build.789',
version: '1.2.3-alpha.1'
})
})
it('handles whitespace around version string', () => {
const result = parseSemver(' v1.2.3 ')
expect(result).toEqual({
prefix: 'v',
major: 1,
minor: 2,
patch: 3,
prerelease: '',
build: '',
version: '1.2.3'
})
})
it('throws on empty string', () => {
expect(() => parseSemver('')).toThrow('Invalid semver: empty string')
})
it('throws on whitespace only', () => {
expect(() => parseSemver(' ')).toThrow('Invalid semver: empty string')
})
it('throws on invalid semver', () => {
expect(() => parseSemver('not-a-version')).toThrow(
"Invalid semver: 'not-a-version'"
)
})
it('throws on incomplete version', () => {
expect(() => parseSemver('v1.2')).toThrow("Invalid semver: 'v1.2'")
})
})
describe('renderTemplate', () => {
const ctx = {
prefix: 'v',
major: 1,
minor: 2,
patch: 3,
prerelease: 'beta.1',
build: 'build.456',
version: '1.2.3-beta.1'
}
it('renders prefix placeholder', () => {
expect(renderTemplate('{{prefix}}', ctx)).toBe('v')
})
it('renders major placeholder', () => {
expect(renderTemplate('{{major}}', ctx)).toBe('1')
})
it('renders minor placeholder', () => {
expect(renderTemplate('{{minor}}', ctx)).toBe('2')
})
it('renders patch placeholder', () => {
expect(renderTemplate('{{patch}}', ctx)).toBe('3')
})
it('renders prerelease placeholder', () => {
expect(renderTemplate('{{prerelease}}', ctx)).toBe('beta.1')
})
it('renders build placeholder', () => {
expect(renderTemplate('{{build}}', ctx)).toBe('build.456')
})
it('renders version placeholder', () => {
expect(renderTemplate('{{version}}', ctx)).toBe('1.2.3-beta.1')
})
it('renders multiple placeholders', () => {
expect(renderTemplate('{{prefix}}{{major}}.{{minor}}', ctx)).toBe('v1.2')
})
it('renders same placeholder multiple times', () => {
expect(renderTemplate('{{major}}-{{major}}', ctx)).toBe('1-1')
})
it('preserves text without placeholders', () => {
expect(renderTemplate('latest', ctx)).toBe('latest')
})
it('handles empty prefix', () => {
const noPrefix = { ...ctx, prefix: '' }
expect(renderTemplate('{{prefix}}{{major}}', noPrefix)).toBe('1')
})
it('handles empty prerelease', () => {
const noPrerelease = { ...ctx, prerelease: '' }
expect(renderTemplate('{{major}}-{{prerelease}}', noPrerelease)).toBe('1-')
})
describe('Handlebars conditionals', () => {
it('renders {{#if}} section when variable has value', () => {
expect(
renderTemplate('{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}', ctx)
).toBe('1-beta.1')
})
it('omits {{#if}} section when variable is empty', () => {
const noPrerelease = { ...ctx, prerelease: '' }
expect(
renderTemplate(
'{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}',
noPrerelease
)
).toBe('1')
})
it('handles {{#if}} with zero-valued numbers (converted to strings)', () => {
const zeroMinor = { ...ctx, minor: 0 }
expect(
renderTemplate('{{major}}{{#if minor}}.{{minor}}{{/if}}', zeroMinor)
).toBe('1.0')
})
it('handles nested conditionals', () => {
expect(
renderTemplate(
'{{major}}{{#if prerelease}}-{{prerelease}}{{#if build}}+{{build}}{{/if}}{{/if}}',
ctx
)
).toBe('1-beta.1+build.456')
})
it('handles nested conditionals with inner empty', () => {
const noBuild = { ...ctx, build: '' }
expect(
renderTemplate(
'{{major}}{{#if prerelease}}-{{prerelease}}{{#if build}}+{{build}}{{/if}}{{/if}}',
noBuild
)
).toBe('1-beta.1')
})
it('handles {{#unless}} for inverse logic', () => {
const noPrerelease = { ...ctx, prerelease: '' }
expect(
renderTemplate('{{major}}{{#unless prerelease}}-stable{{/unless}}', ctx)
).toBe('1')
expect(
renderTemplate(
'{{major}}{{#unless prerelease}}-stable{{/unless}}',
noPrerelease
)
).toBe('1-stable')
})
it('handles multiple {{#if}} sections', () => {
const noBuild = { ...ctx, build: '' }
expect(
renderTemplate(
'{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}{{#if build}}+{{build}}{{/if}}',
noBuild
)
).toBe('1-beta.1')
})
})
})
describe('deriveTags', () => {
it('generates tags with default-style template', () => {
const result = deriveTags(
'v1.2.3',
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}'
)
expect(result).toEqual(['v1', 'v1.2'])
})
it('generates single tag from simple template', () => {
const result = deriveTags('v1.2.3', '{{prefix}}{{major}}')
expect(result).toEqual(['v1'])
})
it('handles version without prefix', () => {
const result = deriveTags(
'1.2.3',
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}'
)
expect(result).toEqual(['1', '1.2'])
})
it('handles prerelease in template', () => {
const result = deriveTags(
'v1.0.0-rc.1',
'{{prefix}}{{major}}-{{prerelease}}'
)
expect(result).toEqual(['v1-rc.1'])
})
it('filters empty tags and prefix-only tags from result', () => {
const result = deriveTags('v1.2.3', '{{prefix}}{{major}},,{{prefix}}')
expect(result).toEqual(['v1'])
})
it('trims whitespace around template parts', () => {
const result = deriveTags('v1.2.3', ' {{prefix}}{{major}} , latest ')
expect(result).toEqual(['v1', 'latest'])
})
it('generates full version tag', () => {
const result = deriveTags('v1.2.3', '{{prefix}}{{version}}')
expect(result).toEqual(['v1.2.3'])
})
it('throws on invalid semver', () => {
expect(() => deriveTags('invalid', '{{major}}')).toThrow(
"Invalid semver: 'invalid'"
)
})
it('handles newline-delimited template', () => {
const result = deriveTags(
'v1.2.3',
'{{prefix}}{{major}}\n{{prefix}}{{major}}.{{minor}}'
)
expect(result).toEqual(['v1', 'v1.2'])
})
it('handles mixed newlines and commas in template', () => {
const result = deriveTags(
'v1.2.3',
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}\nlatest'
)
expect(result).toEqual(['v1', 'v1.2', 'latest'])
})
describe('with Handlebars conditionals', () => {
it('generates tag with optional prerelease when present', () => {
const result = deriveTags(
'v1.0.0-beta.1',
'{{prefix}}{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}'
)
expect(result).toEqual(['v1-beta.1'])
})
it('generates tag without prerelease section when absent', () => {
const result = deriveTags(
'v1.0.0',
'{{prefix}}{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}'
)
expect(result).toEqual(['v1'])
})
it('generates multiple tags with conditionals', () => {
const result = deriveTags(
'v1.2.3-rc.1',
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}{{#if prerelease}}-{{prerelease}}{{/if}}'
)
expect(result).toEqual(['v1', 'v1.2-rc.1'])
})
it('handles build metadata conditional', () => {
const result = deriveTags(
'v1.0.0+build.123',
'{{prefix}}{{major}}{{#if build}}+{{build}}{{/if}}'
)
expect(result).toEqual(['v1+build.123'])
})
it('handles both prerelease and build conditionals', () => {
const result = deriveTags(
'v1.0.0-alpha.1+build.789',
'{{prefix}}{{major}}{{#if prerelease}}-{{prerelease}}{{/if}}{{#if build}}+{{build}}{{/if}}'
)
expect(result).toEqual(['v1-alpha.1+build.789'])
})
})
describe('conditional tag inclusion', () => {
it('includes latest tag only for stable releases', () => {
// Stable release gets latest tag
const stable = deriveTags(
'v1.2.3',
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}{{#unless prerelease}},latest{{/unless}}'
)
expect(stable).toEqual(['v1', 'v1.2', 'latest'])
// Prerelease does not get latest tag
const prerelease = deriveTags(
'v1.2.3-beta.1',
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}{{#unless prerelease}},latest{{/unless}}'
)
expect(prerelease).toEqual(['v1', 'v1.2'])
})
it('conditionally includes entire tag group with {{#if}}', () => {
// With prerelease: include prerelease-specific tags
const withPre = deriveTags(
'v2.0.0-rc.1',
'{{prefix}}{{major}}{{#if prerelease}},{{prefix}}{{major}}-{{prerelease}}{{/if}}'
)
expect(withPre).toEqual(['v2', 'v2-rc.1'])
// Without prerelease: only major tag
const withoutPre = deriveTags(
'v2.0.0',
'{{prefix}}{{major}}{{#if prerelease}},{{prefix}}{{major}}-{{prerelease}}{{/if}}'
)
expect(withoutPre).toEqual(['v2'])
})
it('conditionally includes newline-separated tags', () => {
// Stable release includes all tags
const stable = deriveTags(
'v1.0.0',
'{{prefix}}{{major}}\n{{prefix}}{{major}}.{{minor}}{{#unless prerelease}}\nstable{{/unless}}'
)
expect(stable).toEqual(['v1', 'v1.0', 'stable'])
// Prerelease excludes stable tag
const prerelease = deriveTags(
'v1.0.0-alpha.1',
'{{prefix}}{{major}}\n{{prefix}}{{major}}.{{minor}}{{#unless prerelease}}\nstable{{/unless}}'
)
expect(prerelease).toEqual(['v1', 'v1.0'])
})
it('excludes multiple tags conditionally', () => {
const result = deriveTags(
'v1.0.0-beta.1',
'{{prefix}}{{major}}{{#unless prerelease}},latest,stable,production{{/unless}}'
)
expect(result).toEqual(['v1'])
})
it('includes multiple tags conditionally for stable', () => {
const result = deriveTags(
'v1.0.0',
'{{prefix}}{{major}}{{#unless prerelease}},latest,stable,production{{/unless}}'
)
expect(result).toEqual(['v1', 'latest', 'stable', 'production'])
})
})
})

View File

@@ -4,7 +4,9 @@ import { jest } from '@jest/globals'
export const debug = jest.fn<typeof core.debug>()
export const error = jest.fn<typeof core.error>()
export const info = jest.fn<typeof core.info>()
export const notice = jest.fn<typeof core.notice>()
export const getInput = jest.fn<typeof core.getInput>()
export const getBooleanInput = jest.fn<typeof core.getBooleanInput>()
export const setOutput = jest.fn<typeof core.setOutput>()
export const setFailed = jest.fn<typeof core.setFailed>()
export const warning = jest.fn<typeof core.warning>()

193
tests/logger.test.ts Normal file
View File

@@ -0,0 +1,193 @@
/**
* Unit tests for the logger module, src/logger.ts
*/
import { jest } from '@jest/globals'
import * as core from './fixtures/core.js'
jest.unstable_mockModule('@actions/core', () => core)
const { createLogger } = await import('../src/logger.js')
describe('createLogger', () => {
beforeEach(() => {
jest.resetAllMocks()
})
describe('without prefix', () => {
it('calls core.debug with the message', () => {
const log = createLogger()
log.debug('test debug message')
expect(core.debug).toHaveBeenCalledWith('test debug message')
})
it('calls core.info with the message', () => {
const log = createLogger()
log.info('test info message')
expect(core.info).toHaveBeenCalledWith('test info message')
})
it('calls core.notice with string message', () => {
const log = createLogger()
log.notice('test notice message')
expect(core.notice).toHaveBeenCalledWith('test notice message', undefined)
})
it('calls core.notice with Error unchanged', () => {
const log = createLogger()
const error = new Error('test error')
log.notice(error)
expect(core.notice).toHaveBeenCalledWith(error, undefined)
})
it('calls core.notice with properties', () => {
const log = createLogger()
const props = { title: 'Notice Title' }
log.notice('test notice', props)
expect(core.notice).toHaveBeenCalledWith('test notice', props)
})
it('calls core.warning with string message', () => {
const log = createLogger()
log.warning('test warning message')
expect(core.warning).toHaveBeenCalledWith(
'test warning message',
undefined
)
})
it('calls core.warning with Error unchanged', () => {
const log = createLogger()
const error = new Error('warning error')
log.warning(error)
expect(core.warning).toHaveBeenCalledWith(error, undefined)
})
it('calls core.warning with properties', () => {
const log = createLogger()
const props = { title: 'Warning Title' }
log.warning('test warning', props)
expect(core.warning).toHaveBeenCalledWith('test warning', props)
})
it('calls core.error with string message', () => {
const log = createLogger()
log.error('test error message')
expect(core.error).toHaveBeenCalledWith('test error message', undefined)
})
it('calls core.error with Error unchanged', () => {
const log = createLogger()
const error = new Error('error error')
log.error(error)
expect(core.error).toHaveBeenCalledWith(error, undefined)
})
it('calls core.error with properties', () => {
const log = createLogger()
const props = { title: 'Error Title' }
log.error('test error', props)
expect(core.error).toHaveBeenCalledWith('test error', props)
})
})
describe('with prefix', () => {
it('prefixes debug messages', () => {
const log = createLogger('[dry-run] ')
log.debug('test debug message')
expect(core.debug).toHaveBeenCalledWith('[dry-run] test debug message')
})
it('prefixes info messages', () => {
const log = createLogger('[dry-run] ')
log.info('test info message')
expect(core.info).toHaveBeenCalledWith('[dry-run] test info message')
})
it('prefixes notice string messages', () => {
const log = createLogger('[dry-run] ')
log.notice('test notice message')
expect(core.notice).toHaveBeenCalledWith(
'[dry-run] test notice message',
undefined
)
})
it('wraps notice Error with prefixed message and cause', () => {
const log = createLogger('[dry-run] ')
const original = new Error('notice error')
log.notice(original)
expect(core.notice).toHaveBeenCalledTimes(1)
const [wrapped, props] = core.notice.mock.calls[0]
expect(wrapped).toBeInstanceOf(Error)
expect((wrapped as Error).message).toBe('[dry-run] notice error')
expect((wrapped as Error).cause).toBe(original)
expect((wrapped as Error).stack).toBe(original.stack)
expect(props).toBeUndefined()
})
it('prefixes warning string messages', () => {
const log = createLogger('[dry-run] ')
log.warning('test warning message')
expect(core.warning).toHaveBeenCalledWith(
'[dry-run] test warning message',
undefined
)
})
it('wraps warning Error with prefixed message and cause', () => {
const log = createLogger('[dry-run] ')
const original = new Error('warning error')
log.warning(original)
expect(core.warning).toHaveBeenCalledTimes(1)
const [wrapped, props] = core.warning.mock.calls[0]
expect(wrapped).toBeInstanceOf(Error)
expect((wrapped as Error).message).toBe('[dry-run] warning error')
expect((wrapped as Error).cause).toBe(original)
expect((wrapped as Error).stack).toBe(original.stack)
expect(props).toBeUndefined()
})
it('prefixes error string messages', () => {
const log = createLogger('[dry-run] ')
log.error('test error message')
expect(core.error).toHaveBeenCalledWith(
'[dry-run] test error message',
undefined
)
})
it('wraps error Error with prefixed message and cause', () => {
const log = createLogger('[dry-run] ')
const original = new Error('error error')
log.error(original)
expect(core.error).toHaveBeenCalledTimes(1)
const [wrapped, props] = core.error.mock.calls[0]
expect(wrapped).toBeInstanceOf(Error)
expect((wrapped as Error).message).toBe('[dry-run] error error')
expect((wrapped as Error).cause).toBe(original)
expect((wrapped as Error).stack).toBe(original.stack)
expect(props).toBeUndefined()
})
it('preserves properties when prefixing', () => {
const log = createLogger('[test] ')
const props = { title: 'Test Title', file: 'test.ts', startLine: 10 }
log.warning('message with props', props)
expect(core.warning).toHaveBeenCalledWith(
'[test] message with props',
props
)
})
})
describe('with empty prefix', () => {
it('behaves the same as no prefix', () => {
const log = createLogger('')
log.info('test message')
expect(core.info).toHaveBeenCalledWith('test message')
})
})
})

View File

@@ -16,9 +16,14 @@ jest.unstable_mockModule('csv-parse/sync', () => csvParse)
const { run } = await import('../src/main.js')
// Helper functions for cleaner test setup
const setupInputs = (inputs: Record<string, string>): void => {
const setupInputs = (inputs: Record<string, string | boolean>): void => {
core.getInput.mockImplementation((name: string) => {
return inputs[name] || ''
const value = inputs[name]
return typeof value === 'string' ? value : ''
})
core.getBooleanInput.mockImplementation((name: string) => {
const value = inputs[name]
return typeof value === 'boolean' ? value : false
})
}
@@ -123,7 +128,7 @@ describe('run', () => {
})
expect(core.info).toHaveBeenCalledWith(
"Tag 'v1' does not exist, creating with commit SHA sha-abc123."
"Creating tag 'v1' at commit SHA sha-abc123."
)
expect(getOutputs()).toEqual({
created: ['v1', 'v1.0'],
@@ -155,7 +160,7 @@ describe('run', () => {
})
expect(core.info).toHaveBeenCalledWith(
"Tag 'v1' exists, updating to commit SHA sha-def456 (was sha-old123)."
"Updating tag 'v1' to commit SHA sha-def456 (was sha-old123)."
)
expect(getOutputs()).toEqual({
created: [],
@@ -533,22 +538,42 @@ describe('run', () => {
})
})
it('fails when tag specification has multiple colons', async () => {
it('creates annotated tag with per-tag annotation syntax', async () => {
setupInputs({
tags: 'stable:refs/heads/main:latest',
tags: 'stable:refs/heads/main:Release annotation',
ref: '',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver({ 'refs/heads/main': 'sha-main' })
setupTagDoesNotExist()
github.mockOctokit.rest.git.createTag.mockResolvedValue({
data: { sha: 'sha-tag-object-stable' }
})
await run()
expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('Invalid tag specification')
)
expect(core.setFailed).toHaveBeenCalledWith(
expect.stringContaining('too many colons')
)
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'stable',
message: 'Release annotation',
object: 'sha-main',
type: 'commit'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/stable',
sha: 'sha-tag-object-stable'
})
expect(getOutputs()).toEqual({
created: ['stable'],
updated: [],
skipped: [],
tags: ['stable']
})
})
it('handles mixed scenario with multiple tags', async () => {
@@ -1219,4 +1244,645 @@ describe('run', () => {
tags: ['v1']
})
})
describe('dry-run mode', () => {
it('logs planned creates without executing them', async () => {
setupInputs({
tags: 'v1,v1.0',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
// Should NOT call createRef in dry-run mode
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
// Should log dry-run messages
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v1' at commit SHA sha-abc123."
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v1.0' at commit SHA sha-abc123."
)
// Outputs should be empty in dry-run mode
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('logs planned updates without executing them', async () => {
setupInputs({
tags: 'v1',
ref: 'def456',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-def456')
setupTagExistsForAll('sha-old123')
await run()
// Should NOT call updateRef in dry-run mode
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
// Should log dry-run messages
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would update tag 'v1' " +
'to commit SHA sha-def456 (was sha-old123).'
)
// Outputs should be empty in dry-run mode
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('logs skipped tags with dry-run prefix', async () => {
setupInputs({
tags: 'v1',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-abc123')
setupTagExistsForAll('sha-abc123')
await run()
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Tag 'v1' already exists with desired commit SHA sha-abc123."
)
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('handles mixed operations in dry-run mode', async () => {
setupInputs({
tags: 'v1,v2,v3',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-abc123')
// v1 exists with different SHA, v2 matches, v3 doesn't exist
github.mockOctokit.rest.git.getRef.mockImplementation(
async (args: unknown) => {
const { ref } = args as { ref: string }
if (ref === 'tags/v1') {
return {
data: {
ref: 'refs/tags/v1',
object: { sha: 'sha-old', type: 'commit' }
}
}
}
if (ref === 'tags/v2') {
return {
data: {
ref: 'refs/tags/v2',
object: { sha: 'sha-abc123', type: 'commit' }
}
}
}
throw { status: 404 }
}
)
await run()
// No actual operations should happen
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
// Should log all planned operations
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would update tag 'v1' to commit SHA sha-abc123 (was sha-old)."
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Tag 'v2' already exists with desired commit SHA sha-abc123."
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v3' at commit SHA sha-abc123."
)
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('logs annotated tag creation in dry-run mode', async () => {
setupInputs({
tags: 'v1',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
annotation: 'Release v1',
dry_run: true
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
expect(github.mockOctokit.rest.git.createTag).not.toHaveBeenCalled()
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v1' at commit SHA sha-abc123 (annotated)."
)
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('executes normally when dry_run is false', async () => {
setupInputs({
tags: 'v1',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: false
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
// Should actually create the tag
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(1)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['v1'],
updated: [],
skipped: [],
tags: ['v1']
})
})
})
describe('derive_from input', () => {
it('derives tags from semver using default template', async () => {
setupInputs({
derive_from: 'v1.2.3',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1',
sha: 'sha-abc123'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1.2',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['v1', 'v1.2'],
updated: [],
skipped: [],
tags: ['v1', 'v1.2']
})
})
it('derives tags using custom template', async () => {
setupInputs({
derive_from: 'v2.5.0',
derive_from_template: '{{prefix}}{{major}}',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(1)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v2',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['v2'],
updated: [],
skipped: [],
tags: ['v2']
})
})
it('combines derive_from with explicit tags', async () => {
setupInputs({
tags: 'latest',
derive_from: 'v1.0.0',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(3)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/latest',
sha: 'sha-abc123'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1',
sha: 'sha-abc123'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1.0',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['latest', 'v1', 'v1.0'],
updated: [],
skipped: [],
tags: ['latest', 'v1', 'v1.0']
})
})
it('handles version without v prefix', async () => {
setupInputs({
derive_from: '3.0.0',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/3',
sha: 'sha-abc123'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/3.0',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['3', '3.0'],
updated: [],
skipped: [],
tags: ['3', '3.0']
})
})
it('fails when neither tags nor derive_from is provided', async () => {
setupInputs({
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
await run()
expect(core.setFailed).toHaveBeenCalledWith(
"No tags specified. Provide 'tags' input, 'derive_from' input, or both."
)
})
it('fails on invalid semver in derive_from', async () => {
setupInputs({
derive_from: 'not-a-version',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
await run()
expect(core.setFailed).toHaveBeenCalledWith(
"Invalid semver: 'not-a-version'"
)
})
it('deduplicates when tags and derive_from produce overlapping tags', async () => {
setupInputs({
tags: 'v1,v1.2',
derive_from: 'v1.2.3', // Derives v1 and v1.2, overlapping with explicit
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
// Should only create 2 tags (deduplicated), not 4
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1',
sha: 'sha-abc123'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1.2',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['v1', 'v1.2'],
updated: [],
skipped: [],
tags: ['v1', 'v1.2']
})
})
it('fails when tags and derive_from produce same tag with different refs', async () => {
setupInputs({
tags: 'v1:main', // Explicit v1 pointing to main
derive_from: 'v1.2.3', // Derives v1 (using default ref)
ref: 'develop', // Different default ref
github_token: 'test-token',
when_exists: 'update'
})
await run()
expect(core.setFailed).toHaveBeenCalledWith(
"Duplicate tag 'v1' with different refs: 'main' and 'develop'"
)
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
})
})
describe('per-tag annotations', () => {
it('creates tag with per-tag annotation using empty ref fallback', async () => {
setupInputs({
tags: 'v1::Per-tag annotation',
ref: 'main',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
github.mockOctokit.rest.git.createTag.mockResolvedValue({
data: { sha: 'sha-tag-object-v1' }
})
await run()
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'v1',
message: 'Per-tag annotation',
object: 'sha-main',
type: 'commit'
})
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1',
sha: 'sha-tag-object-v1'
})
expect(getOutputs()).toEqual({
created: ['v1'],
updated: [],
skipped: [],
tags: ['v1']
})
})
it('handles per-tag annotation containing colons', async () => {
setupInputs({
tags: 'v1:main:Release: version 1.0.0: stable',
ref: '',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver({ main: 'sha-main' })
setupTagDoesNotExist()
github.mockOctokit.rest.git.createTag.mockResolvedValue({
data: { sha: 'sha-tag-object-v1' }
})
await run()
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'v1',
message: 'Release: version 1.0.0: stable',
object: 'sha-main',
type: 'commit'
})
})
it('mixes per-tag annotations with global annotation', async () => {
setupInputs({
tags: 'v1:main:Custom message,v2',
ref: 'main',
github_token: 'test-token',
when_exists: 'update',
annotation: 'Global annotation'
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
github.mockOctokit.rest.git.createTag.mockImplementation(
async (args: unknown) => {
const { tag } = args as { tag: string }
return { data: { sha: `sha-tag-object-${tag}` } }
}
)
await run()
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledTimes(2)
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'v1',
message: 'Custom message',
object: 'sha-main',
type: 'commit'
})
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'v2',
message: 'Global annotation',
object: 'sha-main',
type: 'commit'
})
})
it('per-tag annotation overrides global annotation', async () => {
setupInputs({
tags: 'v1:main:Per-tag wins',
ref: 'main',
github_token: 'test-token',
when_exists: 'update',
annotation: 'Global annotation should be ignored'
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
github.mockOctokit.rest.git.createTag.mockResolvedValue({
data: { sha: 'sha-tag-object-v1' }
})
await run()
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'v1',
message: 'Per-tag wins',
object: 'sha-main',
type: 'commit'
})
})
it('updates tag when per-tag annotation differs from existing', async () => {
setupInputs({
tags: 'v1:main:New annotation',
ref: '',
github_token: 'test-token',
when_exists: 'update'
})
setupCommitResolver({ main: 'sha-main' })
// Tag exists as annotated with different message but same commit
github.mockOctokit.rest.git.getRef.mockResolvedValue({
data: {
ref: 'refs/tags/v1',
object: { sha: 'sha-tag-object-old', type: 'tag' }
}
})
github.mockOctokit.rest.git.getTag.mockResolvedValue({
data: {
sha: 'sha-tag-object-old',
message: 'Old annotation',
object: { sha: 'sha-main', type: 'commit' }
}
})
github.mockOctokit.rest.git.createTag.mockResolvedValue({
data: { sha: 'sha-tag-object-new' }
})
await run()
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
tag: 'v1',
message: 'New annotation',
object: 'sha-main',
type: 'commit'
})
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'tags/v1',
sha: 'sha-tag-object-new',
force: true
})
expect(getOutputs()).toEqual({
created: [],
updated: ['v1'],
skipped: [],
tags: ['v1']
})
})
it('fails when empty tag name with per-tag annotation', async () => {
setupInputs({
tags: '::Some annotation',
ref: 'main',
github_token: 'test-token'
})
await run()
expect(core.setFailed).toHaveBeenCalledWith(
"Invalid tag: '::Some annotation'"
)
})
})
})

View File

@@ -4,14 +4,262 @@
import { jest } from '@jest/globals'
import * as core from './fixtures/core.js'
import * as github from './fixtures/github.js'
import type { Inputs } from '../src/inputs.js'
// Mocks should be declared before the module being tested is imported.
jest.unstable_mockModule('@actions/core', () => core)
jest.unstable_mockModule('@actions/github', () => github)
// The module being tested should be imported dynamically.
const { executeTagOperation } = await import('../src/tags.js')
import type { TagOperation } from '../src/tags.js'
const { executeTagOperation, planTagOperations } =
await import('../src/tags.js')
import type {
TagOperation,
CreateOperation,
UpdateOperation
} from '../src/tags.js'
// Helper to create a minimal Inputs object for testing
const createInputs = (overrides: Partial<Inputs> = {}): Inputs => ({
tags: [],
defaultRef: 'main',
whenExists: 'update',
annotation: '',
dryRun: false,
owner: 'test-owner',
repo: 'test-repo',
token: 'test-token',
...overrides
})
describe('planTagOperations', () => {
beforeEach(() => {
jest.resetAllMocks()
github.getOctokit.mockReturnValue(github.mockOctokit)
})
const setupCommitResolver = (
refToSha: Record<string, string> | string
): void => {
if (typeof refToSha === 'string') {
github.mockOctokit.rest.repos.getCommit.mockResolvedValue({
data: { sha: refToSha }
})
} else {
github.mockOctokit.rest.repos.getCommit.mockImplementation(
async (args: unknown) => {
const { ref } = args as { ref: string }
const sha = refToSha[ref]
if (sha) return { data: { sha } }
throw new Error(`Unknown ref: ${ref}`)
}
)
}
}
const setupTagDoesNotExist = (): void => {
github.mockOctokit.rest.git.getRef.mockRejectedValue({
status: 404
})
}
describe('per-tag annotations', () => {
it('parses per-tag annotation with explicit ref', async () => {
const inputs = createInputs({
tags: ['v1:main:Custom annotation'],
annotation: 'Global annotation'
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(1)
expect(operations[0].operation).toBe('create')
expect((operations[0] as CreateOperation).annotation).toBe(
'Custom annotation'
)
})
it('parses per-tag annotation with empty ref (fallback to default)', async () => {
const inputs = createInputs({
tags: ['v1::Custom annotation'],
defaultRef: 'develop',
annotation: 'Global annotation'
})
setupCommitResolver('sha-develop')
setupTagDoesNotExist()
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(1)
expect(operations[0].operation).toBe('create')
expect(operations[0].ref).toBe('develop')
expect((operations[0] as CreateOperation).annotation).toBe(
'Custom annotation'
)
})
it('handles annotation containing colons', async () => {
const inputs = createInputs({
tags: ['v1:main:Release: v1.0.0'],
annotation: ''
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(1)
expect((operations[0] as CreateOperation).annotation).toBe(
'Release: v1.0.0'
)
})
it('falls back to global annotation when per-tag not specified', async () => {
const inputs = createInputs({
tags: ['v1', 'v2:main'],
annotation: 'Global annotation'
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(2)
expect((operations[0] as CreateOperation).annotation).toBe(
'Global annotation'
)
expect((operations[1] as CreateOperation).annotation).toBe(
'Global annotation'
)
})
it('mixes per-tag and global annotations', async () => {
const inputs = createInputs({
tags: ['v1:main:Per-tag message', 'v2'],
annotation: 'Global annotation'
})
setupCommitResolver('sha-main')
setupTagDoesNotExist()
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(2)
expect((operations[0] as CreateOperation).annotation).toBe(
'Per-tag message'
)
expect((operations[1] as CreateOperation).annotation).toBe(
'Global annotation'
)
})
it('uses per-tag annotation for update comparison', async () => {
const inputs = createInputs({
tags: ['v1:main:New annotation'],
annotation: 'Global annotation',
whenExists: 'update'
})
setupCommitResolver('sha-main')
// Tag exists with same commit but different annotation
github.mockOctokit.rest.git.getRef.mockResolvedValue({
data: {
ref: 'refs/tags/v1',
object: { sha: 'sha-tag-object', type: 'tag' }
}
})
github.mockOctokit.rest.git.getTag.mockResolvedValue({
data: {
sha: 'sha-tag-object',
message: 'Old annotation',
object: { sha: 'sha-main', type: 'commit' }
}
})
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(1)
expect(operations[0].operation).toBe('update')
expect((operations[0] as UpdateOperation).annotation).toBe(
'New annotation'
)
expect((operations[0] as UpdateOperation).reasons).toContain(
'annotation message changed'
)
})
it('skips tag when per-tag annotation matches existing', async () => {
const inputs = createInputs({
tags: ['v1:main:Same annotation'],
annotation: 'Global annotation',
whenExists: 'update'
})
setupCommitResolver('sha-main')
github.mockOctokit.rest.git.getRef.mockResolvedValue({
data: {
ref: 'refs/tags/v1',
object: { sha: 'sha-tag-object', type: 'tag' }
}
})
github.mockOctokit.rest.git.getTag.mockResolvedValue({
data: {
sha: 'sha-tag-object',
message: 'Same annotation',
object: { sha: 'sha-main', type: 'commit' }
}
})
const operations = await planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
expect(operations).toHaveLength(1)
expect(operations[0].operation).toBe('skip')
})
it('rejects empty tag name with annotation', async () => {
const inputs = createInputs({
tags: ['::Some annotation']
})
await expect(
planTagOperations(
inputs,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
github.mockOctokit as any
)
).rejects.toThrow("Invalid tag: '::Some annotation'")
})
})
})
describe('executeTagOperation', () => {
beforeEach(() => {