mirror of
https://github.com/jimeh/update-tags-action.git
synced 2026-02-19 09:36:41 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46ebed28cc | ||
|
|
04e502b026 | ||
|
|
fa63dca88c | ||
|
|
6593b10eb9 | ||
| daa038fd7f | |||
| 5580fd8611 | |||
| aa16a0ff75 | |||
| 741ed40bb7 | |||
| a25f37f9e6 | |||
|
|
82dc01ff9e | ||
|
|
1d0890efd6 | ||
|
|
fcb6d5925b | ||
|
|
ecd1432d26 | ||
|
|
73adb32d6a | ||
|
|
0b1a9ea25d | ||
|
|
88c489d592 | ||
|
|
4e16af70e2 | ||
|
|
88edfaa38b | ||
|
|
838b7bd8a1 | ||
|
|
2df44e1d6e | ||
|
|
e33b26678f | ||
| ce60e958b3 | |||
|
|
441aa939fd | ||
| af088380e8 | |||
|
|
d2bac049db | ||
| b18dece8b1 | |||
| 5f14685d44 | |||
| 605c1cfa34 | |||
| bd3bacec30 | |||
| 389bd20c41 | |||
| 38ccbb6879 | |||
| 9016fb217e | |||
|
|
ca138ceed8 | ||
|
|
eecd8caae9 | ||
| 6723e4d4ac | |||
| 92bdad7a4a | |||
| 1576a544fe | |||
| 2b1c01b3ed | |||
| 73e6309596 |
2
.github/.release-please-manifest.json
vendored
2
.github/.release-please-manifest.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.1.1"
|
||||
".": "2.2.1"
|
||||
}
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -6,8 +6,9 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
actions-minor:
|
||||
actions:
|
||||
update-types:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
- package-ecosystem: npm
|
||||
@@ -18,9 +19,12 @@ updates:
|
||||
npm-development:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
npm-production:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -16,8 +16,8 @@ jobs:
|
||||
check-dist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
fi
|
||||
- name: Upload Artifact
|
||||
if: ${{ failure() && steps.diff.outcome == 'failure' }}
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
@@ -51,10 +51,10 @@ jobs:
|
||||
packages: read
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
@@ -64,10 +64,10 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
@@ -85,8 +85,8 @@ jobs:
|
||||
- uses: jimeh/release-please-manifest-action@84f33fd2828210488c36f3e0a7e3209252d2ae7d # v3.0.0
|
||||
id: release-please
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||
|
||||
release-tags:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: jimeh/update-tags-action@bf34cb3d0919fe9e601539e11a89b250e00e9cc3 # v2.0.0
|
||||
- uses: jimeh/update-tags-action@eecd8caae9a536ed536cff9b2b7f0bd187f67c13 # v2.2.0
|
||||
with:
|
||||
tags: |
|
||||
v${{ needs.release-please.outputs.major }}
|
||||
|
||||
6
.github/workflows/copilot-setup-steps.yml
vendored
6
.github/workflows/copilot-setup-steps.yml
vendored
@@ -15,13 +15,13 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0
|
||||
- uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0
|
||||
with:
|
||||
ruby-version: ruby
|
||||
bundler-cache: true
|
||||
|
||||
61
.github/workflows/dependabot-rebuild.yml
vendored
Normal file
61
.github/workflows/dependabot-rebuild.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: Dependabot Rebuild
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
rebuild:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' && github.event.sender.login == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Generate app token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
# Update and push dist if changed.
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Rebuild dist
|
||||
run: npm run bundle
|
||||
- name: Commit and push if changed
|
||||
uses: ryancyq/github-signed-commit@e9f3b28c80da7be66d24b8f501a5abe82a6b855f # v1.2.0
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
files: |
|
||||
dist/
|
||||
commit-message: |-
|
||||
chore: rebuild dist
|
||||
|
||||
# Update and push .licenses if changed.
|
||||
- uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0
|
||||
with:
|
||||
ruby-version: ruby
|
||||
bundler-cache: true
|
||||
- name: Update Licenses
|
||||
id: update-licenses
|
||||
run: bin/licensed cache
|
||||
- name: Commit and push if changed
|
||||
uses: ryancyq/github-signed-commit@e9f3b28c80da7be66d24b8f501a5abe82a6b855f # v1.2.0
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
files: |
|
||||
.licenses/
|
||||
commit-message: |-
|
||||
chore(licensed): update license files
|
||||
36
.github/workflows/licensed.yml
vendored
36
.github/workflows/licensed.yml
vendored
@@ -20,13 +20,24 @@ jobs:
|
||||
check-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
# Checkout code using app token.
|
||||
- name: Generate app token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
# Setup Node and Ruby runtimes.
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0
|
||||
- uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0
|
||||
with:
|
||||
ruby-version: ruby
|
||||
bundler-cache: true
|
||||
@@ -36,17 +47,16 @@ jobs:
|
||||
name: Update Licenses
|
||||
id: update-licenses
|
||||
run: bin/licensed cache
|
||||
|
||||
# Then, commit the updated licenses to the repository.
|
||||
- if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
name: Commit Licenses
|
||||
id: commit-licenses
|
||||
run: |
|
||||
git config --local user.email "licensed-ci@users.noreply.github.com"
|
||||
git config --local user.name "licensed-ci"
|
||||
git add .
|
||||
git commit -m "Auto-update license files"
|
||||
git push
|
||||
name: Commit and push if changed
|
||||
uses: ryancyq/github-signed-commit@e9f3b28c80da7be66d24b8f501a5abe82a6b855f # v1.2.0
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
with:
|
||||
files: |
|
||||
.licenses/
|
||||
commit-message: |-
|
||||
chore(licensed): update license files
|
||||
|
||||
# Last, check the status of the cached licenses.
|
||||
- name: Check Licenses
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -98,7 +98,7 @@ typings/
|
||||
Thumbs.db
|
||||
|
||||
# Ignore built ts files
|
||||
__tests__/runner/*
|
||||
tests/runner/*
|
||||
|
||||
# IDE files
|
||||
.idea
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/core"
|
||||
version: 1.11.1
|
||||
version: 2.0.1
|
||||
type: npm
|
||||
summary: Actions core lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/exec"
|
||||
version: 1.1.1
|
||||
version: 2.0.0
|
||||
type: npm
|
||||
summary: Actions exec lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/exec
|
||||
|
||||
32
.licenses/npm/@actions/http-client-3.0.0.dep.yml
Normal file
32
.licenses/npm/@actions/http-client-3.0.0.dep.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
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: []
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: "@actions/io"
|
||||
version: 1.1.3
|
||||
version: 2.0.0
|
||||
type: npm
|
||||
summary: Actions io lib
|
||||
homepage: https://github.com/actions/toolkit/tree/main/packages/io
|
||||
|
||||
2
.mise.toml
Normal file
2
.mise.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
idiomatic_version_file_enable_tools = ["ruby", "node"]
|
||||
@@ -1 +1 @@
|
||||
24.4.0
|
||||
24.12.0
|
||||
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
3.4.8
|
||||
9
.vscode/settings.shared.json
vendored
9
.vscode/settings.shared.json
vendored
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
||||
48
AGENTS.md
48
AGENTS.md
@@ -21,7 +21,7 @@ up-to-date. Always run `npm run package` (or `npm run bundle`) after modifying
|
||||
|
||||
## Development Commands
|
||||
|
||||
Package manager: npm (Node 24 via mise.toml)
|
||||
Package manager: npm
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
@@ -35,7 +35,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 npx jest tests/main.test.ts
|
||||
|
||||
# CI variants (suppress warnings)
|
||||
npm run ci-test # Run tests in CI mode
|
||||
@@ -59,13 +59,14 @@ 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/tags.ts](src/tags.ts)**: Tag parsing and processing logic:
|
||||
- `parseTagsInput()`: Parses CSV/newline input, handles `tag:ref` syntax,
|
||||
pre-resolves all unique refs to SHAs in parallel (optimization)
|
||||
- `processTag()`: Creates/updates individual tags based on `when_exists` mode
|
||||
- `resolveRefToSha()`: Converts git refs to commit SHAs (private helper)
|
||||
- **[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
|
||||
- `executeTagOperation()`: Executes a single planned operation (create,
|
||||
update, or skip with logging)
|
||||
- Private helpers for tag creation, updates, and annotation handling
|
||||
- **[action.yml](action.yml)**: GitHub Action metadata (inputs/outputs)
|
||||
- **[\_\_fixtures\_\_/](__fixtures__)**: Mock implementations of @actions/core,
|
||||
- **[tests/fixtures/](tests/fixtures)**: Mock implementations of @actions/core,
|
||||
@actions/github, and csv-parse for testing
|
||||
|
||||
### Tag Input Parsing
|
||||
@@ -76,14 +77,16 @@ per-tag ref overrides: `v1:main` tags `v1` to `main` branch.
|
||||
### Tag Update Logic
|
||||
|
||||
1. Parse and validate inputs ([inputs.ts](src/inputs.ts))
|
||||
2. Parse tags and extract per-tag refs ([tags.ts](src/tags.ts):parseTagsInput)
|
||||
3. Pre-resolve all unique refs to SHAs in parallel (optimization)
|
||||
4. For each tag ([tags.ts](src/tags.ts):processTag):
|
||||
- If exists + update mode: Update if SHA differs
|
||||
- If exists + skip mode: Skip silently
|
||||
- If exists + fail mode: Fail action
|
||||
- If doesn't exist (404): Create it
|
||||
5. Set outputs with created/updated tag lists ([main.ts](src/main.ts))
|
||||
2. Plan all tag operations ([tags.ts](src/tags.ts):planTagOperations):
|
||||
- Parse `tag:ref` syntax and extract per-tag refs
|
||||
- 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
|
||||
- If exists + skip mode: Plan skip
|
||||
- If exists + update mode: Plan update if SHA or annotation differs
|
||||
- If doesn't exist (404): Plan create
|
||||
3. Execute each planned operation ([tags.ts](src/tags.ts):executeTagOperation)
|
||||
4. Set outputs with created/updated/skipped tag lists ([main.ts](src/main.ts))
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
@@ -97,12 +100,12 @@ jest.unstable_mockModule('@actions/core', () => core)
|
||||
const { run } = await import('../src/main.ts')
|
||||
```
|
||||
|
||||
Mock fixtures live in `__fixtures__/` (e.g., `core.ts` mocks @actions/core).
|
||||
Mock fixtures live in `tests/fixtures/` (e.g., `core.ts` mocks @actions/core).
|
||||
|
||||
### Testing Best Practices
|
||||
|
||||
- Consider edge cases as well as the main success path
|
||||
- Tests live in `__tests__/` directory, fixtures in `__fixtures__/`
|
||||
- Tests live in `tests/` directory, fixtures in `tests/fixtures/`
|
||||
- Run tests after any refactoring to ensure coverage requirements are met
|
||||
- Use `@actions/core` package for logging (not `console`) for GitHub Actions
|
||||
compatibility
|
||||
@@ -119,9 +122,10 @@ Mock fixtures live in `__fixtures__/` (e.g., `core.ts` mocks @actions/core).
|
||||
`.github/workflows/ci.yml` runs:
|
||||
|
||||
1. **check-dist**: Verify bundled dist/ matches source
|
||||
2. **lint**: ESLint with GitHub formatter
|
||||
3. **release-please**: Semantic versioning releases
|
||||
4. **release-tags**: Self-referential tag updates after release
|
||||
2. **lint**: ESLint check
|
||||
3. **test**: Run Jest test suite
|
||||
4. **release-please**: Semantic versioning releases
|
||||
5. **release-tags**: Self-referential tag updates after release
|
||||
|
||||
## Release Process
|
||||
|
||||
@@ -187,6 +191,7 @@ chore(deps): bump @actions/core to v1.10.0
|
||||
- `tags`: CSV/newline list, supports `tag:ref` syntax
|
||||
- `ref`: SHA/ref to tag (default: current commit)
|
||||
- `when_exists`: update|skip|fail (default: update)
|
||||
- `annotation`: Optional message for annotated tags (default: lightweight)
|
||||
- `github_token`: Auth token (default: github.token)
|
||||
|
||||
**Outputs:**
|
||||
@@ -194,6 +199,7 @@ chore(deps): bump @actions/core to v1.10.0
|
||||
- `tags`: All created/updated tags
|
||||
- `created`: Newly created tags
|
||||
- `updated`: Updated tags
|
||||
- `skipped`: Skipped tags (already matching or when_exists=skip)
|
||||
|
||||
## Code Style and Guidelines
|
||||
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [2.2.1](https://github.com/jimeh/update-tags-action/compare/v2.2.0...v2.2.1) (2025-12-22)
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **AGENTS.md:** update to align with current state of project ([#71](https://github.com/jimeh/update-tags-action/issues/71)) ([a25f37f](https://github.com/jimeh/update-tags-action/commit/a25f37f9e6300ff3fcdf28424cdec9d99944522f))
|
||||
|
||||
## [2.2.0](https://github.com/jimeh/update-tags-action/compare/v2.1.1...v2.2.0) (2025-10-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **action:** add skipped tags output and tracking ([#33](https://github.com/jimeh/update-tags-action/issues/33)) ([6723e4d](https://github.com/jimeh/update-tags-action/commit/6723e4d4aceb3ba7314907830d8b1d5186f0a5d9))
|
||||
|
||||
## [2.1.1](https://github.com/jimeh/update-tags-action/compare/v2.1.0...v2.1.1) (2025-10-28)
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ to move its own major and minor tags.
|
||||
```yaml
|
||||
- uses: jimeh/update-tags-action@v2
|
||||
with:
|
||||
tags: v2,v2.1
|
||||
tags: v2,v2.2
|
||||
```
|
||||
|
||||
```yaml
|
||||
@@ -35,7 +35,7 @@ to move its own major and minor tags.
|
||||
with:
|
||||
tags: |
|
||||
v2
|
||||
v2.1
|
||||
v2.2
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
@@ -122,6 +122,7 @@ jobs:
|
||||
| `tags` | <p>List of tags that were created/updated.</p> |
|
||||
| `created` | <p>List of tags that were created.</p> |
|
||||
| `updated` | <p>List of tags that were updated.</p> |
|
||||
| `skipped` | <p>List of tags that were skipped.</p> |
|
||||
|
||||
<!-- action-docs-outputs source="action.yml" -->
|
||||
<!-- action-docs-runs source="action.yml" -->
|
||||
|
||||
@@ -38,6 +38,8 @@ outputs:
|
||||
description: "List of tags that were created."
|
||||
updated:
|
||||
description: "List of tags that were updated."
|
||||
skipped:
|
||||
description: "List of tags that were skipped."
|
||||
|
||||
runs:
|
||||
using: node24
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 94.44%"><title>Coverage: 94.44%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">94.44%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">94.44%</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="Coverage: 100%"><title>Coverage: 100%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="43" height="20" fill="#4c1"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text><text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="330">100%</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
34431
dist/index.js
generated
vendored
34431
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
585
package-lock.json
generated
585
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "update-tags-action",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.1",
|
||||
"author": "jimeh",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -41,35 +41,34 @@
|
||||
"update-readme": "npx action-docs --update-readme && npx prettier --write README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/core": "^2.0.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"csv-parse": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@jest/globals": "^30.2.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.9",
|
||||
"@rollup/plugin-commonjs": "^29.0.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||
"@types/node": "^25.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"action-docs": "^2.5.1",
|
||||
"eslint": "^9.38.0",
|
||||
"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.0.1",
|
||||
"eslint-plugin-jest": "^29.9.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"jest": "^30.2.0",
|
||||
"make-coverage-badge": "^1.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-eslint": "^16.4.2",
|
||||
"rollup": "^4.52.5",
|
||||
"ts-jest": "^29.4.5",
|
||||
"rollup": "^4.54.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-jest-resolver": "^2.0.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import nodeResolve from '@rollup/plugin-node-resolve'
|
||||
import terser from '@rollup/plugin-terser'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
|
||||
const config = {
|
||||
@@ -13,12 +12,7 @@ const config = {
|
||||
format: 'es',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
typescript(),
|
||||
nodeResolve({ preferBuiltins: true }),
|
||||
commonjs(),
|
||||
terser()
|
||||
]
|
||||
plugins: [typescript(), nodeResolve({ preferBuiltins: true }), commonjs()]
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
49
src/main.ts
49
src/main.ts
@@ -1,7 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { getInputs } from './inputs.js'
|
||||
import { resolveDesiredTags, processTag } from './tags.js'
|
||||
import { planTagOperations, executeTagOperation } from './tags.js'
|
||||
|
||||
/**
|
||||
* The main function for the action.
|
||||
@@ -10,50 +10,33 @@ import { resolveDesiredTags, processTag } from './tags.js'
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
let inputs
|
||||
try {
|
||||
inputs = getInputs()
|
||||
} catch (error) {
|
||||
// For parsing/validation errors, pass message directly.
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
core.setFailed(message)
|
||||
return
|
||||
}
|
||||
|
||||
// Create GitHub API client
|
||||
const inputs = getInputs()
|
||||
const octokit = github.getOctokit(inputs.token)
|
||||
|
||||
let tags
|
||||
try {
|
||||
tags = await resolveDesiredTags(inputs, octokit)
|
||||
} catch (error) {
|
||||
// For tag resolution errors (ref resolution, tag existence checks), pass
|
||||
// message directly.
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
core.setFailed(message)
|
||||
return
|
||||
}
|
||||
const operations = await planTagOperations(inputs, octokit)
|
||||
|
||||
const created: string[] = []
|
||||
const updated: string[] = []
|
||||
const skipped: string[] = []
|
||||
|
||||
// Create or update all tags.
|
||||
for (const tag of tags) {
|
||||
const result = await processTag(tag, octokit)
|
||||
// Execute all planned operations.
|
||||
for (const operation of operations) {
|
||||
await executeTagOperation(operation, octokit)
|
||||
|
||||
if (result === 'failed') {
|
||||
return
|
||||
} else if (result === 'created') {
|
||||
created.push(tag.name)
|
||||
} else if (result === 'updated') {
|
||||
updated.push(tag.name)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
core.setOutput('created', created)
|
||||
core.setOutput('updated', updated)
|
||||
core.setOutput('skipped', skipped)
|
||||
core.setOutput('tags', created.concat(updated))
|
||||
} catch (error) {
|
||||
core.setFailed(`Action failed with error: ${String(error)}`)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
core.setFailed(message)
|
||||
}
|
||||
}
|
||||
|
||||
451
src/tags.ts
451
src/tags.ts
@@ -1,6 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import type { Inputs, WhenExistsMode } from './inputs.js'
|
||||
import type { Inputs } from './inputs.js'
|
||||
|
||||
export interface ExistingTagInfo {
|
||||
commitSHA: string
|
||||
@@ -8,84 +8,56 @@ export interface ExistingTagInfo {
|
||||
annotation?: string
|
||||
}
|
||||
|
||||
export interface DesiredTag {
|
||||
interface BaseOperation {
|
||||
name: string
|
||||
ref: string
|
||||
sha: string
|
||||
whenExists: WhenExistsMode
|
||||
annotation: string
|
||||
owner: string
|
||||
repo: string
|
||||
existing?: ExistingTagInfo
|
||||
}
|
||||
|
||||
export type TagResult = 'created' | 'updated' | 'skipped' | 'failed'
|
||||
export interface CreateOperation extends BaseOperation {
|
||||
operation: 'create'
|
||||
annotation: string
|
||||
}
|
||||
|
||||
interface TagOperationContext {
|
||||
export interface UpdateOperation extends BaseOperation {
|
||||
operation: 'update'
|
||||
annotation: string
|
||||
existingSHA: string
|
||||
existingIsAnnotated: boolean
|
||||
reasons: string[]
|
||||
}
|
||||
|
||||
export interface SkipOperation extends BaseOperation {
|
||||
operation: 'skip'
|
||||
existingIsAnnotated: boolean
|
||||
reason: 'when_exists_skip' | 'already_matches'
|
||||
}
|
||||
|
||||
export type TagOperation = CreateOperation | UpdateOperation | SkipOperation
|
||||
|
||||
interface Context {
|
||||
owner: string
|
||||
repo: string
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch information about an existing tag, dereferencing if annotated.
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param existing - The existing tag reference data
|
||||
* @returns Information about the existing tag
|
||||
*/
|
||||
async function fetchExistingTagInfo(
|
||||
ctx: TagOperationContext,
|
||||
existing: { data: { object: { sha: string; type: string } } }
|
||||
): Promise<ExistingTagInfo> {
|
||||
const existingObject = existing.data.object
|
||||
const isAnnotated = existingObject.type === 'tag'
|
||||
|
||||
if (!isAnnotated) {
|
||||
return {
|
||||
commitSHA: existingObject.sha,
|
||||
isAnnotated: false
|
||||
}
|
||||
}
|
||||
|
||||
// Dereference annotated tag to get underlying commit
|
||||
const tagObject = await ctx.octokit.rest.git.getTag({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
tag_sha: existingObject.sha
|
||||
})
|
||||
|
||||
return {
|
||||
commitSHA: tagObject.data.object.sha,
|
||||
isAnnotated: true,
|
||||
annotation: tagObject.data.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve desired tag objects from inputs.
|
||||
* Plan tag operations based on inputs.
|
||||
*
|
||||
* @param inputs - The validated inputs containing tags, refs, and configuration
|
||||
* @param octokit - The GitHub API client
|
||||
* @returns Array of desired tags with resolved SHAs and configuration
|
||||
* @returns Array of planned tag operations (create, update, or skip)
|
||||
*/
|
||||
export async function resolveDesiredTags(
|
||||
export async function planTagOperations(
|
||||
inputs: Inputs,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
): Promise<DesiredTag[]> {
|
||||
const {
|
||||
tags: parsedTags,
|
||||
defaultRef,
|
||||
whenExists,
|
||||
annotation,
|
||||
owner,
|
||||
repo
|
||||
} = inputs
|
||||
|
||||
): Promise<TagOperation[]> {
|
||||
const uniqueRefs = new Set<string>()
|
||||
const tags: Record<string, string> = {}
|
||||
const tagRefs: Record<string, string> = {}
|
||||
|
||||
for (const tag of parsedTags) {
|
||||
for (const tag of inputs.tags) {
|
||||
const parts = tag.split(':').map((s) => s.trim())
|
||||
if (parts.length > 2) {
|
||||
throw new Error(
|
||||
@@ -102,51 +74,46 @@ export async function resolveDesiredTags(
|
||||
continue
|
||||
}
|
||||
|
||||
const ref = tagRef || defaultRef
|
||||
const ref = tagRef || inputs.defaultRef
|
||||
if (!ref) {
|
||||
throw new Error("Missing ref: provide 'ref' input or specify per-tag ref")
|
||||
}
|
||||
|
||||
// Check for duplicate tag with different ref
|
||||
if (tags[tagName] && tags[tagName] !== ref) {
|
||||
if (tagRefs[tagName] && tagRefs[tagName] !== ref) {
|
||||
throw new Error(
|
||||
`Duplicate tag '${tagName}' with different refs: ` +
|
||||
`'${tags[tagName]}' and '${ref}'`
|
||||
`'${tagRefs[tagName]}' and '${ref}'`
|
||||
)
|
||||
}
|
||||
|
||||
tags[tagName] = ref
|
||||
tagRefs[tagName] = ref
|
||||
uniqueRefs.add(ref)
|
||||
}
|
||||
|
||||
// Pre-resolve all unique refs in parallel.
|
||||
const ctx: TagOperationContext = { owner, repo, octokit }
|
||||
const refToSha: Record<string, string> = {}
|
||||
const ctx: Context = { owner: inputs.owner, repo: inputs.repo, octokit }
|
||||
const refSHAs: Record<string, string> = {}
|
||||
await Promise.all(
|
||||
Array.from(uniqueRefs).map(async (ref) => {
|
||||
refToSha[ref] = await resolveRefToSha(ctx, ref)
|
||||
refSHAs[ref] = await resolveRefToSha(ctx, ref)
|
||||
})
|
||||
)
|
||||
|
||||
// Build result array with resolved SHAs and check for existing tags.
|
||||
const tagNames = Object.keys(tags)
|
||||
const result: DesiredTag[] = await Promise.all(
|
||||
// Build result array with planned operations
|
||||
const tagNames = Object.keys(tagRefs)
|
||||
const result: TagOperation[] = await Promise.all(
|
||||
tagNames.map(async (tagName) => {
|
||||
const tagRef = tags[tagName]
|
||||
const sha = refToSha[tagRef]
|
||||
const tagRef = tagRefs[tagName]
|
||||
const sha = refSHAs[tagRef]
|
||||
|
||||
// Check if tag already exists
|
||||
let existing: ExistingTagInfo | undefined
|
||||
try {
|
||||
const existingRef = await ctx.octokit.rest.git.getRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `tags/${tagName}`
|
||||
})
|
||||
existing = await fetchExistingTagInfo(ctx, existingRef)
|
||||
existing = await fetchTagInfo(ctx, tagName)
|
||||
|
||||
// Fail early if when_exists is 'fail'
|
||||
if (whenExists === 'fail') {
|
||||
if (inputs.whenExists === 'fail') {
|
||||
throw new Error(`Tag '${tagName}' already exists.`)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -161,37 +128,161 @@ export async function resolveDesiredTags(
|
||||
`Failed to check if tag '${tagName}' exists: ${apiError.message || String(error)}`
|
||||
)
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
// Already an Error (e.g., from when_exists === 'fail')
|
||||
throw error
|
||||
} else {
|
||||
// Unknown error type
|
||||
throw new Error(
|
||||
`Failed to check if tag '${tagName}' exists: ${String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const baseOp = {
|
||||
name: tagName,
|
||||
ref: tagRef,
|
||||
sha,
|
||||
whenExists,
|
||||
annotation,
|
||||
owner,
|
||||
repo,
|
||||
existing
|
||||
owner: inputs.owner,
|
||||
repo: inputs.repo
|
||||
}
|
||||
|
||||
// Tag doesn't exist - plan creation
|
||||
if (!existing) {
|
||||
return {
|
||||
...baseOp,
|
||||
operation: 'create',
|
||||
annotation: inputs.annotation
|
||||
} as CreateOperation
|
||||
}
|
||||
|
||||
// Tag exists - determine operation based on mode and state
|
||||
if (inputs.whenExists === 'skip') {
|
||||
return {
|
||||
...baseOp,
|
||||
operation: 'skip',
|
||||
existingIsAnnotated: existing.isAnnotated,
|
||||
reason: 'when_exists_skip'
|
||||
} as SkipOperation
|
||||
}
|
||||
|
||||
// whenExists === 'update' - check if update is needed
|
||||
const { commitMatches, annotationMatches } = compareTagState(
|
||||
sha,
|
||||
inputs.annotation,
|
||||
existing
|
||||
)
|
||||
|
||||
if (commitMatches && annotationMatches) {
|
||||
return {
|
||||
...baseOp,
|
||||
operation: 'skip',
|
||||
existingIsAnnotated: existing.isAnnotated,
|
||||
reason: 'already_matches'
|
||||
} as SkipOperation
|
||||
}
|
||||
|
||||
// Plan update with reasons
|
||||
const reasons = getUpdateReasons(sha, inputs.annotation, existing)
|
||||
return {
|
||||
...baseOp,
|
||||
operation: 'update',
|
||||
annotation: inputs.annotation,
|
||||
existingSHA: existing.commitSHA,
|
||||
existingIsAnnotated: existing.isAnnotated,
|
||||
reasons
|
||||
} as UpdateOperation
|
||||
})
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function resolveRefToSha(
|
||||
ctx: TagOperationContext,
|
||||
ref: string
|
||||
): Promise<string> {
|
||||
/**
|
||||
* Execute a planned tag operation.
|
||||
*
|
||||
* @param operation - The planned tag operation to execute
|
||||
* @param octokit - GitHub API client
|
||||
*/
|
||||
export async function executeTagOperation(
|
||||
operation: TagOperation,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
): Promise<void> {
|
||||
const ctx: Context = {
|
||||
owner: operation.owner,
|
||||
repo: operation.repo,
|
||||
octokit
|
||||
}
|
||||
|
||||
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).' : '.')
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (operation.operation === 'create') {
|
||||
await createTag(ctx, operation)
|
||||
return
|
||||
}
|
||||
|
||||
if (operation.operation === 'update') {
|
||||
await updateExistingTag(ctx, operation)
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unknown operation type: ${(operation as TagOperation).operation}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch information about an existing tag, dereferencing if annotated.
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param tagName - The name of the tag to fetch
|
||||
* @returns Information about the existing tag
|
||||
*/
|
||||
async function fetchTagInfo(
|
||||
ctx: Context,
|
||||
tagName: string
|
||||
): Promise<ExistingTagInfo> {
|
||||
const ref = await ctx.octokit.rest.git.getRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `tags/${tagName}`
|
||||
})
|
||||
const object = ref.data.object
|
||||
const isAnnotated = object.type === 'tag'
|
||||
|
||||
if (!isAnnotated) {
|
||||
return {
|
||||
commitSHA: object.sha,
|
||||
isAnnotated: false
|
||||
}
|
||||
}
|
||||
|
||||
// Dereference annotated tag to get underlying commit
|
||||
const tagRef = await ctx.octokit.rest.git.getTag({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
tag_sha: object.sha
|
||||
})
|
||||
|
||||
return {
|
||||
commitSHA: tagRef.data.object.sha,
|
||||
isAnnotated: true,
|
||||
annotation: tagRef.data.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a ref to a SHA.
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param ref - The ref to resolve
|
||||
* @returns The SHA
|
||||
*/
|
||||
async function resolveRefToSha(ctx: Context, ref: string): Promise<string> {
|
||||
try {
|
||||
const {
|
||||
data: { sha }
|
||||
@@ -207,131 +298,94 @@ async function resolveRefToSha(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single desired tag: create or update it based on configuration.
|
||||
*
|
||||
* @param tag - The desired tag to process (with existing info if applicable)
|
||||
* @param octokit - GitHub API client
|
||||
* @returns The result of the tag operation
|
||||
*/
|
||||
export async function processTag(
|
||||
tag: DesiredTag,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
): Promise<TagResult> {
|
||||
const ctx: TagOperationContext = { owner: tag.owner, repo: tag.repo, octokit }
|
||||
|
||||
// Tag doesn't exist, create it
|
||||
if (!tag.existing) {
|
||||
return await createTag(ctx, tag)
|
||||
}
|
||||
|
||||
// Tag exists - handle based on when_exists strategy
|
||||
if (tag.whenExists === 'skip') {
|
||||
core.info(`Tag '${tag.name}' exists, skipping.`)
|
||||
return 'skipped'
|
||||
}
|
||||
|
||||
if (tag.whenExists === 'fail') {
|
||||
// This should not happen as we fail early in resolveDesiredTags
|
||||
core.setFailed(`Tag '${tag.name}' already exists.`)
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
// whenExists === 'update' - check if update is needed
|
||||
if (tagMatchesTarget(tag)) {
|
||||
core.info(
|
||||
`Tag '${tag.name}' already exists with desired commit SHA ${tag.sha}` +
|
||||
(tag.existing.isAnnotated ? ' (annotated).' : '.')
|
||||
)
|
||||
return 'skipped'
|
||||
}
|
||||
|
||||
return await updateExistingTag(ctx, tag)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing tag to point to a new commit and/or annotation.
|
||||
*/
|
||||
async function updateExistingTag(
|
||||
ctx: TagOperationContext,
|
||||
tag: DesiredTag
|
||||
): Promise<TagResult> {
|
||||
if (!tag.existing) {
|
||||
throw new Error(`Cannot update non-existent tag '${tag.name}'`)
|
||||
}
|
||||
|
||||
const reasons = getUpdateReasons(tag)
|
||||
const commitMatches = tag.existing.commitSHA === tag.sha
|
||||
ctx: Context,
|
||||
operation: UpdateOperation
|
||||
): Promise<void> {
|
||||
const commitMatches = operation.existingSHA === operation.sha
|
||||
|
||||
if (commitMatches) {
|
||||
core.info(
|
||||
`Tag '${tag.name}' exists with same commit but ${reasons.join(', ')}.`
|
||||
`Tag '${operation.name}' exists with same commit but ${operation.reasons.join(', ')}.`
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
`Tag '${tag.name}' exists` +
|
||||
`${tag.existing.isAnnotated ? ' (annotated)' : ''}` +
|
||||
`, updating to ${reasons.join(', ')}.`
|
||||
`Tag '${operation.name}' exists` +
|
||||
`${operation.existingIsAnnotated ? ' (annotated)' : ''}` +
|
||||
`, updating to ${operation.reasons.join(', ')}.`
|
||||
)
|
||||
}
|
||||
|
||||
const targetSha = await resolveTargetSHA(ctx, tag)
|
||||
const targetSha = await resolveTargetSHA(
|
||||
ctx,
|
||||
operation.name,
|
||||
operation.sha,
|
||||
operation.annotation
|
||||
)
|
||||
|
||||
await ctx.octokit.rest.git.updateRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `tags/${tag.name}`,
|
||||
ref: `tags/${operation.name}`,
|
||||
sha: targetSha,
|
||||
force: true
|
||||
})
|
||||
|
||||
return 'updated'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag (doesn't exist yet).
|
||||
*/
|
||||
async function createTag(
|
||||
ctx: TagOperationContext,
|
||||
tag: DesiredTag
|
||||
): Promise<TagResult> {
|
||||
ctx: Context,
|
||||
operation: CreateOperation
|
||||
): Promise<void> {
|
||||
core.info(
|
||||
`Tag '${tag.name}' does not exist, creating with commit SHA ${tag.sha}.`
|
||||
`Tag '${operation.name}' does not exist, creating with commit SHA ${operation.sha}.`
|
||||
)
|
||||
|
||||
const targetSha = await resolveTargetSHA(ctx, tag)
|
||||
const targetSha = await resolveTargetSHA(
|
||||
ctx,
|
||||
operation.name,
|
||||
operation.sha,
|
||||
operation.annotation
|
||||
)
|
||||
|
||||
await ctx.octokit.rest.git.createRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `refs/tags/${tag.name}`,
|
||||
ref: `refs/tags/${operation.name}`,
|
||||
sha: targetSha
|
||||
})
|
||||
|
||||
return 'created'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the target SHA for a tag (creates annotated tag object if needed).
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param tag - The tag to create
|
||||
* @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: TagOperationContext,
|
||||
tag: DesiredTag
|
||||
ctx: Context,
|
||||
tagName: string,
|
||||
commitSha: string,
|
||||
annotation: string
|
||||
): Promise<string> {
|
||||
if (!tag.annotation) {
|
||||
return tag.sha
|
||||
if (!annotation) {
|
||||
return commitSha
|
||||
}
|
||||
|
||||
const tagObject = await ctx.octokit.rest.git.createTag({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
tag: tag.name,
|
||||
message: tag.annotation,
|
||||
object: tag.sha,
|
||||
tag: tagName,
|
||||
message: annotation,
|
||||
object: commitSha,
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
@@ -341,63 +395,64 @@ async function resolveTargetSHA(
|
||||
/**
|
||||
* Compare existing tag state with desired target state.
|
||||
*
|
||||
* @param tag - The desired tag with existing info
|
||||
* @param sha - The desired commit SHA
|
||||
* @param annotation - The desired annotation
|
||||
* @param existing - Information about the existing tag
|
||||
* @returns Object indicating whether commit and annotation match
|
||||
*/
|
||||
function compareTagState(tag: DesiredTag): {
|
||||
function compareTagState(
|
||||
sha: string,
|
||||
annotation: string,
|
||||
existing: ExistingTagInfo
|
||||
): {
|
||||
commitMatches: boolean
|
||||
annotationMatches: boolean
|
||||
} {
|
||||
if (!tag.existing) {
|
||||
return { commitMatches: false, annotationMatches: false }
|
||||
}
|
||||
const isAnnotated = existing.isAnnotated === true
|
||||
|
||||
const commitMatches = tag.existing.commitSHA === tag.sha
|
||||
const commitMatches = existing.commitSHA === sha
|
||||
const annotationMatches =
|
||||
tag.existing.isAnnotated && tag.annotation
|
||||
? tag.existing.annotation === tag.annotation
|
||||
: !tag.existing.isAnnotated && !tag.annotation
|
||||
(isAnnotated && !!annotation && existing.annotation === annotation) ||
|
||||
(!isAnnotated && !annotation) ||
|
||||
false
|
||||
|
||||
return { commitMatches, annotationMatches }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag needs to be updated based on commit and annotation.
|
||||
*
|
||||
* @param tag - The desired tag with existing info
|
||||
* @returns True if the tag matches the target state
|
||||
*/
|
||||
function tagMatchesTarget(tag: DesiredTag): boolean {
|
||||
const { commitMatches, annotationMatches } = compareTagState(tag)
|
||||
return commitMatches && annotationMatches
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update reason messages based on what changed.
|
||||
*
|
||||
* @param tag - The desired tag with existing info
|
||||
* @param sha - The desired commit SHA
|
||||
* @param annotation - The desired annotation
|
||||
* @param existing - Information about the existing tag
|
||||
* @returns Array of reason strings
|
||||
*/
|
||||
function getUpdateReasons(tag: DesiredTag): string[] {
|
||||
if (!tag.existing) return []
|
||||
|
||||
const { commitMatches, annotationMatches } = compareTagState(tag)
|
||||
function getUpdateReasons(
|
||||
sha: string,
|
||||
annotation: string,
|
||||
existing: ExistingTagInfo
|
||||
): string[] {
|
||||
const { commitMatches, annotationMatches } = compareTagState(
|
||||
sha,
|
||||
annotation,
|
||||
existing
|
||||
)
|
||||
const reasons: string[] = []
|
||||
|
||||
if (!commitMatches) {
|
||||
reasons.push(`commit SHA ${tag.sha} (was ${tag.existing.commitSHA})`)
|
||||
reasons.push(`commit SHA ${sha} (was ${existing.commitSHA})`)
|
||||
}
|
||||
|
||||
if (!annotationMatches && tag.annotation) {
|
||||
if (tag.existing.isAnnotated) {
|
||||
if (!annotationMatches && annotation) {
|
||||
if (existing.isAnnotated === true) {
|
||||
reasons.push('annotation message changed')
|
||||
} else {
|
||||
reasons.push('adding annotation')
|
||||
}
|
||||
} else if (
|
||||
!annotationMatches &&
|
||||
!tag.annotation &&
|
||||
tag.existing.isAnnotated
|
||||
!annotation &&
|
||||
existing.isAnnotated === true
|
||||
) {
|
||||
reasons.push('removing annotation')
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* Unit tests for the action's main functionality, src/main.ts
|
||||
*/
|
||||
import { jest } from '@jest/globals'
|
||||
import * as core from '../__fixtures__/core.js'
|
||||
import * as github from '../__fixtures__/github.js'
|
||||
import * as csvParse from '../__fixtures__/csv-parse.js'
|
||||
import * as core from './fixtures/core.js'
|
||||
import * as github from './fixtures/github.js'
|
||||
import * as csvParse from './fixtures/csv-parse.js'
|
||||
|
||||
// Mocks should be declared before the module being tested is imported.
|
||||
jest.unstable_mockModule('@actions/core', () => core)
|
||||
@@ -22,6 +22,19 @@ const setupInputs = (inputs: Record<string, string>): void => {
|
||||
})
|
||||
}
|
||||
|
||||
let outputs: Record<string, unknown> = {}
|
||||
|
||||
const setupOutputCapture = (): void => {
|
||||
outputs = {}
|
||||
core.setOutput.mockImplementation((name: string, value: unknown) => {
|
||||
outputs[name] = value
|
||||
})
|
||||
}
|
||||
|
||||
const getOutputs = (): Record<string, unknown> => {
|
||||
return { ...outputs }
|
||||
}
|
||||
|
||||
const setupCommitResolver = (
|
||||
refToSha: Record<string, string> | string
|
||||
): void => {
|
||||
@@ -80,6 +93,7 @@ describe('run', () => {
|
||||
// Re-setup mocks after reset
|
||||
github.getOctokit.mockReturnValue(github.mockOctokit)
|
||||
csvParse.resetToRealImplementation()
|
||||
setupOutputCapture()
|
||||
})
|
||||
|
||||
it('creates new tags when they do not exist', async () => {
|
||||
@@ -111,9 +125,12 @@ describe('run', () => {
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Tag 'v1' does not exist, creating with commit SHA sha-abc123."
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v1.0'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', ['v1', 'v1.0'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v1.0'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v1.0']
|
||||
})
|
||||
})
|
||||
|
||||
it('updates existing tags when commit SHA differs', async () => {
|
||||
@@ -140,9 +157,12 @@ describe('run', () => {
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Tag 'v1' exists, updating to commit SHA sha-def456 (was sha-old123)."
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('skips updating when tag exists with same commit SHA', async () => {
|
||||
@@ -162,9 +182,12 @@ describe('run', () => {
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Tag 'v1' already exists with desired commit SHA sha-abc123."
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', [])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: [],
|
||||
skipped: ['v1'],
|
||||
tags: []
|
||||
})
|
||||
})
|
||||
|
||||
it('skips tags when when_exists is skip', async () => {
|
||||
@@ -182,9 +205,12 @@ describe('run', () => {
|
||||
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
|
||||
expect(core.info).toHaveBeenCalledWith("Tag 'v1' exists, skipping.")
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', [])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: [],
|
||||
skipped: ['v1'],
|
||||
tags: []
|
||||
})
|
||||
})
|
||||
|
||||
it('handles per-tag ref overrides', async () => {
|
||||
@@ -226,7 +252,12 @@ describe('run', () => {
|
||||
sha: 'sha-develop'
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v2'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2']
|
||||
})
|
||||
})
|
||||
|
||||
it('handles various input formats (newlines and whitespace)', async () => {
|
||||
@@ -242,11 +273,12 @@ describe('run', () => {
|
||||
await run()
|
||||
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(3)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [
|
||||
'v1',
|
||||
'v1.0',
|
||||
'v1.0.1'
|
||||
])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v1.0', 'v1.0.1'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v1.0', 'v1.0.1']
|
||||
})
|
||||
})
|
||||
|
||||
it('creates and updates tags in single run', async () => {
|
||||
@@ -263,9 +295,12 @@ describe('run', () => {
|
||||
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(1)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v2'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', ['v2', 'v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v2'],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v2', 'v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('fails when ref is missing', async () => {
|
||||
@@ -420,7 +455,12 @@ describe('run', () => {
|
||||
await run()
|
||||
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('handles duplicate tags by using last occurrence', async () => {
|
||||
@@ -437,7 +477,12 @@ describe('run', () => {
|
||||
|
||||
// Should only create 2 tags (v1 and v2), not 3
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v2'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2']
|
||||
})
|
||||
})
|
||||
|
||||
it('optimizes by resolving unique refs only once', async () => {
|
||||
@@ -458,6 +503,12 @@ describe('run', () => {
|
||||
// Should only call getCommit 2 times (main and develop), not 3
|
||||
expect(github.mockOctokit.rest.repos.getCommit).toHaveBeenCalledTimes(2)
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(3)
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2', 'v3'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2', 'v3']
|
||||
})
|
||||
})
|
||||
|
||||
it('handles tag with colon but empty ref part', async () => {
|
||||
@@ -474,7 +525,12 @@ describe('run', () => {
|
||||
|
||||
// Both should use default ref
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v2'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2']
|
||||
})
|
||||
})
|
||||
|
||||
it('fails when tag specification has multiple colons', async () => {
|
||||
@@ -522,8 +578,12 @@ describe('run', () => {
|
||||
// Should skip v1, create v2 and v3
|
||||
expect(core.info).toHaveBeenCalledWith("Tag 'v1' exists, skipping.")
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v2', 'v3'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', [])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v2', 'v3'],
|
||||
updated: [],
|
||||
skipped: ['v1'],
|
||||
tags: ['v2', 'v3']
|
||||
})
|
||||
})
|
||||
|
||||
it('fails when tag name is empty (e.g., ":main")', async () => {
|
||||
@@ -618,7 +678,12 @@ describe('run', () => {
|
||||
sha: 'sha-abc123'
|
||||
})
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v2'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2']
|
||||
})
|
||||
})
|
||||
|
||||
it('skips empty lines in multi-line input (e.g., "v1\\n\\nv2")', async () => {
|
||||
@@ -647,7 +712,12 @@ describe('run', () => {
|
||||
sha: 'sha-abc123'
|
||||
})
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v2'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2']
|
||||
})
|
||||
})
|
||||
|
||||
it('skips empty tags from mix of empty CSV fields and empty lines', async () => {
|
||||
@@ -688,12 +758,12 @@ describe('run', () => {
|
||||
sha: 'sha-abc123'
|
||||
})
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(4)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [
|
||||
'v1',
|
||||
'v2',
|
||||
'v3',
|
||||
'v4'
|
||||
])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v2', 'v3', 'v4'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v2', 'v3', 'v4']
|
||||
})
|
||||
})
|
||||
|
||||
it('creates annotated tags when annotation is provided', async () => {
|
||||
@@ -748,7 +818,12 @@ describe('run', () => {
|
||||
sha: 'sha-tag-object-v1.0'
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v1.0'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1', 'v1.0'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1', 'v1.0']
|
||||
})
|
||||
})
|
||||
|
||||
it('updates existing tags with annotation', async () => {
|
||||
@@ -787,7 +862,12 @@ describe('run', () => {
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('creates lightweight tags when annotation is empty', async () => {
|
||||
@@ -812,7 +892,12 @@ describe('run', () => {
|
||||
sha: 'sha-abc123'
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: ['v1'],
|
||||
updated: [],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('updates lightweight tags when annotation is empty', async () => {
|
||||
@@ -838,7 +923,12 @@ describe('run', () => {
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('detects and dereferences existing annotated tags', async () => {
|
||||
@@ -872,7 +962,12 @@ describe('run', () => {
|
||||
|
||||
// Should update because commit SHAs differ
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('updates existing annotated tags with new annotated tags', async () => {
|
||||
@@ -931,7 +1026,12 @@ describe('run', () => {
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('updates annotated tag to lightweight when annotation removed', async () => {
|
||||
@@ -970,7 +1070,12 @@ describe('run', () => {
|
||||
sha: 'sha-abc123',
|
||||
force: true
|
||||
})
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('skips when annotated tag has same commit and same annotation', async () => {
|
||||
@@ -1000,7 +1105,12 @@ describe('run', () => {
|
||||
// Should NOT update because both commit and annotation match
|
||||
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', [])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: [],
|
||||
skipped: ['v1'],
|
||||
tags: []
|
||||
})
|
||||
})
|
||||
|
||||
it('updates when annotated tag has same commit but different annotation', async () => {
|
||||
@@ -1052,7 +1162,12 @@ describe('run', () => {
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
|
||||
it('updates lightweight tag to annotated when annotation added', async () => {
|
||||
@@ -1097,6 +1212,11 @@ describe('run', () => {
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(getOutputs()).toEqual({
|
||||
created: [],
|
||||
updated: ['v1'],
|
||||
skipped: [],
|
||||
tags: ['v1']
|
||||
})
|
||||
})
|
||||
})
|
||||
37
tests/tags.test.ts
Normal file
37
tests/tags.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Unit tests for tag operation execution, src/tags.ts
|
||||
*/
|
||||
import { jest } from '@jest/globals'
|
||||
import * as core from './fixtures/core.js'
|
||||
import * as github from './fixtures/github.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'
|
||||
|
||||
describe('executeTagOperation', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
github.getOctokit.mockReturnValue(github.mockOctokit)
|
||||
})
|
||||
|
||||
it('throws error for unknown operation type', async () => {
|
||||
const invalidOperation = {
|
||||
operation: 'invalid',
|
||||
name: 'v1',
|
||||
ref: 'main',
|
||||
sha: 'abc123',
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo'
|
||||
} as unknown as TagOperation
|
||||
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
executeTagOperation(invalidOperation, github.mockOctokit as any)
|
||||
).rejects.toThrow('Unknown operation type: invalid')
|
||||
})
|
||||
})
|
||||
@@ -7,8 +7,7 @@
|
||||
},
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"include": [
|
||||
"__fixtures__",
|
||||
"__tests__",
|
||||
"tests",
|
||||
"src",
|
||||
"eslint.config.mjs",
|
||||
"jest.config.js",
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["__fixtures__", "__tests__", "coverage", "dist", "node_modules"],
|
||||
"exclude": ["tests", "coverage", "dist", "node_modules"],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user