From 2790ab3ae51ef127a43b5b80121e3a003aa1d9af Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:35:10 +0200 Subject: [PATCH 01/24] chore: adr code release --- adr/2026-06-02-release-automation.md | 59 ++++++++++++++++++++++ adr/2026-06-02-release-branch-per-cycle.md | 49 ++++++++++++++++++ adr/index.md | 2 + 3 files changed, 110 insertions(+) create mode 100644 adr/2026-06-02-release-automation.md create mode 100644 adr/2026-06-02-release-branch-per-cycle.md diff --git a/adr/2026-06-02-release-automation.md b/adr/2026-06-02-release-automation.md new file mode 100644 index 0000000000..799cfeaf97 --- /dev/null +++ b/adr/2026-06-02-release-automation.md @@ -0,0 +1,59 @@ +# Release automation: explicit versioning, one branch per release cycle + +- Status: accepted + +## Context and Problem Statement + +The release process was split across three workflows (`create_rc.yml`, `patch_rc.yml`, `Releases.yml`) using two competing tools (`standard-version` and `semantic-release`). Version progression was implicit — `standard-version` inferred the next semver bump from conventional commit prefixes. Branch naming (`rc/v`) was inconsistent with the repo's existing `releases/` prefix convention. How should a replacement automation be structured? + +## Decision Drivers + +- Version drift risk: two tools writing to `package.json` and git tags in different workflows with no shared guard against duplicates +- Operational clarity: release managers need to know exactly what version will be tagged before triggering a workflow +- Release cycle continuity: RC patches, stable cuts, and post-release patch RCs must all live on one branch without manual branch juggling + +## Considered Options + +- Keep `standard-version` with implicit version detection from conventional commits +- Replace with explicit version inputs and GitHub's native release notes API +- Replace with `semantic-release` (already present in `Releases.yml`) as the single tool + +## Decision Outcome + +Chosen option: "explicit version inputs + GitHub native release notes API", because it makes the release manager's intent unambiguous at trigger time, eliminates the dual-tool conflict, and removes the dependency on conventional commit discipline as a hard requirement for correct version bumps. + +### Positive Consequences + +- A single `workflow_dispatch` input (`version`) is the source of truth — no inference, no surprise bumps +- GitHub Releases become the authoritative changelog; `CHANGELOG.md` and its maintenance scripts are removed +- Stable release notes correctly skip RC tags by passing the previous stable tag as `previous_tag_name` +- "Cut Release Branch" opens a second PR bumping `base_branch` to the next minor RC (e.g., `1.5.0-rc.0`), keeping `main` from stalling at the released version; the PR makes the bump reviewable and safe against protected branches + +### Negative Consequences + +- Release managers must supply the correct version string manually; a typo creates a wrong tag (mitigated by the format validation regex and the branch-name cross-check in "Release from Branch") +- `CHANGELOG.md` history is no longer in the repo; it must be consulted via the GitHub Releases page + +## Release Branch Change Workflow + +Fixes are cherry-picked onto `release/v` via PR, not direct push. Developers open a fix branch, cherry-pick the relevant commit(s), and target the PR at the Release Cycle branch. `release/v` branches are expected to be protected; the `BOT_TOKEN` PAT holds the branch protection bypass used exclusively for the version bump commits made by "Release from Branch". + +## Pros and Cons of the Options + +### Keep `standard-version` with implicit detection + +- Good, because version bump logic is automatic and tied to commit history +- Bad, because it conflicts with `semantic-release` already present in `Releases.yml` +- Bad, because a single non-conventional commit silently produces the wrong version bump + +### Explicit version inputs + GitHub native release notes API + +- Good, because intent is explicit and auditable in the workflow dispatch log +- Good, because no external changelog tool needed — GitHub generates diff-based notes natively +- Bad, because version must be typed correctly by a human + +### `semantic-release` as sole tool + +- Good, because it handles the full release lifecycle in one tool +- Bad, because it requires all commits to follow conventional format to produce correct output +- Bad, because it was already present and unused in `Releases.yml`, indicating it was never fully adopted diff --git a/adr/2026-06-02-release-branch-per-cycle.md b/adr/2026-06-02-release-branch-per-cycle.md new file mode 100644 index 0000000000..5e80f2c1d7 --- /dev/null +++ b/adr/2026-06-02-release-branch-per-cycle.md @@ -0,0 +1,49 @@ +# One release branch per major.minor cycle + +- Status: accepted + +## Context and Problem Statement + +When cutting a release, should each individual version (including each RC) get its own branch, or should a single branch cover the entire major.minor cycle (all RCs, the stable cut, and subsequent patches)? + +## Decision Drivers + +- Patch releases are common: every minor version in this repo has had multiple patches (`v4.13.0–rc.4`, `v4.15.0–rc.3`, `v5.0.0–rc.1`) +- Cherry-pick overhead: fixes targeting an RC must reach subsequent RCs and the stable cut +- Branch proliferation: one branch per version produces `release/v1.4.0-rc.1`, `release/v1.4.0-rc.2`, `release/v1.4.0`, `release/v1.4.1` as separate branches for a single minor cycle + +## Considered Options + +- One branch per version (e.g., `release/v1.4.0-rc.1`, `release/v1.4.0`, `release/v1.4.1`) +- One branch per major.minor cycle (e.g., `release/v1.4`) + +## Decision Outcome + +Chosen option: "one branch per major.minor cycle", because it eliminates cross-branch cherry-picks within a release cycle and matches the existing `rc/v` convention already used in this repo. + +The branch is created once by "Cut Release Branch". All subsequent RC tags, the stable cut, and patch RCs are produced by running "Release from Branch" against the same branch. The "Cut Release Branch" guard (branch must not already exist) correctly prevents accidental cycle restarts. + +### Positive Consequences + +- A fix committed to `release/v1.4` is automatically present in all subsequent tags from that branch — no cherry-picks needed +- Branch list stays proportional to minor releases, not to the number of RC iterations +- Patch releases (`v1.4.1`, `v1.4.2`) require no new branch; "Release from Branch" handles them via the auto-bump-to-next-patch-RC behaviour after each stable cut + +### Negative Consequences + +- The branch name (`release/v1.4`) does not encode the current patch level; the authoritative version is always `package.json`, not the branch name +- "Release from Branch" must validate that the major.minor in `package.json` matches the branch suffix, rather than doing an exact version match + +## Pros and Cons of the Options + +### One branch per version + +- Good, because the branch name encodes the exact version being released +- Bad, because a fix on `release/v1.4.0-rc.1` must be cherry-picked to `release/v1.4.0-rc.2`, then again to `release/v1.4.0`, then to `release/v1.4.1` — four manual operations for one fix +- Bad, because the "Cut Release Branch" workflow would need to run once per RC, creating a new branch each time + +### One branch per major.minor cycle + +- Good, because fixes flow forward automatically within the cycle +- Good, because "Cut Release Branch" runs once and "Release from Branch" handles all subsequent releases on that branch +- Bad, because the branch name alone does not tell you which patch is currently in flight diff --git a/adr/index.md b/adr/index.md index a56c70aaac..790fe0bc21 100644 --- a/adr/index.md +++ b/adr/index.md @@ -17,6 +17,8 @@ This log lists the architectural decisions for apl-core. - [ADR-2022-06-07](2022-06-07-ingress-classes.md) - Ingress classes - [ADR-2022-07-02](2022-07-02-node-affinity.md) - Node affinity - [ADR-2022-08-26](2022-08-26-other-dns-provider.md) - Other DNS provider +- [ADR-2026-06-02](2026-06-02-release-automation.md) - Release automation: explicit versioning, one branch per release cycle +- [ADR-2026-06-02](2026-06-02-release-branch-per-cycle.md) - One release branch per major.minor cycle From 75b9072db6fbe08435b96c67d238d76398db59fd Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:35:43 +0200 Subject: [PATCH 02/24] chore: context for decision architecture --- CONTEXT.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000..328d7ec227 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,21 @@ +# apl-core Domain Glossary + +## Release Cycle + +A `release/v` branch (e.g., `release/v1.4`) that covers all versions in a major.minor series: release candidates (`v1.4.0-rc.1`, `v1.4.0-rc.2`), the stable cut (`v1.4.0`), and any subsequent patches (`v1.4.1-rc.1`, `v1.4.1`). One branch is created per cycle; it is never recreated. + +## Cut Release Branch + +The `workflow_dispatch` GitHub Actions workflow that starts a Release Cycle. Accepts a `version` input (`1.4.0` or `1.4.0-rc.1`), runs tests via Docker build, bumps `package.json` to that version, creates `release/v`, and opens two PRs: one from `release/v` back to `base_branch` with the release checklist, and one bumping `base_branch` to the next minor development version (e.g., cutting `1.4.0-rc.1` also opens a PR bumping `main` to `1.5.0-rc.0`). + +## Release from Branch + +The `workflow_dispatch` GitHub Actions workflow that publishes a release from an existing Release Cycle branch. Reads the current version from `package.json`, tags and releases it, then auto-bumps `package.json` to the next iteration. Has a `promote_to_stable` input that strips the RC prerelease suffix and advances to the next patch RC after tagging. Also publishes the Docker image and Helm chart. + +## Release Candidate (RC) + +A pre-release version in `..-rc.` format (e.g., `1.4.0-rc.1`). Produced by "Release from Branch" without `promote_to_stable`. Each RC run auto-increments `n` and commits the next RC version to the Release Cycle branch. + +## Stable Release + +A version without a prerelease suffix (e.g., `1.4.0`, `1.4.1`). Produced by "Release from Branch" with `promote_to_stable=true`. After tagging stable, the branch is auto-bumped to the next patch RC (e.g., `1.4.1-rc.1`). From 5fa7943fb2b77833ddb49276a01c6dd62c35f78d Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:36:30 +0200 Subject: [PATCH 03/24] ci: implement base functions --- ci/jest.config.ts | 11 +++ ci/src/release/version.test.ts | 129 +++++++++++++++++++++++++++++++++ ci/src/release/version.ts | 48 ++++++++++++ ci/tsconfig.json | 16 ++++ 4 files changed, 204 insertions(+) create mode 100644 ci/jest.config.ts create mode 100644 ci/src/release/version.test.ts create mode 100644 ci/src/release/version.ts create mode 100644 ci/tsconfig.json diff --git a/ci/jest.config.ts b/ci/jest.config.ts new file mode 100644 index 0000000000..ee64815019 --- /dev/null +++ b/ci/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from '@jest/types' + +const config: Config.InitialOptions = { + preset: 'ts-jest', + roots: ['/src'], + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], + verbose: true, +} + +export default config diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts new file mode 100644 index 0000000000..634c9ea022 --- /dev/null +++ b/ci/src/release/version.test.ts @@ -0,0 +1,129 @@ +import { + validateVersion, + releaseBranchName, + incrementRc, + promoteToStable, + nextPatchRc, + nextMainVersion, + versionMatchesBranch, + previousStableTag, + isHighestStableTag, +} from './version' + +describe('validateVersion', () => { + it.each([ + ['1.4.0', true], + ['1.4.0-rc.1', true], + ['1.4.0-rc.10', true], + ['6.0.0-rc.0', true], + ['v1.4.0', false], + ['v1.4.0-rc.1', false], + ['1.4', false], + ['1.4.0-beta.1', false], + ['1.4.0-rc', false], + ['latest', false], + ['', false], + ['1.4.0-rc.1.2', false], + ])('validateVersion(%s) → %s', (version, expected) => { + expect(validateVersion(version)).toBe(expected) + }) +}) + +describe('releaseBranchName', () => { + it.each([ + ['1.4.0-rc.1', 'release/v1.4'], + ['1.4.0', 'release/v1.4'], + ['6.0.0-rc.0', 'release/v6.0'], + ['1.4.1', 'release/v1.4'], + ])('releaseBranchName(%s) → %s', (version, expected) => { + expect(releaseBranchName(version)).toBe(expected) + }) +}) + +describe('incrementRc', () => { + it.each([ + ['1.4.0-rc.1', '1.4.0-rc.2'], + ['1.4.0-rc.9', '1.4.0-rc.10'], + ['6.0.0-rc.0', '6.0.0-rc.1'], + ])('incrementRc(%s) → %s', (version, expected) => { + expect(incrementRc(version)).toBe(expected) + }) +}) + +describe('promoteToStable', () => { + it.each([ + ['1.4.0-rc.1', '1.4.0'], + ['1.4.0-rc.3', '1.4.0'], + ['6.0.0-rc.0', '6.0.0'], + ])('promoteToStable(%s) → %s', (version, expected) => { + expect(promoteToStable(version)).toBe(expected) + }) +}) + +describe('nextPatchRc', () => { + it.each([ + ['1.4.0', '1.4.1-rc.1'], + ['1.4.5', '1.4.6-rc.1'], + ['6.0.0', '6.0.1-rc.1'], + ])('nextPatchRc(%s) → %s', (version, expected) => { + expect(nextPatchRc(version)).toBe(expected) + }) +}) + +describe('nextMainVersion', () => { + it.each([ + ['1.4.0-rc.1', '1.5.0-rc.0'], + ['1.4.0', '1.5.0-rc.0'], + ['6.0.0-rc.0', '6.1.0-rc.0'], + ])('nextMainVersion(%s) → %s', (version, expected) => { + expect(nextMainVersion(version)).toBe(expected) + }) +}) + +describe('versionMatchesBranch', () => { + it.each([ + ['1.4.0-rc.2', 'release/v1.4', true], + ['1.4.0', 'release/v1.4', true], + ['1.4.1-rc.1', 'release/v1.4', true], + ['1.5.0-rc.1', 'release/v1.4', false], + ['1.4.0-rc.1', 'release/v1.5', false], + ])('versionMatchesBranch(%s, %s) → %s', (version, branch, expected) => { + expect(versionMatchesBranch(version, branch)).toBe(expected) + }) +}) + +describe('previousStableTag', () => { + it('returns the second-highest stable tag', () => { + const tags = ['v1.4.0', 'v1.3.5', 'v1.3.4', 'v1.4.0-rc.2', 'v1.4.0-rc.1'] + expect(previousStableTag(tags)).toBe('v1.3.5') + }) + + it('skips all rc tags', () => { + const tags = ['v1.4.0', 'v1.3.5', 'v1.4.0-rc.2'] + expect(previousStableTag(tags)).toBe('v1.3.5') + }) + + it('returns null when fewer than two stable tags exist', () => { + expect(previousStableTag(['v1.4.0', 'v1.4.0-rc.1'])).toBeNull() + expect(previousStableTag([])).toBeNull() + }) +}) + +describe('isHighestStableTag', () => { + it('returns true when the new tag is strictly higher than all existing stable tags', () => { + expect(isHighestStableTag('v1.4.1', ['v1.4.0', 'v1.3.5'])).toBe(true) + }) + + it('returns false when a higher stable tag already exists', () => { + expect(isHighestStableTag('v1.3.5', ['v1.4.0', 'v1.3.4'])).toBe(false) + }) + + it('ignores rc tags when comparing', () => { + expect(isHighestStableTag('v1.4.1', ['v1.4.0', 'v1.5.0-rc.1'])).toBe(true) + }) + + it('returns true when no existing stable tags exist', () => { + expect(isHighestStableTag('v1.4.0', [])).toBe(true) + expect(isHighestStableTag('v1.4.0', ['v1.4.0-rc.1'])).toBe(true) + }) +}) diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts new file mode 100644 index 0000000000..3e0f87d050 --- /dev/null +++ b/ci/src/release/version.ts @@ -0,0 +1,48 @@ +import semver from 'semver' + +const VERSION_RE = /^\d+\.\d+\.\d+(-rc\.\d+)?$/ + +export function validateVersion(version: string): boolean { + return VERSION_RE.test(version) +} + +export function releaseBranchName(version: string): string { + const [major, minor] = version.split('.') + return `release/v${major}.${minor}` +} + +export function incrementRc(version: string): string { + const [base, pre] = version.split('-rc.') + return `${base}-rc.${parseInt(pre, 10) + 1}` +} + +export function promoteToStable(version: string): string { + return version.replace(/-rc\.\d+$/, '') +} + +export function nextPatchRc(version: string): string { + const [major, minor, patch] = version.split('.').map(Number) + return `${major}.${minor}.${patch + 1}-rc.1` +} + +export function nextMainVersion(version: string): string { + const base = promoteToStable(version) + const [major, minor] = base.split('.').map(Number) + return `${major}.${minor + 1}.0-rc.0` +} + +export function versionMatchesBranch(version: string, branch: string): boolean { + return releaseBranchName(version) === branch +} + +export function previousStableTag(tags: string[]): string | null { + const stable = tags + .filter((t) => !t.includes('-rc.')) + .sort((a, b) => semver.rcompare(a, b)) + return stable.length >= 2 ? stable[1] : null +} + +export function isHighestStableTag(newTag: string, existingTags: string[]): boolean { + const stableTags = existingTags.filter((t) => !t.includes('-rc.')) + return stableTags.every((t) => semver.gt(newTag, t)) +} diff --git a/ci/tsconfig.json b/ci/tsconfig.json new file mode 100644 index 0000000000..e511a712b5 --- /dev/null +++ b/ci/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": false, + "strictNullChecks": true, + "target": "esnext", + "types": ["jest", "node"] + }, + "exclude": ["node_modules"], + "include": ["src"] +} From 547e32e9cd5423d2ec74a1bb19f16acea1281991 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:24:51 +0200 Subject: [PATCH 04/24] feat: add ci release scripts --- ci/package-lock.json | 5181 ++++++++++++++++- ci/package.json | 28 +- ci/src/release/auto-bump.ts | 24 + ci/src/release/bump-version.ts | 14 + ci/src/release/check-branch-not-exists.ts | 13 + ci/src/release/check-tag-not-exists.ts | 11 + .../release/check-version-matches-branch.ts | 19 + ci/src/release/compute-tag.test.ts | 13 + ci/src/release/compute-tag.ts | 27 + ci/src/release/create-github-release.ts | 45 + ci/src/release/docker-push.ts | 40 + ci/src/release/open-main-bump-pr.ts | 47 + ci/src/release/open-release-pr.test.ts | 27 + ci/src/release/open-release-pr.ts | 53 + ci/src/release/validate-version.ts | 10 + ci/src/release/version.test.ts | 45 + ci/src/release/version.ts | 23 + 17 files changed, 5546 insertions(+), 74 deletions(-) create mode 100644 ci/src/release/auto-bump.ts create mode 100644 ci/src/release/bump-version.ts create mode 100644 ci/src/release/check-branch-not-exists.ts create mode 100644 ci/src/release/check-tag-not-exists.ts create mode 100644 ci/src/release/check-version-matches-branch.ts create mode 100644 ci/src/release/compute-tag.test.ts create mode 100644 ci/src/release/compute-tag.ts create mode 100644 ci/src/release/create-github-release.ts create mode 100644 ci/src/release/docker-push.ts create mode 100644 ci/src/release/open-main-bump-pr.ts create mode 100644 ci/src/release/open-release-pr.test.ts create mode 100644 ci/src/release/open-release-pr.ts create mode 100644 ci/src/release/validate-version.ts diff --git a/ci/package-lock.json b/ci/package-lock.json index e6886ff61d..7c24fab7f1 100644 --- a/ci/package-lock.json +++ b/ci/package-lock.json @@ -1,11 +1,11 @@ { - "name": "apl-core", + "name": "apl-core-ci", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "apl-core", + "name": "apl-core-ci", "version": "1.0.0", "license": "Apache-2.0", "dependencies": { @@ -17,122 +17,5076 @@ "yaml": "^2.8.3", "zx": "^8.2.2" }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "24.12.4", + "@types/semver": "^7.5.8", + "jest": "^30.4.2", + "ts-jest": "^29.4.11", + "tsx": "^4.19.4", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=24 <25" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "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", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.365", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.365.tgz", + "integrity": "sha512-xfip4u1QF1s+URFqpA6N+OeFpDGpN7VJz1f3MO3bVL0QYBjpGiZ5/Of7kugvM+o8TTqmanUlviHN3c8M9vYWCw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/envalid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.0.0.tgz", + "integrity": "sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.6.2" + }, + "engines": { + "node": ">=8.12" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "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" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "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" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, "engines": { - "node": ">=24 <25" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ts-jest": { + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.0", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, "engines": { - "node": ">=12" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/envalid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.0.0.tgz", - "integrity": "sha512-PGeYJnJB5naN0ME6SH8nFcDj9HVbLpYIfg1p5lAyM9T4cH2lwtu2fLbozC/bq+HUUOIFxhX/LP0/GmlqPHT4tQ==", + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "2.6.2" + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">=8.12" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "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": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/fastify" + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, { - "type": "opencollective", - "url": "https://opencollective.com/fastify" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "BSD-3-Clause" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/json-schema-traverse": { + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, "license": "MIT" }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "license": "0BSD" + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" }, "node_modules/yaml": { "version": "2.9.0", @@ -149,6 +5103,93 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zx": { "version": "8.8.5", "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", diff --git a/ci/package.json b/ci/package.json index 8175f77edc..8caeb43224 100644 --- a/ci/package.json +++ b/ci/package.json @@ -12,22 +12,42 @@ "yaml": "^2.8.3", "zx": "^8.2.2" }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "24.12.4", + "@types/semver": "^7.5.8", + "jest": "^30.4.2", + "ts-jest": "^29.4.11", + "tsx": "^4.19.4", + "typescript": "5.9.3" + }, "description": "CI scripts", "engines": { "node": ">=24 <25" }, "homepage": "https://github.com/linode/apl-core#readme", "license": "Apache-2.0", - "name": "apl-core", + "name": "apl-core-ci", "private": true, "repository": { "type": "git", "url": "git+https://github.com/linode/apl-core.git" }, "scripts": { - "update-helm-chart-deps": "src/update-helm-chart-deps.mjs", - "check-schema-versions": "src/check-schema-versions.mjs" + "update-helm-chart-deps": "node src/update-helm-chart-deps.mjs", + "check-schema-versions": "node src/check-schema-versions.mjs", + "test": "jest", + "release:validate-version": "tsx src/release/validate-version.ts", + "release:check-branch-not-exists": "tsx src/release/check-branch-not-exists.ts", + "release:bump-version": "tsx src/release/bump-version.ts", + "release:open-release-pr": "tsx src/release/open-release-pr.ts", + "release:open-main-bump-pr": "tsx src/release/open-main-bump-pr.ts", + "release:check-version-matches-branch": "tsx src/release/check-version-matches-branch.ts", + "release:compute-tag": "tsx src/release/compute-tag.ts", + "release:check-tag-not-exists": "tsx src/release/check-tag-not-exists.ts", + "release:auto-bump": "tsx src/release/auto-bump.ts", + "release:create-github-release": "tsx src/release/create-github-release.ts", + "release:docker-push": "tsx src/release/docker-push.ts" }, - "type": "commonjs", "version": "1.0.0" } diff --git a/ci/src/release/auto-bump.ts b/ci/src/release/auto-bump.ts new file mode 100644 index 0000000000..29d4f8ea77 --- /dev/null +++ b/ci/src/release/auto-bump.ts @@ -0,0 +1,24 @@ +import { execSync } from 'child_process' +import { readFileSync } from 'fs' +import path from 'path' +import { incrementRc, nextPatchRc } from './version' + +const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') +const dryRun = process.env.DRY_RUN === 'true' + +const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) +const currentVersion: string = pkg.version +const isRc = currentVersion.includes('-rc.') +const nextVersion = isRc ? incrementRc(currentVersion) : nextPatchRc(currentVersion) + +console.log(`Auto-bumping from ${currentVersion} → ${nextVersion}`) + +if (dryRun) { + console.log(`[dry-run] Would bump package.json to ${nextVersion} and push`) + process.exit(0) +} + +execSync(`npm version ${nextVersion} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git commit -m "chore(release): bump to v${nextVersion}"`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git push`, { cwd: repoRoot, stdio: 'inherit' }) diff --git a/ci/src/release/bump-version.ts b/ci/src/release/bump-version.ts new file mode 100644 index 0000000000..9a43f4b8ae --- /dev/null +++ b/ci/src/release/bump-version.ts @@ -0,0 +1,14 @@ +import { execSync } from 'child_process' +import path from 'path' + +const version = process.env.VERSION! +const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') +const dryRun = process.env.DRY_RUN === 'true' + +if (dryRun) { + console.log(`[dry-run] Would run: npm version ${version} --no-git-tag-version`) + process.exit(0) +} + +execSync(`npm version ${version} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) +console.log(`Bumped package.json to ${version}`) diff --git a/ci/src/release/check-branch-not-exists.ts b/ci/src/release/check-branch-not-exists.ts new file mode 100644 index 0000000000..2df9d12618 --- /dev/null +++ b/ci/src/release/check-branch-not-exists.ts @@ -0,0 +1,13 @@ +import { execSync } from 'child_process' +import { releaseBranchName } from './version' + +const version = process.env.VERSION! +const branch = releaseBranchName(version) + +try { + execSync(`git ls-remote --exit-code origin refs/heads/${branch}`, { stdio: 'pipe' }) + console.error(`Branch "${branch}" already exists. Aborting to prevent cycle restart.`) + process.exit(1) +} catch { + console.log(`Branch "${branch}" does not exist — safe to create`) +} diff --git a/ci/src/release/check-tag-not-exists.ts b/ci/src/release/check-tag-not-exists.ts new file mode 100644 index 0000000000..891f47287a --- /dev/null +++ b/ci/src/release/check-tag-not-exists.ts @@ -0,0 +1,11 @@ +import { execSync } from 'child_process' + +const tag = process.env.RELEASE_TAG! + +try { + execSync(`git rev-parse --verify "refs/tags/${tag}"`, { stdio: 'pipe' }) + console.error(`Tag "${tag}" already exists. Aborting to prevent duplicate release.`) + process.exit(1) +} catch { + console.log(`Tag "${tag}" does not exist — safe to create`) +} diff --git a/ci/src/release/check-version-matches-branch.ts b/ci/src/release/check-version-matches-branch.ts new file mode 100644 index 0000000000..4f99c935d4 --- /dev/null +++ b/ci/src/release/check-version-matches-branch.ts @@ -0,0 +1,19 @@ +import { readFileSync } from 'fs' +import path from 'path' +import { versionMatchesBranch } from './version' + +const releaseBranch = process.env.RELEASE_BRANCH! +const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + +const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) +const version: string = pkg.version + +if (!versionMatchesBranch(version, releaseBranch)) { + console.error( + `Version mismatch: package.json is at "${version}" but branch is "${releaseBranch}". ` + + `Expected major.minor to be "${releaseBranch.replace('release/v', '')}".`, + ) + process.exit(1) +} + +console.log(`Version "${version}" matches branch "${releaseBranch}"`) diff --git a/ci/src/release/compute-tag.test.ts b/ci/src/release/compute-tag.test.ts new file mode 100644 index 0000000000..db8e0d99d1 --- /dev/null +++ b/ci/src/release/compute-tag.test.ts @@ -0,0 +1,13 @@ +import { computeTag } from './compute-tag' + +describe('computeTag', () => { + it('returns the version as-is when not promoting to stable', () => { + expect(computeTag('1.4.0-rc.1', false)).toBe('v1.4.0-rc.1') + expect(computeTag('1.4.0-rc.9', false)).toBe('v1.4.0-rc.9') + }) + + it('strips the RC suffix when promoting to stable', () => { + expect(computeTag('1.4.0-rc.3', true)).toBe('v1.4.0') + expect(computeTag('6.0.0-rc.0', true)).toBe('v6.0.0') + }) +}) diff --git a/ci/src/release/compute-tag.ts b/ci/src/release/compute-tag.ts new file mode 100644 index 0000000000..42000a7bf3 --- /dev/null +++ b/ci/src/release/compute-tag.ts @@ -0,0 +1,27 @@ +import { appendFileSync } from 'fs' +import { readFileSync } from 'fs' +import path from 'path' +import { promoteToStable } from './version' + +export function computeTag(version: string, promote: boolean): string { + return `v${promote ? promoteToStable(version) : version}` +} + +async function main() { + const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) + const version: string = pkg.version + const promote = process.env.PROMOTE_TO_STABLE === 'true' + const tag = computeTag(version, promote) + + console.log(`Computed tag: ${tag}`) + + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tag}\n`) + appendFileSync(process.env.GITHUB_OUTPUT, `is_prerelease=${tag.includes('-rc.')}\n`) + } +} + +if (require.main === module) { + main().catch((err) => { console.error(err.message); process.exit(1) }) +} diff --git a/ci/src/release/create-github-release.ts b/ci/src/release/create-github-release.ts new file mode 100644 index 0000000000..738219346c --- /dev/null +++ b/ci/src/release/create-github-release.ts @@ -0,0 +1,45 @@ +import { execSync } from 'child_process' +import { previousRcTag, previousStableTagBefore } from './version' + +async function main() { + const tag = process.env.RELEASE_TAG! + const isPrerelease = process.env.IS_PRERELEASE === 'true' + const dryRun = process.env.DRY_RUN === 'true' + + const allTags = execSync('git tag --sort=-v:refname', { encoding: 'utf8' }) + .trim() + .split('\n') + .filter(Boolean) + + const previousTag = isPrerelease + ? previousRcTag(tag, allTags) + : previousStableTagBefore(tag, allTags) + + const prereleaseFlag = isPrerelease ? '--prerelease' : '' + const notesStartFlag = previousTag ? `--notes-start-tag "${previousTag}"` : '' + const rcBanner = isPrerelease + ? `> ⚠️ This is a release candidate. Do not use in production without testing.\n\n` + : '' + + console.log(`Creating GitHub release for ${tag}`) + console.log(` prerelease: ${isPrerelease}`) + console.log(` previous tag: ${previousTag ?? '(none)'}`) + + if (dryRun) { + console.log(`[dry-run] Would run: gh release create ${tag} --generate-notes ${notesStartFlag} ${prereleaseFlag}`) + return + } + + const notesCmd = [ + `gh release create "${tag}"`, + `--title "Release ${tag}"`, + `--generate-notes`, + notesStartFlag, + prereleaseFlag, + rcBanner ? `--notes-prepend "${rcBanner}"` : '', + ].filter(Boolean).join(' ') + + execSync(notesCmd, { stdio: 'inherit' }) +} + +main().catch((err) => { console.error(err.message); process.exit(1) }) diff --git a/ci/src/release/docker-push.ts b/ci/src/release/docker-push.ts new file mode 100644 index 0000000000..6178afc10a --- /dev/null +++ b/ci/src/release/docker-push.ts @@ -0,0 +1,40 @@ +import { execSync } from 'child_process' +import { isHighestStableTag } from './version' + +async function main() { + const tag = process.env.RELEASE_TAG! + const cacheImage = process.env.CACHE_IMAGE! + const dockerRepo = process.env.DOCKER_REPO ?? 'linode/apl-core' + const cacheRegistry = process.env.CACHE_REGISTRY ?? 'ghcr.io/linode/apl-core' + const dryRun = process.env.DRY_RUN === 'true' + const isRc = tag.includes('-rc.') + + const allTags = execSync('git tag --sort=-v:refname', { encoding: 'utf8' }) + .trim() + .split('\n') + .filter(Boolean) + + const shouldPushLatest = !isRc && isHighestStableTag(tag, allTags) + + console.log(`Pushing Docker image for ${tag}`) + console.log(` cache image: ${cacheImage}`) + console.log(` push :latest: ${shouldPushLatest}`) + + function run(cmd: string) { + if (dryRun) { console.log(`[dry-run] ${cmd}`); return } + execSync(cmd, { stdio: 'inherit' }) + } + + run(`docker pull "${cacheImage}"`) + run(`docker tag "${cacheImage}" "${dockerRepo}:${tag}"`) + run(`docker push "${dockerRepo}:${tag}"`) + run(`docker tag "${cacheImage}" "${cacheRegistry}:${tag}"`) + run(`docker push "${cacheRegistry}:${tag}"`) + + if (shouldPushLatest) { + run(`docker tag "${cacheImage}" "${dockerRepo}:latest"`) + run(`docker push "${dockerRepo}:latest"`) + } +} + +main().catch((err) => { console.error(err.message); process.exit(1) }) diff --git a/ci/src/release/open-main-bump-pr.ts b/ci/src/release/open-main-bump-pr.ts new file mode 100644 index 0000000000..3da7e8ede3 --- /dev/null +++ b/ci/src/release/open-main-bump-pr.ts @@ -0,0 +1,47 @@ +import { execSync } from 'child_process' +import { writeFileSync, unlinkSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import path from 'path' +import { nextMainVersion } from './version' + +async function main() { + const version = process.env.VERSION! + const baseBranch = process.env.BASE_BRANCH! + const dryRun = process.env.DRY_RUN === 'true' + const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + + const nextVersion = nextMainVersion(version) + const bumpBranch = `chore/bump-main-v${nextVersion}` + const title = `chore: bump main to v${nextVersion} after cutting release/v${version.split('.').slice(0, 2).join('.')}` + const body = `Automated version bump after cutting the \`release/v${version.split('.').slice(0, 2).join('.')}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` + + if (dryRun) { + console.log('[dry-run] Would create main bump PR:') + console.log(` branch: ${bumpBranch}`) + console.log(` title: ${title}`) + console.log(` next version: ${nextVersion}`) + return + } + + execSync(`git checkout "${baseBranch}"`, { cwd: repoRoot, stdio: 'inherit' }) + execSync(`git pull`, { cwd: repoRoot, stdio: 'inherit' }) + execSync(`git checkout -b "${bumpBranch}"`, { cwd: repoRoot, stdio: 'inherit' }) + execSync(`npm version ${nextVersion} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) + execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) + execSync(`git commit -m "chore(release): bump main to v${nextVersion}"`, { cwd: repoRoot, stdio: 'inherit' }) + execSync(`git push -u origin "${bumpBranch}"`, { cwd: repoRoot, stdio: 'inherit' }) + + const bodyFile = join(tmpdir(), `pr-body-${Date.now()}.md`) + writeFileSync(bodyFile, body) + try { + execSync(`gh pr create --title "${title}" --body-file "${bodyFile}" --base "${baseBranch}" --head "${bumpBranch}"`, { + cwd: repoRoot, + stdio: 'inherit', + }) + } finally { + unlinkSync(bodyFile) + } +} + +main().catch((err) => { console.error(err.message); process.exit(1) }) diff --git a/ci/src/release/open-release-pr.test.ts b/ci/src/release/open-release-pr.test.ts new file mode 100644 index 0000000000..6b8c600650 --- /dev/null +++ b/ci/src/release/open-release-pr.test.ts @@ -0,0 +1,27 @@ +import { buildPrBody } from './open-release-pr' + +describe('buildPrBody', () => { + it('RC body contains base checklist only', () => { + const body = buildPrBody('1.4.0-rc.1') + expect(body).toContain('Integration tests passed') + expect(body).toContain('Release notes reviewed') + expect(body).toContain('Sign-off received') + expect(body).not.toContain('Docs updated') + expect(body).not.toContain('Helm chart version confirmed') + expect(body).not.toContain('Announced in #releases') + }) + + it('stable body contains base checklist plus stable-specific items', () => { + const body = buildPrBody('1.4.0') + expect(body).toContain('Integration tests passed') + expect(body).toContain('Sign-off received') + expect(body).toContain('Docs updated') + expect(body).toContain('Helm chart version confirmed') + expect(body).toContain('Announced in #releases') + }) + + it('body header includes the version', () => { + expect(buildPrBody('1.4.0-rc.2')).toContain('v1.4.0-rc.2') + expect(buildPrBody('1.4.0')).toContain('v1.4.0') + }) +}) diff --git a/ci/src/release/open-release-pr.ts b/ci/src/release/open-release-pr.ts new file mode 100644 index 0000000000..3b5a8794f7 --- /dev/null +++ b/ci/src/release/open-release-pr.ts @@ -0,0 +1,53 @@ +import { execSync } from 'child_process' +import { writeFileSync, unlinkSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { releaseBranchName } from './version' + +export function buildPrBody(version: string): string { + const isRc = version.includes('-rc.') + const lines = [ + `## Release checklist — v${version}`, + '', + '- [ ] Integration tests passed', + '- [ ] Release notes reviewed', + '- [ ] Sign-off received', + ] + if (!isRc) { + lines.push('- [ ] Docs updated') + lines.push('- [ ] Helm chart version confirmed') + lines.push('- [ ] Announced in #releases') + } + return lines.join('\n') +} + +async function main() { + const version = process.env.VERSION! + const baseBranch = process.env.BASE_BRANCH! + const dryRun = process.env.DRY_RUN === 'true' + const branch = releaseBranchName(version) + const title = `release: v${version}` + const body = buildPrBody(version) + + if (dryRun) { + console.log('[dry-run] Would open PR:') + console.log(` title: ${title}`) + console.log(` head: ${branch} → ${baseBranch}`) + console.log(` body:\n${body}`) + return + } + + const bodyFile = join(tmpdir(), `pr-body-${Date.now()}.md`) + writeFileSync(bodyFile, body) + try { + execSync(`gh pr create --title "${title}" --body-file "${bodyFile}" --base "${baseBranch}" --head "${branch}"`, { + stdio: 'inherit', + }) + } finally { + unlinkSync(bodyFile) + } +} + +if (require.main === module) { + main().catch((err) => { console.error(err.message); process.exit(1) }) +} diff --git a/ci/src/release/validate-version.ts b/ci/src/release/validate-version.ts new file mode 100644 index 0000000000..15b32368bb --- /dev/null +++ b/ci/src/release/validate-version.ts @@ -0,0 +1,10 @@ +import { validateVersion } from './version' + +const version = process.env.VERSION ?? '' + +if (!validateVersion(version)) { + console.error(`Invalid version: "${version}". Expected format: 1.4.0 or 1.4.0-rc.1`) + process.exit(1) +} + +console.log(`Version "${version}" is valid`) diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index 634c9ea022..8a21038337 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -8,6 +8,8 @@ import { versionMatchesBranch, previousStableTag, isHighestStableTag, + previousRcTag, + previousStableTagBefore, } from './version' describe('validateVersion', () => { @@ -109,6 +111,49 @@ describe('previousStableTag', () => { }) }) +describe('previousRcTag', () => { + it('returns the previous RC tag in the same major.minor series', () => { + const tags = ['v1.4.0-rc.2', 'v1.4.0-rc.1', 'v1.4.0-rc.0', 'v1.3.5'] + expect(previousRcTag('v1.4.0-rc.2', tags)).toBe('v1.4.0-rc.1') + }) + + it('excludes the current tag from consideration', () => { + const tags = ['v1.4.0-rc.1', 'v1.4.0-rc.0'] + expect(previousRcTag('v1.4.0-rc.1', tags)).toBe('v1.4.0-rc.0') + }) + + it('ignores RC tags from other major.minor series', () => { + const tags = ['v1.4.0-rc.1', 'v1.3.0-rc.2'] + expect(previousRcTag('v1.4.0-rc.1', tags)).toBeNull() + }) + + it('returns null when no previous RC tag exists', () => { + expect(previousRcTag('v1.4.0-rc.0', ['v1.4.0-rc.0'])).toBeNull() + }) +}) + +describe('previousStableTagBefore', () => { + it('returns the highest stable tag strictly before the given tag', () => { + const tags = ['v1.4.0', 'v1.3.5', 'v1.3.4'] + expect(previousStableTagBefore('v1.4.0', tags)).toBe('v1.3.5') + }) + + it('handles backport patches correctly', () => { + // releasing v1.3.6 when v1.4.0 already exists — should return v1.3.5, not v1.4.0 + const tags = ['v1.4.0', 'v1.3.6', 'v1.3.5'] + expect(previousStableTagBefore('v1.3.6', tags)).toBe('v1.3.5') + }) + + it('ignores RC tags', () => { + const tags = ['v1.4.0', 'v1.3.5', 'v1.4.0-rc.2'] + expect(previousStableTagBefore('v1.4.0', tags)).toBe('v1.3.5') + }) + + it('returns null when no previous stable tag exists', () => { + expect(previousStableTagBefore('v1.4.0', ['v1.4.0'])).toBeNull() + }) +}) + describe('isHighestStableTag', () => { it('returns true when the new tag is strictly higher than all existing stable tags', () => { expect(isHighestStableTag('v1.4.1', ['v1.4.0', 'v1.3.5'])).toBe(true) diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index 3e0f87d050..adf5d80a50 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -42,6 +42,29 @@ export function previousStableTag(tags: string[]): string | null { return stable.length >= 2 ? stable[1] : null } +export function previousStableTagBefore(newTag: string, tags: string[]): string | null { + const stable = tags + .filter((t) => !t.includes('-rc.')) + .filter((t) => semver.lt(t, newTag)) + .sort((a, b) => semver.rcompare(a, b)) + return stable.length > 0 ? stable[0] : null +} + +export function previousRcTag(currentTag: string, tags: string[]): string | null { + const [majorMinorPatch] = currentTag.replace('v', '').split('-rc.') + const [major, minor] = majorMinorPatch.split('.') + const sameSeries = tags + .filter((t) => t !== currentTag) + .filter((t) => t.includes('-rc.')) + .filter((t) => { + const bare = t.replace('v', '').split('-rc.')[0] + const [m, n] = bare.split('.') + return m === major && n === minor + }) + .sort((a, b) => semver.rcompare(a, b)) + return sameSeries.length > 0 ? sameSeries[0] : null +} + export function isHighestStableTag(newTag: string, existingTags: string[]): boolean { const stableTags = existingTags.filter((t) => !t.includes('-rc.')) return stableTags.every((t) => semver.gt(newTag, t)) From e5de673287026bfd6530d3e99771681d658f7e7c Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:25:26 +0200 Subject: [PATCH 05/24] ci: add gh release actions --- .github/workflows/cut-release-branch.yml | 128 +++++++++++++++++ .github/workflows/release-from-branch.yml | 166 ++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 .github/workflows/cut-release-branch.yml create mode 100644 .github/workflows/release-from-branch.yml diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml new file mode 100644 index 0000000000..ffebdf5c96 --- /dev/null +++ b/.github/workflows/cut-release-branch.yml @@ -0,0 +1,128 @@ +name: Cut Release Branch + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g. 1.4.0 or 1.4.0-rc.1)' + required: true + type: string + base_branch: + description: 'Branch to cut from' + required: true + default: 'main' + type: string + dry_run: + description: 'Dry run — skip all writes (git push, PRs)' + required: true + default: 'true' + type: choice + options: + - 'true' + - 'false' + +env: + CACHE_REGISTRY: ghcr.io + CACHE_REPO: linode/apl-core + BOT_EMAIL: ${{ vars.BOT_EMAIL }} + BOT_USERNAME: ${{ vars.BOT_USERNAME }} + +jobs: + cut-release-branch: + runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.base_branch }} + fetch-depth: 0 + token: ${{ secrets.BOT_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install CI dependencies + working-directory: ci + run: npm ci + + - name: Validate version input + working-directory: ci + run: npm run release:validate-version + env: + VERSION: ${{ inputs.version }} + + - name: Check release branch does not already exist + working-directory: ci + run: npm run release:check-branch-not-exists + env: + VERSION: ${{ inputs.version }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.CACHE_REGISTRY }} + username: ${{ env.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + + - name: Run tests in Docker (test gate) + uses: docker/build-push-action@v7 + with: + push: false + load: true + context: . + build-args: SKIP_TESTS=false + + - name: Configure git bot identity + run: | + git config --global user.email "$BOT_EMAIL" + git config --global user.name "$BOT_USERNAME" + + - name: Create release branch + run: | + VERSION="${{ inputs.version }}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + BRANCH="release/v${MAJOR}.${MINOR}" + echo "RELEASE_BRANCH=$BRANCH" >> "$GITHUB_ENV" + git checkout -b "$BRANCH" + + - name: Bump package.json to target version + working-directory: ci + run: npm run release:bump-version + env: + VERSION: ${{ inputs.version }} + REPO_ROOT: ${{ github.workspace }} + DRY_RUN: ${{ inputs.dry_run }} + + - name: Commit and push release branch + if: ${{ inputs.dry_run == 'false' }} + run: | + git add package.json package-lock.json + git commit -m "chore(release): bump to v${{ inputs.version }}" + git push -u origin "$RELEASE_BRANCH" + + - name: Open release PR + working-directory: ci + run: npm run release:open-release-pr + env: + VERSION: ${{ inputs.version }} + BASE_BRANCH: ${{ inputs.base_branch }} + DRY_RUN: ${{ inputs.dry_run }} + + - name: Open main version bump PR + working-directory: ci + run: npm run release:open-main-bump-pr + env: + VERSION: ${{ inputs.version }} + BASE_BRANCH: ${{ inputs.base_branch }} + REPO_ROOT: ${{ github.workspace }} + DRY_RUN: ${{ inputs.dry_run }} diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml new file mode 100644 index 0000000000..5b8ab9c5b9 --- /dev/null +++ b/.github/workflows/release-from-branch.yml @@ -0,0 +1,166 @@ +name: Release from Branch + +on: + workflow_dispatch: + inputs: + release_branch: + description: 'Release cycle branch (e.g. release/v1.4)' + required: true + type: string + promote_to_stable: + description: 'Promote to stable — strips RC suffix and cuts stable tag' + required: true + default: 'false' + type: choice + options: + - 'false' + - 'true' + dry_run: + description: 'Dry run — skip all writes (tag, release, Docker push, Helm)' + required: true + default: 'true' + type: choice + options: + - 'true' + - 'false' + +env: + REPO: linode/apl-core + CACHE_REGISTRY: ghcr.io + CACHE_REPO: linode/apl-core + BOT_EMAIL: ${{ vars.BOT_EMAIL }} + BOT_USERNAME: ${{ vars.BOT_USERNAME }} + DOCKER_USERNAME: ${{ vars.DOCKERHUB_LINODEBOT_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_LINODEBOT_TOKEN }} + +jobs: + release-from-branch: + runs-on: ubuntu-22.04 + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.release_branch }} + fetch-depth: 0 + token: ${{ secrets.BOT_TOKEN }} + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install CI dependencies + working-directory: ci + run: npm ci + + - name: Check package.json version matches release branch + working-directory: ci + run: npm run release:check-version-matches-branch + env: + RELEASE_BRANCH: ${{ inputs.release_branch }} + REPO_ROOT: ${{ github.workspace }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.CACHE_REGISTRY }} + username: ${{ env.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + + - name: Run tests in Docker (test gate) + uses: docker/build-push-action@v7 + with: + push: ${{ inputs.dry_run == 'false' }} + context: . + build-args: SKIP_TESTS=false + tags: ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }}:${{ inputs.release_branch }} + + - name: Compute release tag + id: compute-tag + working-directory: ci + run: npm run release:compute-tag + env: + PROMOTE_TO_STABLE: ${{ inputs.promote_to_stable }} + REPO_ROOT: ${{ github.workspace }} + + - name: Check tag does not already exist + working-directory: ci + run: npm run release:check-tag-not-exists + env: + RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} + + - name: Configure git bot identity + run: | + git config --global user.email "$BOT_EMAIL" + git config --global user.name "$BOT_USERNAME" + + - name: Promote package.json to stable (if applicable) + if: ${{ inputs.promote_to_stable == 'true' && inputs.dry_run == 'false' }} + run: | + npm version "${{ steps.compute-tag.outputs.tag }}" --no-git-tag-version + git add package.json package-lock.json + git commit -m "chore(release): promote to ${{ steps.compute-tag.outputs.tag }}" + git push + + - name: Create and push git tag + if: ${{ inputs.dry_run == 'false' }} + run: | + git tag -a "${{ steps.compute-tag.outputs.tag }}" -m "Release ${{ steps.compute-tag.outputs.tag }}" + git push --follow-tags + + - name: Auto-bump version for next iteration + working-directory: ci + run: npm run release:auto-bump + env: + REPO_ROOT: ${{ github.workspace }} + DRY_RUN: ${{ inputs.dry_run }} + + - name: Create GitHub Release + working-directory: ci + run: npm run release:create-github-release + env: + RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} + IS_PRERELEASE: ${{ steps.compute-tag.outputs.is_prerelease }} + DRY_RUN: ${{ inputs.dry_run }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + if: ${{ inputs.dry_run == 'false' }} + uses: docker/login-action@v4 + with: + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} + + - name: Push Docker image with release tag + working-directory: ci + run: npm run release:docker-push + env: + RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} + CACHE_IMAGE: ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }}:${{ inputs.release_branch }} + DOCKER_REPO: ${{ env.REPO }} + CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }} + DRY_RUN: ${{ inputs.dry_run }} + + - name: Prepare Helm chart + if: ${{ inputs.dry_run == 'false' }} + env: + BOT_EMAIL: ${{ vars.BOT_EMAIL }} + BOT_USERNAME: ${{ vars.BOT_USERNAME }} + run: ci/scripts/prepare_chart_for_release.sh + + - name: Publish Helm chart + if: ${{ inputs.dry_run == 'false' }} + uses: helm/chart-releaser-action@v1.7.0 + with: + charts_dir: chart + skip_existing: true + mark_as_latest: false + env: + CR_TOKEN: ${{ secrets.GITHUB_TOKEN }} From bb72d395de04f3d92db59de159449717298558dd Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:37:46 +0200 Subject: [PATCH 06/24] ci: do not push artifacts to ghcr --- .github/workflows/cut-release-branch.yml | 9 --------- .github/workflows/release-from-branch.yml | 17 ++++------------- ci/src/release/docker-push.ts | 4 ---- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index ffebdf5c96..679a772f56 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -22,8 +22,6 @@ on: - 'false' env: - CACHE_REGISTRY: ghcr.io - CACHE_REPO: linode/apl-core BOT_EMAIL: ${{ vars.BOT_EMAIL }} BOT_USERNAME: ${{ vars.BOT_USERNAME }} @@ -66,13 +64,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ${{ env.CACHE_REGISTRY }} - username: ${{ env.BOT_USERNAME }} - password: ${{ secrets.BOT_TOKEN }} - - name: Run tests in Docker (test gate) uses: docker/build-push-action@v7 with: diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index 5b8ab9c5b9..6b5657a75a 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -26,8 +26,6 @@ on: env: REPO: linode/apl-core - CACHE_REGISTRY: ghcr.io - CACHE_REPO: linode/apl-core BOT_EMAIL: ${{ vars.BOT_EMAIL }} BOT_USERNAME: ${{ vars.BOT_USERNAME }} DOCKER_USERNAME: ${{ vars.DOCKERHUB_LINODEBOT_USERNAME }} @@ -67,20 +65,14 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ${{ env.CACHE_REGISTRY }} - username: ${{ env.BOT_USERNAME }} - password: ${{ secrets.BOT_TOKEN }} - - name: Run tests in Docker (test gate) uses: docker/build-push-action@v7 with: - push: ${{ inputs.dry_run == 'false' }} + push: false + load: true context: . build-args: SKIP_TESTS=false - tags: ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }}:${{ inputs.release_branch }} + tags: notused - name: Compute release tag id: compute-tag @@ -143,9 +135,8 @@ jobs: run: npm run release:docker-push env: RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} - CACHE_IMAGE: ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }}:${{ inputs.release_branch }} + CACHE_IMAGE: notused DOCKER_REPO: ${{ env.REPO }} - CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }} DRY_RUN: ${{ inputs.dry_run }} - name: Prepare Helm chart diff --git a/ci/src/release/docker-push.ts b/ci/src/release/docker-push.ts index 6178afc10a..2afa476b9e 100644 --- a/ci/src/release/docker-push.ts +++ b/ci/src/release/docker-push.ts @@ -5,7 +5,6 @@ async function main() { const tag = process.env.RELEASE_TAG! const cacheImage = process.env.CACHE_IMAGE! const dockerRepo = process.env.DOCKER_REPO ?? 'linode/apl-core' - const cacheRegistry = process.env.CACHE_REGISTRY ?? 'ghcr.io/linode/apl-core' const dryRun = process.env.DRY_RUN === 'true' const isRc = tag.includes('-rc.') @@ -25,11 +24,8 @@ async function main() { execSync(cmd, { stdio: 'inherit' }) } - run(`docker pull "${cacheImage}"`) run(`docker tag "${cacheImage}" "${dockerRepo}:${tag}"`) run(`docker push "${dockerRepo}:${tag}"`) - run(`docker tag "${cacheImage}" "${cacheRegistry}:${tag}"`) - run(`docker push "${cacheRegistry}:${tag}"`) if (shouldPushLatest) { run(`docker tag "${cacheImage}" "${dockerRepo}:latest"`) From 47212bf30cf36eb924eadbae00f4920aad7a6834 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:46:15 +0200 Subject: [PATCH 07/24] ci: add comments --- .github/workflows/release-from-branch.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index 6b5657a75a..f89a4ac794 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -146,6 +146,10 @@ jobs: BOT_USERNAME: ${{ vars.BOT_USERNAME }} run: ci/scripts/prepare_chart_for_release.sh + # chart-releaser-action has no RC awareness — it publishes whatever version + # prepare_chart_for_release.sh stamps into Chart.yaml (e.g. 1.4.0-rc.1). + # mark_as_latest: false prevents RC charts from becoming the default in + # `helm search repo`, which is the closest Helm equivalent to a prerelease flag. - name: Publish Helm chart if: ${{ inputs.dry_run == 'false' }} uses: helm/chart-releaser-action@v1.7.0 From 016c41d5ec58438f5b0076df086e52545407e622 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:54:00 +0200 Subject: [PATCH 08/24] ci: use minor version while cuting the release branch --- .github/workflows/cut-release-branch.yml | 21 ++++++++---------- ci/src/release/bump-version.ts | 5 ++++- ci/src/release/check-branch-not-exists.ts | 5 ++--- ci/src/release/open-main-bump-pr.ts | 10 ++++----- ci/src/release/open-release-pr.ts | 7 +++--- ci/src/release/validate-version.ts | 10 ++++----- ci/src/release/version.test.ts | 27 +++++++++++++++++++++++ ci/src/release/version.ts | 9 ++++++++ 8 files changed, 65 insertions(+), 29 deletions(-) diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index 679a772f56..432864a7d4 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -3,8 +3,8 @@ name: Cut Release Branch on: workflow_dispatch: inputs: - version: - description: 'Version to release (e.g. 1.4.0 or 1.4.0-rc.1)' + minor_version: + description: 'Major.minor version to release (e.g. 1.4)' required: true type: string base_branch: @@ -53,13 +53,13 @@ jobs: working-directory: ci run: npm run release:validate-version env: - VERSION: ${{ inputs.version }} + MINOR_VERSION: ${{ inputs.minor_version }} - name: Check release branch does not already exist working-directory: ci run: npm run release:check-branch-not-exists env: - VERSION: ${{ inputs.version }} + MINOR_VERSION: ${{ inputs.minor_version }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -79,10 +79,7 @@ jobs: - name: Create release branch run: | - VERSION="${{ inputs.version }}" - MAJOR=$(echo "$VERSION" | cut -d. -f1) - MINOR=$(echo "$VERSION" | cut -d. -f2) - BRANCH="release/v${MAJOR}.${MINOR}" + BRANCH="release/v${{ inputs.minor_version }}" echo "RELEASE_BRANCH=$BRANCH" >> "$GITHUB_ENV" git checkout -b "$BRANCH" @@ -90,7 +87,7 @@ jobs: working-directory: ci run: npm run release:bump-version env: - VERSION: ${{ inputs.version }} + MINOR_VERSION: ${{ inputs.minor_version }} REPO_ROOT: ${{ github.workspace }} DRY_RUN: ${{ inputs.dry_run }} @@ -98,14 +95,14 @@ jobs: if: ${{ inputs.dry_run == 'false' }} run: | git add package.json package-lock.json - git commit -m "chore(release): bump to v${{ inputs.version }}" + git commit -m "chore(release): bump to v${{ inputs.minor_version }}.0-rc.1" git push -u origin "$RELEASE_BRANCH" - name: Open release PR working-directory: ci run: npm run release:open-release-pr env: - VERSION: ${{ inputs.version }} + MINOR_VERSION: ${{ inputs.minor_version }} BASE_BRANCH: ${{ inputs.base_branch }} DRY_RUN: ${{ inputs.dry_run }} @@ -113,7 +110,7 @@ jobs: working-directory: ci run: npm run release:open-main-bump-pr env: - VERSION: ${{ inputs.version }} + MINOR_VERSION: ${{ inputs.minor_version }} BASE_BRANCH: ${{ inputs.base_branch }} REPO_ROOT: ${{ github.workspace }} DRY_RUN: ${{ inputs.dry_run }} diff --git a/ci/src/release/bump-version.ts b/ci/src/release/bump-version.ts index 9a43f4b8ae..fc21f4d2b0 100644 --- a/ci/src/release/bump-version.ts +++ b/ci/src/release/bump-version.ts @@ -1,10 +1,13 @@ import { execSync } from 'child_process' import path from 'path' +import { cycleStartVersion } from './version' -const version = process.env.VERSION! +const minorVersion = process.env.MINOR_VERSION! const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') const dryRun = process.env.DRY_RUN === 'true' +const version = cycleStartVersion(minorVersion) + if (dryRun) { console.log(`[dry-run] Would run: npm version ${version} --no-git-tag-version`) process.exit(0) diff --git a/ci/src/release/check-branch-not-exists.ts b/ci/src/release/check-branch-not-exists.ts index 2df9d12618..36ca89fc93 100644 --- a/ci/src/release/check-branch-not-exists.ts +++ b/ci/src/release/check-branch-not-exists.ts @@ -1,8 +1,7 @@ import { execSync } from 'child_process' -import { releaseBranchName } from './version' -const version = process.env.VERSION! -const branch = releaseBranchName(version) +const minorVersion = process.env.MINOR_VERSION! +const branch = `release/v${minorVersion}` try { execSync(`git ls-remote --exit-code origin refs/heads/${branch}`, { stdio: 'pipe' }) diff --git a/ci/src/release/open-main-bump-pr.ts b/ci/src/release/open-main-bump-pr.ts index 3da7e8ede3..dbe8d92c68 100644 --- a/ci/src/release/open-main-bump-pr.ts +++ b/ci/src/release/open-main-bump-pr.ts @@ -3,18 +3,18 @@ import { writeFileSync, unlinkSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import path from 'path' -import { nextMainVersion } from './version' +import { nextMainVersion, cycleStartVersion } from './version' async function main() { - const version = process.env.VERSION! + const minorVersion = process.env.MINOR_VERSION! const baseBranch = process.env.BASE_BRANCH! const dryRun = process.env.DRY_RUN === 'true' const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - const nextVersion = nextMainVersion(version) + const nextVersion = nextMainVersion(cycleStartVersion(minorVersion)) const bumpBranch = `chore/bump-main-v${nextVersion}` - const title = `chore: bump main to v${nextVersion} after cutting release/v${version.split('.').slice(0, 2).join('.')}` - const body = `Automated version bump after cutting the \`release/v${version.split('.').slice(0, 2).join('.')}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` + const title = `chore: bump main to v${nextVersion} after cutting release/v${minorVersion}` + const body = `Automated version bump after cutting the \`release/v${minorVersion}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` if (dryRun) { console.log('[dry-run] Would create main bump PR:') diff --git a/ci/src/release/open-release-pr.ts b/ci/src/release/open-release-pr.ts index 3b5a8794f7..1a228963f8 100644 --- a/ci/src/release/open-release-pr.ts +++ b/ci/src/release/open-release-pr.ts @@ -2,7 +2,7 @@ import { execSync } from 'child_process' import { writeFileSync, unlinkSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' -import { releaseBranchName } from './version' +import { releaseBranchName, cycleStartVersion } from './version' export function buildPrBody(version: string): string { const isRc = version.includes('-rc.') @@ -22,11 +22,12 @@ export function buildPrBody(version: string): string { } async function main() { - const version = process.env.VERSION! + const minorVersion = process.env.MINOR_VERSION! const baseBranch = process.env.BASE_BRANCH! const dryRun = process.env.DRY_RUN === 'true' + const version = cycleStartVersion(minorVersion) const branch = releaseBranchName(version) - const title = `release: v${version}` + const title = `release: v${minorVersion}` const body = buildPrBody(version) if (dryRun) { diff --git a/ci/src/release/validate-version.ts b/ci/src/release/validate-version.ts index 15b32368bb..1046b93a30 100644 --- a/ci/src/release/validate-version.ts +++ b/ci/src/release/validate-version.ts @@ -1,10 +1,10 @@ -import { validateVersion } from './version' +import { validateMinorVersion } from './version' -const version = process.env.VERSION ?? '' +const minorVersion = process.env.MINOR_VERSION ?? '' -if (!validateVersion(version)) { - console.error(`Invalid version: "${version}". Expected format: 1.4.0 or 1.4.0-rc.1`) +if (!validateMinorVersion(minorVersion)) { + console.error(`Invalid minor version: "${minorVersion}". Expected format: 1.4`) process.exit(1) } -console.log(`Version "${version}" is valid`) +console.log(`Minor version "${minorVersion}" is valid`) diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index 8a21038337..e3d82bbcb1 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -1,5 +1,7 @@ import { validateVersion, + validateMinorVersion, + cycleStartVersion, releaseBranchName, incrementRc, promoteToStable, @@ -31,6 +33,31 @@ describe('validateVersion', () => { }) }) +describe('validateMinorVersion', () => { + it.each([ + ['1.4', true], + ['6.0', true], + ['10.12', true], + ['1.4.0', false], + ['1.4.0-rc.1', false], + ['1', false], + ['v1.4', false], + ['', false], + ])('validateMinorVersion(%s) → %s', (v, expected) => { + expect(validateMinorVersion(v)).toBe(expected) + }) +}) + +describe('cycleStartVersion', () => { + it.each([ + ['1.4', '1.4.0-rc.1'], + ['6.0', '6.0.0-rc.1'], + ['10.12', '10.12.0-rc.1'], + ])('cycleStartVersion(%s) → %s', (minorVersion, expected) => { + expect(cycleStartVersion(minorVersion)).toBe(expected) + }) +}) + describe('releaseBranchName', () => { it.each([ ['1.4.0-rc.1', 'release/v1.4'], diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index adf5d80a50..8ef400285b 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -1,11 +1,20 @@ import semver from 'semver' const VERSION_RE = /^\d+\.\d+\.\d+(-rc\.\d+)?$/ +const MINOR_VERSION_RE = /^\d+\.\d+$/ export function validateVersion(version: string): boolean { return VERSION_RE.test(version) } +export function validateMinorVersion(minorVersion: string): boolean { + return MINOR_VERSION_RE.test(minorVersion) +} + +export function cycleStartVersion(minorVersion: string): string { + return `${minorVersion}.0-rc.1` +} + export function releaseBranchName(version: string): string { const [major, minor] = version.split('.') return `release/v${major}.${minor}` From 913a526c565562442fe591355d0c46f1c9bf50d4 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:15:59 +0200 Subject: [PATCH 09/24] ci: expect version starting from v letter --- ci/src/release/bump-version.ts | 4 ++-- ci/src/release/check-branch-not-exists.ts | 2 +- ci/src/release/open-main-bump-pr.ts | 10 +++++----- ci/src/release/open-release-pr.ts | 6 +++--- ci/src/release/validate-version.ts | 2 +- ci/src/release/version.test.ts | 20 ++++++++++++++++---- ci/src/release/version.ts | 6 +++++- 7 files changed, 33 insertions(+), 17 deletions(-) diff --git a/ci/src/release/bump-version.ts b/ci/src/release/bump-version.ts index fc21f4d2b0..b793396dc7 100644 --- a/ci/src/release/bump-version.ts +++ b/ci/src/release/bump-version.ts @@ -1,12 +1,12 @@ import { execSync } from 'child_process' import path from 'path' -import { cycleStartVersion } from './version' +import { cycleStartVersion, stripV } from './version' const minorVersion = process.env.MINOR_VERSION! const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') const dryRun = process.env.DRY_RUN === 'true' -const version = cycleStartVersion(minorVersion) +const version = cycleStartVersion(stripV(minorVersion)) if (dryRun) { console.log(`[dry-run] Would run: npm version ${version} --no-git-tag-version`) diff --git a/ci/src/release/check-branch-not-exists.ts b/ci/src/release/check-branch-not-exists.ts index 36ca89fc93..b19f1541a0 100644 --- a/ci/src/release/check-branch-not-exists.ts +++ b/ci/src/release/check-branch-not-exists.ts @@ -1,7 +1,7 @@ import { execSync } from 'child_process' const minorVersion = process.env.MINOR_VERSION! -const branch = `release/v${minorVersion}` +const branch = `release/${minorVersion}` try { execSync(`git ls-remote --exit-code origin refs/heads/${branch}`, { stdio: 'pipe' }) diff --git a/ci/src/release/open-main-bump-pr.ts b/ci/src/release/open-main-bump-pr.ts index dbe8d92c68..3da0b34f4d 100644 --- a/ci/src/release/open-main-bump-pr.ts +++ b/ci/src/release/open-main-bump-pr.ts @@ -3,7 +3,7 @@ import { writeFileSync, unlinkSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import path from 'path' -import { nextMainVersion, cycleStartVersion } from './version' +import { nextMainVersion, cycleStartVersion, stripV } from './version' async function main() { const minorVersion = process.env.MINOR_VERSION! @@ -11,10 +11,10 @@ async function main() { const dryRun = process.env.DRY_RUN === 'true' const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - const nextVersion = nextMainVersion(cycleStartVersion(minorVersion)) - const bumpBranch = `chore/bump-main-v${nextVersion}` - const title = `chore: bump main to v${nextVersion} after cutting release/v${minorVersion}` - const body = `Automated version bump after cutting the \`release/v${minorVersion}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` + const nextVersion = nextMainVersion(cycleStartVersion(stripV(minorVersion))) + const bumpBranch = `chore/bump-main-${minorVersion}` + const title = `chore: bump main after cutting release/${minorVersion}` + const body = `Automated version bump after cutting the \`release/${minorVersion}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` if (dryRun) { console.log('[dry-run] Would create main bump PR:') diff --git a/ci/src/release/open-release-pr.ts b/ci/src/release/open-release-pr.ts index 1a228963f8..2dde7aa0f5 100644 --- a/ci/src/release/open-release-pr.ts +++ b/ci/src/release/open-release-pr.ts @@ -2,7 +2,7 @@ import { execSync } from 'child_process' import { writeFileSync, unlinkSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' -import { releaseBranchName, cycleStartVersion } from './version' +import { releaseBranchName, cycleStartVersion, stripV } from './version' export function buildPrBody(version: string): string { const isRc = version.includes('-rc.') @@ -25,9 +25,9 @@ async function main() { const minorVersion = process.env.MINOR_VERSION! const baseBranch = process.env.BASE_BRANCH! const dryRun = process.env.DRY_RUN === 'true' - const version = cycleStartVersion(minorVersion) + const version = cycleStartVersion(stripV(minorVersion)) const branch = releaseBranchName(version) - const title = `release: v${minorVersion}` + const title = `release: ${minorVersion}` const body = buildPrBody(version) if (dryRun) { diff --git a/ci/src/release/validate-version.ts b/ci/src/release/validate-version.ts index 1046b93a30..24520e0c3e 100644 --- a/ci/src/release/validate-version.ts +++ b/ci/src/release/validate-version.ts @@ -3,7 +3,7 @@ import { validateMinorVersion } from './version' const minorVersion = process.env.MINOR_VERSION ?? '' if (!validateMinorVersion(minorVersion)) { - console.error(`Invalid minor version: "${minorVersion}". Expected format: 1.4`) + console.error(`Invalid minor version: "${minorVersion}". Expected format: v1.4`) process.exit(1) } diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index e3d82bbcb1..8d2cc2d22a 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -1,6 +1,7 @@ import { validateVersion, validateMinorVersion, + stripV, cycleStartVersion, releaseBranchName, incrementRc, @@ -35,19 +36,30 @@ describe('validateVersion', () => { describe('validateMinorVersion', () => { it.each([ - ['1.4', true], - ['6.0', true], - ['10.12', true], + ['v1.4', true], + ['v6.0', true], + ['v10.12', true], + ['1.4', false], ['1.4.0', false], ['1.4.0-rc.1', false], ['1', false], - ['v1.4', false], + ['v1.4.0', false], ['', false], ])('validateMinorVersion(%s) → %s', (v, expected) => { expect(validateMinorVersion(v)).toBe(expected) }) }) +describe('stripV', () => { + it.each([ + ['v1.4', '1.4'], + ['v10.12', '10.12'], + ['1.4', '1.4'], + ])('stripV(%s) → %s', (input, expected) => { + expect(stripV(input)).toBe(expected) + }) +}) + describe('cycleStartVersion', () => { it.each([ ['1.4', '1.4.0-rc.1'], diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index 8ef400285b..ae923834bc 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -1,7 +1,7 @@ import semver from 'semver' const VERSION_RE = /^\d+\.\d+\.\d+(-rc\.\d+)?$/ -const MINOR_VERSION_RE = /^\d+\.\d+$/ +const MINOR_VERSION_RE = /^v\d+\.\d+$/ export function validateVersion(version: string): boolean { return VERSION_RE.test(version) @@ -11,6 +11,10 @@ export function validateMinorVersion(minorVersion: string): boolean { return MINOR_VERSION_RE.test(minorVersion) } +export function stripV(minorVersion: string): string { + return minorVersion.replace(/^v/, '') +} + export function cycleStartVersion(minorVersion: string): string { return `${minorVersion}.0-rc.1` } From 852bf138ea9bc436b99004cf9afbd7fc1ae79059 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:16:17 +0200 Subject: [PATCH 10/24] ci: docs --- .github/workflows/cut-release-branch.yml | 6 +- docs/releasing.md | 109 +++++++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 docs/releasing.md diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index 432864a7d4..06a7a66edc 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: minor_version: - description: 'Major.minor version to release (e.g. 1.4)' + description: 'Major.minor version to release, with v prefix (e.g. v1.4)' required: true type: string base_branch: @@ -79,7 +79,7 @@ jobs: - name: Create release branch run: | - BRANCH="release/v${{ inputs.minor_version }}" + BRANCH="release/${{ inputs.minor_version }}" echo "RELEASE_BRANCH=$BRANCH" >> "$GITHUB_ENV" git checkout -b "$BRANCH" @@ -95,7 +95,7 @@ jobs: if: ${{ inputs.dry_run == 'false' }} run: | git add package.json package-lock.json - git commit -m "chore(release): bump to v${{ inputs.minor_version }}.0-rc.1" + git commit -m "chore(release): bump to ${{ inputs.minor_version }}.0-rc.1" git push -u origin "$RELEASE_BRANCH" - name: Open release PR diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000000..2bcd013943 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,109 @@ +# Releasing + +Releases are driven by two `workflow_dispatch` GitHub Actions workflows. Both default to `dry_run: true` — always test with a dry run before writing anything. + +## Prerequisites + +```sh +gh auth login # requires write access to linode/apl-core +``` + +## 1. Cut a release branch + +Creates `release/v`, bumps `package.json` to `.0-rc.1`, opens a release checklist PR, and opens a version-bump PR against `main`. + +```sh +# Dry run (default) — validate inputs, no writes +gh workflow run cut-release-branch.yml \ + -f minor_version=v1.4 \ + -f base_branch=main \ + -f dry_run=true + +# Live run +gh workflow run cut-release-branch.yml \ + -f minor_version=v1.4 \ + -f base_branch=main \ + -f dry_run=false +``` + +**Inputs** + +| Input | Required | Default | Description | +|---|---|---|---| +| `minor_version` | yes | — | Major.minor version with `v` prefix, e.g. `v1.4` — patch and RC suffix are derived automatically | +| `base_branch` | yes | `main` | Branch to cut from | +| `dry_run` | yes | `true` | Skip all writes (git push, PRs) | + +## 2. Release from branch + +Runs the test gate, tags the commit, publishes a GitHub Release, pushes Docker image(s) to Docker Hub, and publishes the Helm chart. + +```sh +# Dry run — validate, build, no writes +gh workflow run release-from-branch.yml \ + -f release_branch=release/v1.4 \ + -f promote_to_stable=false \ + -f dry_run=true + +# Cut an RC release +gh workflow run release-from-branch.yml \ + -f release_branch=release/v1.4 \ + -f promote_to_stable=false \ + -f dry_run=false + +# Promote the current RC to stable +gh workflow run release-from-branch.yml \ + -f release_branch=release/v1.4 \ + -f promote_to_stable=true \ + -f dry_run=false +``` + +**Inputs** + +| Input | Required | Default | Description | +|---|---|---|---| +| `release_branch` | yes | — | Release cycle branch, e.g. `release/v1.4` | +| `promote_to_stable` | yes | `false` | Strip `-rc.N` suffix and cut a stable tag | +| `dry_run` | yes | `true` | Skip all writes (tag, GitHub release, Docker push, Helm publish) | + +**What gets published** + +| Artifact | RC release | Stable release | +|---|---|---| +| Git tag | `v1.4.0-rc.1` | `v1.4.0` | +| GitHub Release | pre-release | release | +| Docker image | `linode/apl-core:v1.4.0-rc.1` | `linode/apl-core:v1.4.0` + `:latest`* | +| Helm chart | published (version contains `-rc.1`) | published | + +\* `:latest` is only pushed when the new stable tag is higher than all existing stable tags. + +After a stable cut the workflow auto-bumps `package.json` on the release branch to `v1.4.1-rc.1` and commits it, ready for patch work. + +## Typical release cycle + +``` +# 1. Start the cycle +gh workflow run cut-release-branch.yml -f minor_version=v1.4 -f dry_run=false + +# 2. Cut successive RCs as needed +gh workflow run release-from-branch.yml -f release_branch=release/v1.4 -f dry_run=false +# (repeat — auto-bumps rc.1 → rc.2 → ... each time) + +# 3. Promote to stable +gh workflow run release-from-branch.yml \ + -f release_branch=release/v1.4 \ + -f promote_to_stable=true \ + -f dry_run=false + +# 4. Cut patch releases from the same branch +gh workflow run release-from-branch.yml -f release_branch=release/v1.4 -f dry_run=false +# (auto-bumps to v1.4.1-rc.1 → v1.4.1 → v1.4.2-rc.1 → ...) +``` + +## Monitoring a run + +```sh +gh run list --workflow=cut-release-branch.yml --limit=5 +gh run list --workflow=release-from-branch.yml --limit=5 +gh run watch # stream logs of the most recent run +``` From 574d915f40f66c88cc3ca99ebe069711c2047bec Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:19:38 +0200 Subject: [PATCH 11/24] ci: set module resulution --- ci/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/tsconfig.json b/ci/tsconfig.json index e511a712b5..e26dd4c1bd 100644 --- a/ci/tsconfig.json +++ b/ci/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "module": "commonjs", - "moduleResolution": "node", + "module": "NodeNext", + "moduleResolution": "nodenext", "resolveJsonModule": true, "skipLibCheck": true, "strict": false, From ff200b6134fc231c1d39e9ba242fc855d7b86b76 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:07:40 +0200 Subject: [PATCH 12/24] refactor: var rename --- .github/workflows/release-from-branch.yml | 5 +++-- ci/src/release/docker-push.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index f89a4ac794..16f11346f4 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -26,6 +26,7 @@ on: env: REPO: linode/apl-core + LOCAL_CACHE_IMAGE: apl-core:ci-build BOT_EMAIL: ${{ vars.BOT_EMAIL }} BOT_USERNAME: ${{ vars.BOT_USERNAME }} DOCKER_USERNAME: ${{ vars.DOCKERHUB_LINODEBOT_USERNAME }} @@ -72,7 +73,7 @@ jobs: load: true context: . build-args: SKIP_TESTS=false - tags: notused + tags: ${{ env.LOCAL_CACHE_IMAGE }} - name: Compute release tag id: compute-tag @@ -135,7 +136,7 @@ jobs: run: npm run release:docker-push env: RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} - CACHE_IMAGE: notused + CACHE_IMAGE: ${{ env.LOCAL_CACHE_IMAGE }} DOCKER_REPO: ${{ env.REPO }} DRY_RUN: ${{ inputs.dry_run }} diff --git a/ci/src/release/docker-push.ts b/ci/src/release/docker-push.ts index 2afa476b9e..14e2b1ef3e 100644 --- a/ci/src/release/docker-push.ts +++ b/ci/src/release/docker-push.ts @@ -3,7 +3,7 @@ import { isHighestStableTag } from './version' async function main() { const tag = process.env.RELEASE_TAG! - const cacheImage = process.env.CACHE_IMAGE! + const cacheImage = process.env.LOCAL_CACHE_IMAGE! const dockerRepo = process.env.DOCKER_REPO ?? 'linode/apl-core' const dryRun = process.env.DRY_RUN === 'true' const isRc = tag.includes('-rc.') From 2c0e20be5b1364e8ad45eb6085502013278447dc Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:09:10 +0200 Subject: [PATCH 13/24] refactor: unncessary env --- .github/workflows/release-from-branch.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index 16f11346f4..e034e699b7 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -142,9 +142,6 @@ jobs: - name: Prepare Helm chart if: ${{ inputs.dry_run == 'false' }} - env: - BOT_EMAIL: ${{ vars.BOT_EMAIL }} - BOT_USERNAME: ${{ vars.BOT_USERNAME }} run: ci/scripts/prepare_chart_for_release.sh # chart-releaser-action has no RC awareness — it publishes whatever version From 3d30b3c95dceeabca3a17047eda73ab58fb527e8 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:32:18 +0200 Subject: [PATCH 14/24] chore(release): bump to v6.0.0-rc.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16c78bd2f6..943f90c8ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "apl-core", - "version": "6.0.0-rc.0", + "version": "6.0.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "apl-core", - "version": "6.0.0-rc.0", + "version": "6.0.0-rc.2", "license": "Apache-2.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "15.3.5", diff --git a/package.json b/package.json index 8c8ee13328..f5311ef0bd 100644 --- a/package.json +++ b/package.json @@ -173,5 +173,5 @@ } }, "type": "commonjs", - "version": "6.0.0-rc.0" + "version": "6.0.0-rc.2" } From 18dcc235f3dca6e914c2ec6aeeacec6b84688478 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:41:44 +0200 Subject: [PATCH 15/24] ci: validate versions.yaml --- .github/workflows/release-from-branch.yml | 7 +++ ci/package.json | 1 + ci/src/release/check-versions-yaml.test.ts | 66 ++++++++++++++++++++++ ci/src/release/check-versions-yaml.ts | 50 ++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 ci/src/release/check-versions-yaml.test.ts create mode 100644 ci/src/release/check-versions-yaml.ts diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index e034e699b7..768db9437a 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -63,6 +63,13 @@ jobs: RELEASE_BRANCH: ${{ inputs.release_branch }} REPO_ROOT: ${{ github.workspace }} + - name: Check versions.yaml entries are semver-compatible + working-directory: ci + run: npm run release:check-versions-yaml + env: + REPO_ROOT: ${{ github.workspace }} + STABLE: ${{ inputs.promote_to_stable }} + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 diff --git a/ci/package.json b/ci/package.json index 8caeb43224..096c59264d 100644 --- a/ci/package.json +++ b/ci/package.json @@ -43,6 +43,7 @@ "release:open-release-pr": "tsx src/release/open-release-pr.ts", "release:open-main-bump-pr": "tsx src/release/open-main-bump-pr.ts", "release:check-version-matches-branch": "tsx src/release/check-version-matches-branch.ts", + "release:check-versions-yaml": "tsx src/release/check-versions-yaml.ts", "release:compute-tag": "tsx src/release/compute-tag.ts", "release:check-tag-not-exists": "tsx src/release/check-tag-not-exists.ts", "release:auto-bump": "tsx src/release/auto-bump.ts", diff --git a/ci/src/release/check-versions-yaml.test.ts b/ci/src/release/check-versions-yaml.test.ts new file mode 100644 index 0000000000..be00d795c1 --- /dev/null +++ b/ci/src/release/check-versions-yaml.test.ts @@ -0,0 +1,66 @@ +import { parseVersionsYaml, findInvalidVersions, findRcVersions } from './check-versions-yaml' + +describe('parseVersionsYaml', () => { + it('parses all key-value pairs', () => { + const content = `api: 1.4.0\nconsole: 1.4.0-rc.1\n` + expect(parseVersionsYaml(content)).toEqual({ api: '1.4.0', console: '1.4.0-rc.1' }) + }) + + it('returns empty object for empty content', () => { + expect(parseVersionsYaml('')).toEqual({}) + }) +}) + +describe('findInvalidVersions', () => { + it('returns entries whose values are not semver', () => { + const versions = { api: 'main', console: '1.4.0', tools: 'latest' } + expect(findInvalidVersions(versions)).toEqual([ + { key: 'api', value: 'main' }, + { key: 'tools', value: 'latest' }, + ]) + }) + + it('returns empty array when all values are valid semver', () => { + const versions = { api: '1.4.0', console: '1.4.0-rc.1', tasks: '2.1.0' } + expect(findInvalidVersions(versions)).toEqual([]) + }) + + it('accepts rc versions as valid', () => { + const versions = { api: '6.0.0-rc.2' } + expect(findInvalidVersions(versions)).toEqual([]) + }) + + it('rejects branch names and non-semver strings', () => { + const versions = { api: 'release/v1.4', console: 'v1.4.0', tasks: 'main' } + expect(findInvalidVersions(versions)).toEqual([ + { key: 'api', value: 'release/v1.4' }, + { key: 'console', value: 'v1.4.0' }, + { key: 'tasks', value: 'main' }, + ]) + }) + + it('reports all invalid entries, not just the first', () => { + const versions = { api: 'main', console: 'main', tools: 'main' } + expect(findInvalidVersions(versions)).toHaveLength(3) + }) +}) + +describe('findRcVersions', () => { + it('returns entries whose values contain an rc suffix', () => { + const versions = { api: '1.4.0-rc.1', console: '1.4.0', tools: '2.0.0-rc.3' } + expect(findRcVersions(versions)).toEqual([ + { key: 'api', value: '1.4.0-rc.1' }, + { key: 'tools', value: '2.0.0-rc.3' }, + ]) + }) + + it('returns empty array when no rc versions are present', () => { + const versions = { api: '1.4.0', console: '2.0.0', tasks: '6.0.0' } + expect(findRcVersions(versions)).toEqual([]) + }) + + it('reports all rc entries, not just the first', () => { + const versions = { api: '1.0.0-rc.1', console: '2.0.0-rc.2', tools: '3.0.0-rc.3' } + expect(findRcVersions(versions)).toHaveLength(3) + }) +}) diff --git a/ci/src/release/check-versions-yaml.ts b/ci/src/release/check-versions-yaml.ts new file mode 100644 index 0000000000..535356a485 --- /dev/null +++ b/ci/src/release/check-versions-yaml.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'fs' +import path from 'path' +import { load } from 'js-yaml' +import { validateVersion } from './version' + +export function parseVersionsYaml(content: string): Record { + return (load(content) as Record) ?? {} +} + +export function findInvalidVersions(versions: Record): Array<{ key: string; value: string }> { + return Object.entries(versions) + .filter(([, value]) => !validateVersion(value)) + .map(([key, value]) => ({ key, value })) +} + +export function findRcVersions(versions: Record): Array<{ key: string; value: string }> { + return Object.entries(versions) + .filter(([, value]) => value.includes('-rc.')) + .map(([key, value]) => ({ key, value })) +} + +if (require.main === module) { + const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + const stable = process.env.STABLE === 'true' + const content = readFileSync(path.join(repoRoot, 'versions.yaml'), 'utf8') + const versions = parseVersionsYaml(content) + + const invalid = findInvalidVersions(versions) + if (invalid.length > 0) { + console.error(`versions.yaml contains non-semver values:`) + for (const { key, value } of invalid) { + console.error(` ${key}: "${value}"`) + } + console.error(`All values must be semver (e.g. 1.4.0 or 1.4.0-rc.1)`) + process.exit(1) + } + + if (stable) { + const rcs = findRcVersions(versions) + if (rcs.length > 0) { + console.error(`versions.yaml contains rc versions — stable release requires all versions to be stable:`) + for (const { key, value } of rcs) { + console.error(` ${key}: "${value}"`) + } + process.exit(1) + } + } + + console.log(`versions.yaml: all ${Object.keys(versions).length} entries are semver-compatible`) +} From 12e89269670fda2127e8df0a28e8068255e24c16 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:10:21 +0200 Subject: [PATCH 16/24] refactor: set release branch in gh actions --- .github/workflows/cut-release-branch.yml | 26 +++++++++----------- .github/workflows/release-from-branch.yml | 29 ++++++++++++----------- ci/package.json | 5 ++++ ci/src/release/check-branch-not-exists.ts | 3 +-- ci/src/release/open-release-pr.ts | 18 +++++++------- 5 files changed, 42 insertions(+), 39 deletions(-) diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index 06a7a66edc..878ea0b148 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -22,6 +22,7 @@ on: - 'false' env: + RELEASE_BRANCH: release/${{ inputs.minor_version }} BOT_EMAIL: ${{ vars.BOT_EMAIL }} BOT_USERNAME: ${{ vars.BOT_USERNAME }} @@ -58,8 +59,6 @@ jobs: - name: Check release branch does not already exist working-directory: ci run: npm run release:check-branch-not-exists - env: - MINOR_VERSION: ${{ inputs.minor_version }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -73,15 +72,12 @@ jobs: build-args: SKIP_TESTS=false - name: Configure git bot identity - run: | - git config --global user.email "$BOT_EMAIL" - git config --global user.name "$BOT_USERNAME" + working-directory: ci + run: npm run release:configure-git - name: Create release branch - run: | - BRANCH="release/${{ inputs.minor_version }}" - echo "RELEASE_BRANCH=$BRANCH" >> "$GITHUB_ENV" - git checkout -b "$BRANCH" + working-directory: ci + run: npm run release:create-release-branch - name: Bump package.json to target version working-directory: ci @@ -92,18 +88,18 @@ jobs: DRY_RUN: ${{ inputs.dry_run }} - name: Commit and push release branch - if: ${{ inputs.dry_run == 'false' }} - run: | - git add package.json package-lock.json - git commit -m "chore(release): bump to ${{ inputs.minor_version }}.0-rc.1" - git push -u origin "$RELEASE_BRANCH" + working-directory: ci + run: npm run release:commit-release-branch + env: + REPO_ROOT: ${{ github.workspace }} + DRY_RUN: ${{ inputs.dry_run }} - name: Open release PR working-directory: ci run: npm run release:open-release-pr env: - MINOR_VERSION: ${{ inputs.minor_version }} BASE_BRANCH: ${{ inputs.base_branch }} + REPO_ROOT: ${{ github.workspace }} DRY_RUN: ${{ inputs.dry_run }} - name: Open main version bump PR diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index 768db9437a..e9cb3314f6 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -25,6 +25,7 @@ on: - 'false' env: + RELEASE_BRANCH: ${{ inputs.release_branch }} REPO: linode/apl-core LOCAL_CACHE_IMAGE: apl-core:ci-build BOT_EMAIL: ${{ vars.BOT_EMAIL }} @@ -60,7 +61,6 @@ jobs: working-directory: ci run: npm run release:check-version-matches-branch env: - RELEASE_BRANCH: ${{ inputs.release_branch }} REPO_ROOT: ${{ github.workspace }} - name: Check versions.yaml entries are semver-compatible @@ -97,23 +97,24 @@ jobs: RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} - name: Configure git bot identity - run: | - git config --global user.email "$BOT_EMAIL" - git config --global user.name "$BOT_USERNAME" + working-directory: ci + run: npm run release:configure-git - name: Promote package.json to stable (if applicable) - if: ${{ inputs.promote_to_stable == 'true' && inputs.dry_run == 'false' }} - run: | - npm version "${{ steps.compute-tag.outputs.tag }}" --no-git-tag-version - git add package.json package-lock.json - git commit -m "chore(release): promote to ${{ steps.compute-tag.outputs.tag }}" - git push + working-directory: ci + run: npm run release:promote-package-version + env: + RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} + PROMOTE_TO_STABLE: ${{ inputs.promote_to_stable }} + REPO_ROOT: ${{ github.workspace }} + DRY_RUN: ${{ inputs.dry_run }} - name: Create and push git tag - if: ${{ inputs.dry_run == 'false' }} - run: | - git tag -a "${{ steps.compute-tag.outputs.tag }}" -m "Release ${{ steps.compute-tag.outputs.tag }}" - git push --follow-tags + working-directory: ci + run: npm run release:tag-release + env: + RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} + DRY_RUN: ${{ inputs.dry_run }} - name: Auto-bump version for next iteration working-directory: ci diff --git a/ci/package.json b/ci/package.json index 096c59264d..ac05c768f0 100644 --- a/ci/package.json +++ b/ci/package.json @@ -38,14 +38,19 @@ "check-schema-versions": "node src/check-schema-versions.mjs", "test": "jest", "release:validate-version": "tsx src/release/validate-version.ts", + "release:configure-git": "tsx src/release/configure-git.ts", "release:check-branch-not-exists": "tsx src/release/check-branch-not-exists.ts", + "release:create-release-branch": "tsx src/release/create-release-branch.ts", "release:bump-version": "tsx src/release/bump-version.ts", + "release:commit-release-branch": "tsx src/release/commit-release-branch.ts", "release:open-release-pr": "tsx src/release/open-release-pr.ts", "release:open-main-bump-pr": "tsx src/release/open-main-bump-pr.ts", "release:check-version-matches-branch": "tsx src/release/check-version-matches-branch.ts", "release:check-versions-yaml": "tsx src/release/check-versions-yaml.ts", "release:compute-tag": "tsx src/release/compute-tag.ts", "release:check-tag-not-exists": "tsx src/release/check-tag-not-exists.ts", + "release:promote-package-version": "tsx src/release/promote-package-version.ts", + "release:tag-release": "tsx src/release/tag-release.ts", "release:auto-bump": "tsx src/release/auto-bump.ts", "release:create-github-release": "tsx src/release/create-github-release.ts", "release:docker-push": "tsx src/release/docker-push.ts" diff --git a/ci/src/release/check-branch-not-exists.ts b/ci/src/release/check-branch-not-exists.ts index b19f1541a0..84e77b93e2 100644 --- a/ci/src/release/check-branch-not-exists.ts +++ b/ci/src/release/check-branch-not-exists.ts @@ -1,7 +1,6 @@ import { execSync } from 'child_process' -const minorVersion = process.env.MINOR_VERSION! -const branch = `release/${minorVersion}` +const branch = process.env.RELEASE_BRANCH! try { execSync(`git ls-remote --exit-code origin refs/heads/${branch}`, { stdio: 'pipe' }) diff --git a/ci/src/release/open-release-pr.ts b/ci/src/release/open-release-pr.ts index 2dde7aa0f5..6a2174c5a2 100644 --- a/ci/src/release/open-release-pr.ts +++ b/ci/src/release/open-release-pr.ts @@ -1,8 +1,8 @@ import { execSync } from 'child_process' -import { writeFileSync, unlinkSync } from 'fs' +import { readFileSync, writeFileSync, unlinkSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' -import { releaseBranchName, cycleStartVersion, stripV } from './version' +import path from 'path' export function buildPrBody(version: string): string { const isRc = version.includes('-rc.') @@ -22,18 +22,20 @@ export function buildPrBody(version: string): string { } async function main() { - const minorVersion = process.env.MINOR_VERSION! + const releaseBranch = process.env.RELEASE_BRANCH! const baseBranch = process.env.BASE_BRANCH! const dryRun = process.env.DRY_RUN === 'true' - const version = cycleStartVersion(stripV(minorVersion)) - const branch = releaseBranchName(version) - const title = `release: ${minorVersion}` + const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + + const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) + const version: string = pkg.version + const title = `release: ${releaseBranch.replace('release/', '')}` const body = buildPrBody(version) if (dryRun) { console.log('[dry-run] Would open PR:') console.log(` title: ${title}`) - console.log(` head: ${branch} → ${baseBranch}`) + console.log(` head: ${releaseBranch} → ${baseBranch}`) console.log(` body:\n${body}`) return } @@ -41,7 +43,7 @@ async function main() { const bodyFile = join(tmpdir(), `pr-body-${Date.now()}.md`) writeFileSync(bodyFile, body) try { - execSync(`gh pr create --title "${title}" --body-file "${bodyFile}" --base "${baseBranch}" --head "${branch}"`, { + execSync(`gh pr create --title "${title}" --body-file "${bodyFile}" --base "${baseBranch}" --head "${releaseBranch}"`, { stdio: 'inherit', }) } finally { From 5928c6be9848e1e71ed52ea192a25fe74609cf97 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:11:07 +0200 Subject: [PATCH 17/24] refactor: oermifrm git operations as typescript --- ci/src/release/commit-release-branch.ts | 20 ++++++++++++++++++++ ci/src/release/configure-git.ts | 8 ++++++++ ci/src/release/create-release-branch.ts | 6 ++++++ ci/src/release/promote-package-version.ts | 23 +++++++++++++++++++++++ ci/src/release/tag-release.ts | 13 +++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 ci/src/release/commit-release-branch.ts create mode 100644 ci/src/release/configure-git.ts create mode 100644 ci/src/release/create-release-branch.ts create mode 100644 ci/src/release/promote-package-version.ts create mode 100644 ci/src/release/tag-release.ts diff --git a/ci/src/release/commit-release-branch.ts b/ci/src/release/commit-release-branch.ts new file mode 100644 index 0000000000..c905c496ca --- /dev/null +++ b/ci/src/release/commit-release-branch.ts @@ -0,0 +1,20 @@ +import { execSync } from 'child_process' +import { readFileSync } from 'fs' +import path from 'path' + +const branch = process.env.RELEASE_BRANCH! +const dryRun = process.env.DRY_RUN === 'true' +const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + +const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) +const version: string = pkg.version + +if (dryRun) { + console.log(`[dry-run] Would commit and push ${branch} with version ${version}`) + process.exit(0) +} + +execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git commit -m "chore(release): bump to v${version}"`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git push -u origin "${branch}"`, { cwd: repoRoot, stdio: 'inherit' }) +console.log(`Committed and pushed ${branch}`) diff --git a/ci/src/release/configure-git.ts b/ci/src/release/configure-git.ts new file mode 100644 index 0000000000..b4ceacc2cf --- /dev/null +++ b/ci/src/release/configure-git.ts @@ -0,0 +1,8 @@ +import { execSync } from 'child_process' + +const email = process.env.BOT_EMAIL! +const username = process.env.BOT_USERNAME! + +execSync(`git config --global user.email "${email}"`, { stdio: 'inherit' }) +execSync(`git config --global user.name "${username}"`, { stdio: 'inherit' }) +console.log(`Git identity configured: ${username} <${email}>`) diff --git a/ci/src/release/create-release-branch.ts b/ci/src/release/create-release-branch.ts new file mode 100644 index 0000000000..acc3b706a0 --- /dev/null +++ b/ci/src/release/create-release-branch.ts @@ -0,0 +1,6 @@ +import { execSync } from 'child_process' + +const branch = process.env.RELEASE_BRANCH! + +execSync(`git checkout -b "${branch}"`, { stdio: 'inherit' }) +console.log(`Created branch: ${branch}`) diff --git a/ci/src/release/promote-package-version.ts b/ci/src/release/promote-package-version.ts new file mode 100644 index 0000000000..b31df981e8 --- /dev/null +++ b/ci/src/release/promote-package-version.ts @@ -0,0 +1,23 @@ +import { execSync } from 'child_process' +import path from 'path' + +const releaseTag = process.env.RELEASE_TAG! +const promoteToStable = process.env.PROMOTE_TO_STABLE === 'true' +const dryRun = process.env.DRY_RUN === 'true' +const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') + +if (!promoteToStable) { + console.log('Not promoting to stable — skipping') + process.exit(0) +} + +if (dryRun) { + console.log(`[dry-run] Would promote package.json to ${releaseTag} and push`) + process.exit(0) +} + +execSync(`npm version ${releaseTag} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git commit -m "chore(release): promote to ${releaseTag}"`, { cwd: repoRoot, stdio: 'inherit' }) +execSync(`git push`, { cwd: repoRoot, stdio: 'inherit' }) +console.log(`Promoted package.json to ${releaseTag} and pushed`) diff --git a/ci/src/release/tag-release.ts b/ci/src/release/tag-release.ts new file mode 100644 index 0000000000..563d9ac268 --- /dev/null +++ b/ci/src/release/tag-release.ts @@ -0,0 +1,13 @@ +import { execSync } from 'child_process' + +const releaseTag = process.env.RELEASE_TAG! +const dryRun = process.env.DRY_RUN === 'true' + +if (dryRun) { + console.log(`[dry-run] Would create and push tag ${releaseTag}`) + process.exit(0) +} + +execSync(`git tag -a "${releaseTag}" -m "Release ${releaseTag}"`, { stdio: 'inherit' }) +execSync(`git push --follow-tags`, { stdio: 'inherit' }) +console.log(`Tagged and pushed: ${releaseTag}`) From 26297646bfa7be7eafd2d965e9194fe2b3b33bd9 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:13:36 +0200 Subject: [PATCH 18/24] refactor: branch name --- .github/workflows/cut-release-branch.yml | 2 +- .../release/check-version-matches-branch.ts | 2 +- ci/src/release/check-versions-yaml.test.ts | 6 +-- ci/src/release/open-main-bump-pr.ts | 11 ++--- ci/src/release/open-release-pr.ts | 7 ++- ci/src/release/version.test.ts | 46 +++++++++---------- ci/src/release/version.ts | 2 +- 7 files changed, 37 insertions(+), 39 deletions(-) diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index 878ea0b148..5c8d4ee2fc 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -22,7 +22,7 @@ on: - 'false' env: - RELEASE_BRANCH: release/${{ inputs.minor_version }} + RELEASE_BRANCH: releases/${{ inputs.minor_version }} BOT_EMAIL: ${{ vars.BOT_EMAIL }} BOT_USERNAME: ${{ vars.BOT_USERNAME }} diff --git a/ci/src/release/check-version-matches-branch.ts b/ci/src/release/check-version-matches-branch.ts index 4f99c935d4..5eeaacd3bb 100644 --- a/ci/src/release/check-version-matches-branch.ts +++ b/ci/src/release/check-version-matches-branch.ts @@ -11,7 +11,7 @@ const version: string = pkg.version if (!versionMatchesBranch(version, releaseBranch)) { console.error( `Version mismatch: package.json is at "${version}" but branch is "${releaseBranch}". ` + - `Expected major.minor to be "${releaseBranch.replace('release/v', '')}".`, + `Expected major.minor to be "${releaseBranch.replace('releases/v', '')}".`, ) process.exit(1) } diff --git a/ci/src/release/check-versions-yaml.test.ts b/ci/src/release/check-versions-yaml.test.ts index be00d795c1..d7008f58e9 100644 --- a/ci/src/release/check-versions-yaml.test.ts +++ b/ci/src/release/check-versions-yaml.test.ts @@ -1,4 +1,4 @@ -import { parseVersionsYaml, findInvalidVersions, findRcVersions } from './check-versions-yaml' +import { findInvalidVersions, findRcVersions, parseVersionsYaml } from './check-versions-yaml' describe('parseVersionsYaml', () => { it('parses all key-value pairs', () => { @@ -31,9 +31,9 @@ describe('findInvalidVersions', () => { }) it('rejects branch names and non-semver strings', () => { - const versions = { api: 'release/v1.4', console: 'v1.4.0', tasks: 'main' } + const versions = { api: 'releases/v1.4', console: 'v1.4.0', tasks: 'main' } expect(findInvalidVersions(versions)).toEqual([ - { key: 'api', value: 'release/v1.4' }, + { key: 'api', value: 'releases/v1.4' }, { key: 'console', value: 'v1.4.0' }, { key: 'tasks', value: 'main' }, ]) diff --git a/ci/src/release/open-main-bump-pr.ts b/ci/src/release/open-main-bump-pr.ts index 3da0b34f4d..73d5cf05bb 100644 --- a/ci/src/release/open-main-bump-pr.ts +++ b/ci/src/release/open-main-bump-pr.ts @@ -1,9 +1,8 @@ import { execSync } from 'child_process' -import { writeFileSync, unlinkSync } from 'fs' +import { unlinkSync, writeFileSync } from 'fs' import { tmpdir } from 'os' -import { join } from 'path' -import path from 'path' -import { nextMainVersion, cycleStartVersion, stripV } from './version' +import path, { join } from 'path' +import { cycleStartVersion, nextMainVersion, stripV } from './version' async function main() { const minorVersion = process.env.MINOR_VERSION! @@ -13,8 +12,8 @@ async function main() { const nextVersion = nextMainVersion(cycleStartVersion(stripV(minorVersion))) const bumpBranch = `chore/bump-main-${minorVersion}` - const title = `chore: bump main after cutting release/${minorVersion}` - const body = `Automated version bump after cutting the \`release/${minorVersion}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` + const title = `chore: bump main after cutting releases/${minorVersion}` + const body = `Automated version bump after cutting the \`releases/${minorVersion}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` if (dryRun) { console.log('[dry-run] Would create main bump PR:') diff --git a/ci/src/release/open-release-pr.ts b/ci/src/release/open-release-pr.ts index 6a2174c5a2..647f0a4b09 100644 --- a/ci/src/release/open-release-pr.ts +++ b/ci/src/release/open-release-pr.ts @@ -1,8 +1,7 @@ import { execSync } from 'child_process' -import { readFileSync, writeFileSync, unlinkSync } from 'fs' +import { readFileSync, unlinkSync, writeFileSync } from 'fs' import { tmpdir } from 'os' -import { join } from 'path' -import path from 'path' +import path, { join } from 'path' export function buildPrBody(version: string): string { const isRc = version.includes('-rc.') @@ -29,7 +28,7 @@ async function main() { const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) const version: string = pkg.version - const title = `release: ${releaseBranch.replace('release/', '')}` + const title = `release: ${releaseBranch.replace('releases/', '')}` const body = buildPrBody(version) if (dryRun) { diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index 8d2cc2d22a..c7b131a5af 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -1,18 +1,18 @@ import { - validateVersion, - validateMinorVersion, - stripV, - cycleStartVersion, - releaseBranchName, - incrementRc, - promoteToStable, - nextPatchRc, - nextMainVersion, - versionMatchesBranch, - previousStableTag, - isHighestStableTag, - previousRcTag, - previousStableTagBefore, + cycleStartVersion, + incrementRc, + isHighestStableTag, + nextMainVersion, + nextPatchRc, + previousRcTag, + previousStableTag, + previousStableTagBefore, + promoteToStable, + releaseBranchName, + stripV, + validateMinorVersion, + validateVersion, + versionMatchesBranch, } from './version' describe('validateVersion', () => { @@ -72,10 +72,10 @@ describe('cycleStartVersion', () => { describe('releaseBranchName', () => { it.each([ - ['1.4.0-rc.1', 'release/v1.4'], - ['1.4.0', 'release/v1.4'], - ['6.0.0-rc.0', 'release/v6.0'], - ['1.4.1', 'release/v1.4'], + ['1.4.0-rc.1', 'releases/v1.4'], + ['1.4.0', 'releases/v1.4'], + ['6.0.0-rc.0', 'releases/v6.0'], + ['1.4.1', 'releases/v1.4'], ])('releaseBranchName(%s) → %s', (version, expected) => { expect(releaseBranchName(version)).toBe(expected) }) @@ -123,11 +123,11 @@ describe('nextMainVersion', () => { describe('versionMatchesBranch', () => { it.each([ - ['1.4.0-rc.2', 'release/v1.4', true], - ['1.4.0', 'release/v1.4', true], - ['1.4.1-rc.1', 'release/v1.4', true], - ['1.5.0-rc.1', 'release/v1.4', false], - ['1.4.0-rc.1', 'release/v1.5', false], + ['1.4.0-rc.2', 'releases/v1.4', true], + ['1.4.0', 'releases/v1.4', true], + ['1.4.1-rc.1', 'releases/v1.4', true], + ['1.5.0-rc.1', 'releases/v1.4', false], + ['1.4.0-rc.1', 'releases/v1.5', false], ])('versionMatchesBranch(%s, %s) → %s', (version, branch, expected) => { expect(versionMatchesBranch(version, branch)).toBe(expected) }) diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index ae923834bc..18f0bd818a 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -21,7 +21,7 @@ export function cycleStartVersion(minorVersion: string): string { export function releaseBranchName(version: string): string { const [major, minor] = version.split('.') - return `release/v${major}.${minor}` + return `releases/v${major}.${minor}` } export function incrementRc(version: string): string { From 10a87e09960ce46cd4bf221dad0695f44b5faa51 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:40:29 +0200 Subject: [PATCH 19/24] ci: workflow updates --- .github/workflows/cut-release-branch.yml | 14 +-- .github/workflows/integration.yml | 3 +- .github/workflows/main.yml | 130 +-------------------- .github/workflows/release-from-branch.yml | 15 +-- ci/package-lock.json | 27 ++++- ci/scripts/prepare_chart_for_release.sh | 13 --- ci/src/release/check-versions-yaml.test.ts | 93 ++++++++++++++- ci/src/release/check-versions-yaml.ts | 90 +++++++++++++- ci/src/release/create-github-release.ts | 3 +- 9 files changed, 220 insertions(+), 168 deletions(-) diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index 5c8d4ee2fc..b2a9565094 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -94,13 +94,13 @@ jobs: REPO_ROOT: ${{ github.workspace }} DRY_RUN: ${{ inputs.dry_run }} - - name: Open release PR - working-directory: ci - run: npm run release:open-release-pr - env: - BASE_BRANCH: ${{ inputs.base_branch }} - REPO_ROOT: ${{ github.workspace }} - DRY_RUN: ${{ inputs.dry_run }} + # - name: Open release PR + # working-directory: ci + # run: npm run release:open-release-pr + # env: + # BASE_BRANCH: ${{ inputs.base_branch }} + # REPO_ROOT: ${{ github.workspace }} + # DRY_RUN: ${{ inputs.dry_run }} - name: Open main version bump PR working-directory: ci diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0c8fc517db..24bab5d3da 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -52,7 +52,8 @@ on: type: choice options: - 'v1.31.9+lke7' - - 'v1.32.9+lke2' + - 'v1.32.9+lke4' + - 'v1.33.6+lke7' - '1.33' - '1.34' - '1.35' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2279e1ca8a..f5e89c235c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,8 +1,8 @@ name: Build and publish Docker on: push: - branches: - - '**' + branches-ignore: + - 'releases/**' tags-ignore: - '*' workflow_dispatch: ~ @@ -90,129 +90,3 @@ jobs: DEV_CLUSTER_ID: ${{ secrets.DEV_CLUSTER_ID }} LKE_CP_ACL_IPV4: ${{ secrets.LKE_CP_ACL_IPV4 }} run: ci/scripts/trigger_dev.sh - - release: - needs: push-to-docker - if: always() && (startsWith(github.ref, 'refs/heads/releases/') || startsWith(github.ref, 'refs/heads/main')) && startsWith(github.event.head_commit.message, 'chore(release)') && !github.event.act - runs-on: ubuntu-22.04 - env: - COMMIT_MSG: ${{ github.event.head_commit.message }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Set env - run: | - git config --global user.email $BOT_EMAIL - git config --global user.name $BOT_USERNAME - - name: Create and push git tag - id: git_tag - run: | - TAG=${GITHUB_REF##*/} - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD - docker pull $REPO:$TAG - docker tag $REPO:$TAG $REPO:latest - docker push $REPO:latest - release_tag=v$(jq -r '.version' < package.json) - echo tag=$release_tag >> $GITHUB_OUTPUT - echo "Releasing $REPO:$release_tag" - docker tag $REPO:$TAG $REPO:$release_tag - docker push $REPO:$release_tag - docker login -u $BOT_USERNAME -p '${{ secrets.BOT_TOKEN }}' ghcr.io - docker tag $REPO:$TAG $CACHE_REGISTRY/$CACHE_REPO:$release_tag - docker push $CACHE_REGISTRY/$CACHE_REPO:$release_tag - echo "machine github.com login ${{ env.BOT_USERNAME }} password ${{ secrets.BOT_TOKEN }}" > ~/.netrc - git tag -am "$COMMIT_MSG" $release_tag && git push --follow-tags - # Cut the CHANGELOG.md file up to the second occurence of the line starting with `## [` (meaning two #, a space,a square bracket). - # This way we always get the changes of the latest patch (if any) and the latest minor. - awk '/^## \[/{count++; if(count==2) exit} {print}' CHANGELOG.md > NEW_CHANGELOG.md - - name: Create GitHub release - uses: ncipollo/release-action@v1.21.0 - env: - token: ${{ secrets.GITHUB_TOKEN }} - with: - tag: ${{ steps.git_tag.outputs.tag }} - name: Release ${{ steps.git_tag.outputs.tag }} - bodyFile: 'NEW_CHANGELOG.md' - generateReleaseNotes: true - chart-release: - needs: release - if: always() && contains(needs.release.result, 'success') && !github.event.act - runs-on: ubuntu-22.04 - container: - image: linode/apl-tools:v2.10.6 - options: --user 0 # See https://docs.github.com/en/actions/sharing-automations/creating-actions/dockerfile-support-for-github-actions#user - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Prepare chart - id: prepare_chart - run: | - # Install and update helm repo - helm repo add apl https://linode.github.io/apl-core - helm repo update - - # Retrieve the app version from package.json - app_version=$(jq -r '.version' package.json) - if [ -z "$app_version" ]; then - echo "Error: Could not retrieve app version from package.json" - exit 1 - fi - - # Extract major and minor from the app version - new_app_major=$(echo "$app_version" | cut -d '.' -f 1) - new_app_minor=$(echo "$app_version" | cut -d '.' -f 2) - - # Get existing helm charts in the registry - helm_output=$(helm search repo otomi -l -o json) - - # Use jq to parse the output and find the latest version for the given $new_app_major.$new_app_minor app version - existing_version=$(echo "$helm_output" | jq -r --arg major "$new_app_major" --arg minor "$new_app_minor" ' - map(select(.app_version | startswith("v\($major).\($minor)"))) | - max_by(.version | split(".") | map(tonumber)) | - .version' - ) - - # Update Chart.yaml and values.yaml with the new app version - sed -i "s/0.0.0-chart-version/$app_version/g" chart/apl/Chart.yaml - sed -i "s/APP_VERSION_PLACEHOLDER/v$app_version/g" chart/apl/Chart.yaml - - echo "Chart and values files updated successfully with version $app_version" - - # Copy readme from repo into the charts and add tpl/chart-values.md - cp README.md chart/apl/ - printf "\n\n" >>chart/apl/README.md - cat tpl/chart-values.md >>chart/apl/README.md - - # Generate schema - npx js-yaml values-schema.yaml > chart/apl/values.schema.json - - # Set the global id for git as it seems needed by the next step when a custom image is used - git config --global user.email ${{ env.BOT_EMAIL }} - git config --global user.name ${{ env.BOT_USERNAME }} - #TODO: Use the same user id on the container as in the runner to avoid the "dubious ownership" error - - name: Mark repository as safe for Git - run: git config --global --add safe.directory /__w/apl-core/apl-core - - name: Create and publish otomi chart release - id: chart_release - uses: helm/chart-releaser-action@v1.7.0 - with: - charts_dir: chart - skip_existing: true - mark_as_latest: false - env: - CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - - # notification: - # needs: [build-test-cache, push-to-docker, release, chart-release] - # if: always() - # runs-on: ubuntu-22.04 - # steps: - # - name: Slack Notification - # uses: rtCamp/action-slack-notify@v2 - # env: - # SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - # SLACK_CHANNEL: github-ci - # SLACK_COLOR: ${{ job.status }} - # SLACK_ICON: https://github.com/redkubes.png?size=48 - # SLACK_TITLE: CI run - # SLACK_USERNAME: RedKubesBot diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index e9cb3314f6..2140948936 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -69,6 +69,7 @@ jobs: env: REPO_ROOT: ${{ github.workspace }} STABLE: ${{ inputs.promote_to_stable }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -116,13 +117,6 @@ jobs: RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} DRY_RUN: ${{ inputs.dry_run }} - - name: Auto-bump version for next iteration - working-directory: ci - run: npm run release:auto-bump - env: - REPO_ROOT: ${{ github.workspace }} - DRY_RUN: ${{ inputs.dry_run }} - - name: Create GitHub Release working-directory: ci run: npm run release:create-github-release @@ -165,3 +159,10 @@ jobs: mark_as_latest: false env: CR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Auto-bump version for next iteration + # working-directory: ci + # run: npm run release:auto-bump + # env: + # REPO_ROOT: ${{ github.workspace }} + # DRY_RUN: ${{ inputs.dry_run }} diff --git a/ci/package-lock.json b/ci/package-lock.json index 7c24fab7f1..3443a6bb93 100644 --- a/ci/package-lock.json +++ b/ci/package-lock.json @@ -61,7 +61,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -547,6 +546,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -2228,7 +2250,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3167,7 +3188,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -4808,7 +4828,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/ci/scripts/prepare_chart_for_release.sh b/ci/scripts/prepare_chart_for_release.sh index bb4795e345..545fcffd9b 100755 --- a/ci/scripts/prepare_chart_for_release.sh +++ b/ci/scripts/prepare_chart_for_release.sh @@ -1,15 +1,6 @@ #!/bin/bash set -euo pipefail -# This script prepares the helm chart for release. -# Input: -# - BOT_EMAIL: The email address to use for the git commit. -# - BOT_USERNAME: The username to use for the git commit. - -# Validate required environment variables -: "${BOT_EMAIL:?BOT_EMAIL is required}" -: "${BOT_USERNAME:?BOT_USERNAME is required}" - # Retrieve the app version from package.json app_version=$(jq -r '.version' package.json) @@ -26,7 +17,3 @@ cat tpl/chart-values.md >>chart/apl/README.md # Generate schema npx js-yaml values-schema.yaml > chart/apl/values.schema.json - -# Set the global id for git as it seems needed by the next step when a custom image is used -git config --global user.email $BOT_EMAIL -git config --global user.name $BOT_USERNAME \ No newline at end of file diff --git a/ci/src/release/check-versions-yaml.test.ts b/ci/src/release/check-versions-yaml.test.ts index d7008f58e9..0469077dba 100644 --- a/ci/src/release/check-versions-yaml.test.ts +++ b/ci/src/release/check-versions-yaml.test.ts @@ -1,4 +1,4 @@ -import { findInvalidVersions, findRcVersions, parseVersionsYaml } from './check-versions-yaml' +import { findInvalidVersions, findMissingGithubTags, findMissingImages, findRcVersions, githubRepoForEntry, imageRefForEntry, parseVersionsYaml } from './check-versions-yaml' describe('parseVersionsYaml', () => { it('parses all key-value pairs', () => { @@ -30,11 +30,15 @@ describe('findInvalidVersions', () => { expect(findInvalidVersions(versions)).toEqual([]) }) + it('accepts v-prefixed semver (Docker tag format)', () => { + const versions = { api: 'v5.0.0', console: 'v1.4.0-rc.1' } + expect(findInvalidVersions(versions)).toEqual([]) + }) + it('rejects branch names and non-semver strings', () => { - const versions = { api: 'releases/v1.4', console: 'v1.4.0', tasks: 'main' } + const versions = { api: 'releases/v1.4', tasks: 'main' } expect(findInvalidVersions(versions)).toEqual([ { key: 'api', value: 'releases/v1.4' }, - { key: 'console', value: 'v1.4.0' }, { key: 'tasks', value: 'main' }, ]) }) @@ -64,3 +68,86 @@ describe('findRcVersions', () => { expect(findRcVersions(versions)).toHaveLength(3) }) }) + +describe('imageRefForEntry', () => { + it.each([ + ['api', 'v5.0.0', 'linode/apl-api:v5.0.0'], + ['console', 'v5.0.0', 'linode/apl-console:v5.0.0'], + ['consoleLogin', 'v5.0.0', 'linode/apl-console:v5.0.0'], + ['tasks', 'v4.0.0', 'linode/apl-tasks:v4.0.0'], + ['tools', 'v2.11.2','linode/apl-tools:v2.11.2'], + ])('imageRefForEntry(%s, %s) → %s', (key, version, expected) => { + expect(imageRefForEntry(key, version)).toBe(expected) + }) + + it('returns null for aplCharts', () => { + expect(imageRefForEntry('aplCharts', 'v1.5.0')).toBeNull() + }) + + it('returns null for unknown keys', () => { + expect(imageRefForEntry('unknown', 'v1.0.0')).toBeNull() + }) +}) + +describe('findMissingImages', () => { + it('returns empty array when all images exist', () => { + const versions = { api: 'v5.0.0', console: 'v5.0.0' } + expect(findMissingImages(versions, () => true)).toEqual([]) + }) + + it('returns the ref for a missing image', () => { + const versions = { api: 'v5.0.0' } + expect(findMissingImages(versions, () => false)).toEqual(['linode/apl-api:v5.0.0']) + }) + + it('skips aplCharts — it has no container image', () => { + const versions = { aplCharts: 'v1.5.0' } + expect(findMissingImages(versions, () => false)).toEqual([]) + }) + + it('reports all missing images, not just the first', () => { + const versions = { api: 'v5.0.0', tasks: 'v4.0.0', tools: 'v2.11.2' } + expect(findMissingImages(versions, () => false)).toHaveLength(3) + }) +}) + +describe('githubRepoForEntry', () => { + it.each([ + ['api', 'linode/apl-api'], + ['console', 'linode/apl-console'], + ['consoleLogin', 'linode/apl-console'], + ['tasks', 'linode/apl-tasks'], + ['aplCharts', 'linode/apl-charts'], + ])('githubRepoForEntry(%s) → %s', (key, expected) => { + expect(githubRepoForEntry(key)).toBe(expected) + }) + + it('returns null for tools', () => { + expect(githubRepoForEntry('tools')).toBeNull() + }) + + it('returns null for unknown keys', () => { + expect(githubRepoForEntry('unknown')).toBeNull() + }) +}) + +describe('findMissingGithubTags', () => { + it('returns empty array when tag exists', () => { + const versions = { aplCharts: 'v1.5.0' } + expect(findMissingGithubTags(versions, () => true)).toEqual([]) + }) + + it('returns a descriptor when the tag is missing', () => { + const versions = { aplCharts: 'v1.5.0' } + expect(findMissingGithubTags(versions, () => false)).toEqual(['linode/apl-charts@v1.5.0']) + }) + + it('checks all known entries including container image components', () => { + const versions = { api: 'v5.0.0', console: 'v5.0.0' } + expect(findMissingGithubTags(versions, () => false)).toEqual([ + 'linode/apl-api@v5.0.0', + 'linode/apl-console@v5.0.0', + ]) + }) + +}) diff --git a/ci/src/release/check-versions-yaml.ts b/ci/src/release/check-versions-yaml.ts index 535356a485..e621955c67 100644 --- a/ci/src/release/check-versions-yaml.ts +++ b/ci/src/release/check-versions-yaml.ts @@ -1,15 +1,24 @@ +import { execSync } from 'child_process' import { readFileSync } from 'fs' import path from 'path' import { load } from 'js-yaml' import { validateVersion } from './version' +const IMAGE_MAP: Record = { + api: 'linode/apl-api', + console: 'linode/apl-console', + consoleLogin: 'linode/apl-console', + tasks: 'linode/apl-tasks', + tools: 'linode/apl-tools', +} + export function parseVersionsYaml(content: string): Record { return (load(content) as Record) ?? {} } export function findInvalidVersions(versions: Record): Array<{ key: string; value: string }> { return Object.entries(versions) - .filter(([, value]) => !validateVersion(value)) + .filter(([, value]) => !validateVersion(value.replace(/^v/, ''))) .map(([key, value]) => ({ key, value })) } @@ -19,6 +28,45 @@ export function findRcVersions(versions: Record): Array<{ key: s .map(([key, value]) => ({ key, value })) } +export function imageRefForEntry(key: string, version: string): string | null { + const repo = IMAGE_MAP[key] + return repo ? `${repo}:${version}` : null +} + +const GITHUB_REPO_MAP: Record = { + api: 'linode/apl-api', + console: 'linode/apl-console', + consoleLogin: 'linode/apl-console', + tasks: 'linode/apl-tasks', + aplCharts: 'linode/apl-charts', +} + +export function githubRepoForEntry(key: string): string | null { + return GITHUB_REPO_MAP[key] ?? null +} + +export function findMissingGithubTags( + versions: Record, + tagExists: (repo: string, tag: string) => boolean, +): string[] { + return Object.entries(versions) + .flatMap(([key, version]) => { + const repo = githubRepoForEntry(key) + return repo && !tagExists(repo, version) ? [`${repo}@${version}`] : [] + }) +} + +export function findMissingImages( + versions: Record, + imageExists: (ref: string) => boolean, +): string[] { + return Object.entries(versions) + .flatMap(([key, version]) => { + const ref = imageRefForEntry(key, version) + return ref && !imageExists(ref) ? [ref] : [] + }) +} + if (require.main === module) { const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') const stable = process.env.STABLE === 'true' @@ -31,7 +79,7 @@ if (require.main === module) { for (const { key, value } of invalid) { console.error(` ${key}: "${value}"`) } - console.error(`All values must be semver (e.g. 1.4.0 or 1.4.0-rc.1)`) + console.error(`All values must be semver (e.g. v1.4.0 or v1.4.0-rc.1)`) process.exit(1) } @@ -46,5 +94,41 @@ if (require.main === module) { } } - console.log(`versions.yaml: all ${Object.keys(versions).length} entries are semver-compatible`) + const missing = findMissingImages(versions, (ref) => { + try { + execSync(`docker manifest inspect ${ref}`, { stdio: 'pipe' }) + console.log(`dockerhub.io/${ref}`) + return true + } catch { + return false + } + }) + + if (missing.length > 0) { + console.error(`versions.yaml references container images that do not exist:`) + for (const ref of missing) { + console.error(` ${ref}`) + } + process.exit(1) + } + + const missingTags = findMissingGithubTags(versions, (repo, tag) => { + try { + execSync(`gh api repos/${repo}/git/ref/tags/${tag}`, { stdio: 'pipe' }) + console.log(`github.com/${repo}@${tag}`) + return true + } catch { + return false + } + }) + + if (missingTags.length > 0) { + console.error(`versions.yaml references GitHub tags that do not exist:`) + for (const ref of missingTags) { + console.error(` ${ref}`) + } + process.exit(1) + } + + console.log(`versions.yaml: all ${Object.keys(versions).length} entries are semver-compatible, images exist, and GitHub tags exist`) } diff --git a/ci/src/release/create-github-release.ts b/ci/src/release/create-github-release.ts index 738219346c..7adeef4f1d 100644 --- a/ci/src/release/create-github-release.ts +++ b/ci/src/release/create-github-release.ts @@ -35,8 +35,7 @@ async function main() { `--title "Release ${tag}"`, `--generate-notes`, notesStartFlag, - prereleaseFlag, - rcBanner ? `--notes-prepend "${rcBanner}"` : '', + prereleaseFlag ].filter(Boolean).join(' ') execSync(notesCmd, { stdio: 'inherit' }) From 85e0bfe3644b246ba448088ce20727d043bd4eba Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:53:57 +0200 Subject: [PATCH 20/24] ci: remove old workflows --- .github/workflows/Releases.yml | 26 --------- .github/workflows/create_rc.yml | 99 --------------------------------- .github/workflows/patch_rc.yml | 82 --------------------------- package.json | 3 - 4 files changed, 210 deletions(-) delete mode 100644 .github/workflows/Releases.yml delete mode 100644 .github/workflows/create_rc.yml delete mode 100644 .github/workflows/patch_rc.yml diff --git a/.github/workflows/Releases.yml b/.github/workflows/Releases.yml deleted file mode 100644 index 2f5c9cc21f..0000000000 --- a/.github/workflows/Releases.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - dry-run: - type: boolean - description: 'Dry run: Uncheck if you want to publish a release' - default: true - -jobs: - release-please: - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - name: Install dependencies - run: | - npm install semantic-release@24 @semantic-release/git @semantic-release/changelog -D - - name: Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release --dry-run=${{ github.event.inputs.dry-run }} diff --git a/.github/workflows/create_rc.yml b/.github/workflows/create_rc.yml deleted file mode 100644 index 65a95eaa46..0000000000 --- a/.github/workflows/create_rc.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Create Release Candidate. -run-name: Create Release Candidate from ${{ github.event.inputs.rc_commit_sha || 'main' }} - -on: - workflow_dispatch: - inputs: - rc_commit_sha: - description: 'Commit from which to create the release candidate. If not provided, the latest commit on main will be used' - required: false - type: string - dry_run: - description: 'Dry Run: If true, the pipeline will not publish the helm chart.' - required: true - default: 'true' - type: choice - options: - - true - - false - -jobs: - create_patch_release: - runs-on: ubuntu-22.04 - outputs: - rc_branch: ${{ steps.create_release.outputs.rc_branch }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Determine commit from where to create the RC - id: determine_commit - run: | - if [ -n "${{ github.event.inputs.rc_commit_sha }}" ]; then - commit_sha="${{ github.event.inputs.rc_commit_sha }}" - echo "Using provided commit sha: $commit_sha" - echo "commit_sha=$commit_sha" >> $GITHUB_ENV - else - commit_sha=$(git rev-parse HEAD) - echo "No commit sha provided, using the latest commit on main: $commit_sha" - echo "commit_sha=$commit_sha" >> $GITHUB_ENV - fi - - - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Install dependencies - run: | - npm install standard-version - - - name: Create release candidate tag & branch - id: create_release - env: - DRY_RUN: ${{ github.event.inputs.dry_run }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMMIT_SHA: ${{ env.commit_sha }} - BOT_EMAIL: ${{ vars.BOT_EMAIL }} - BOT_USERNAME: ${{ vars.BOT_USERNAME }} - run: | - ci/scripts/create_rc.sh - - - name: Prepare chart - env: - BOT_EMAIL: ${{ vars.BOT_EMAIL }} - BOT_USERNAME: ${{ vars.BOT_USERNAME }} - id: prepare_chart - run: | - ci/scripts/prepare_chart_for_release.sh - - - name: Dry Run Outputs - if: ${{ github.event.inputs.dry_run == 'true'}} - run: | - echo "This Pipeline was executed in dry run mode so it will not publish the helm chart." - echo "Below are some useful data to check:" - echo "Release candidate branch: $(git rev-parse --abbrev-ref HEAD)" - echo "-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*" - echo "Contents of: chart/apl/Chart.yaml" - cat chart/apl/Chart.yaml - echo "-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*" - echo "Contents of: package.json" - cat package.json - echo "-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*" - echo "Latest 5 commits on the release candidate branch" - git log -n 5 --pretty=format:"%h %s" --abbrev-commit - echo "-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*--*-*" - echo "Commits behind main branch VS Commits ahead of main branch" - git rev-list --left-right --count origin/main...HEAD - - - name: Create and publish otomi chart release - if: ${{ github.event.inputs.dry_run == 'false' }} - id: chart_release - uses: helm/chart-releaser-action@v1.7.0 - with: - charts_dir: chart - skip_existing: true - mark_as_latest: false - env: - CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/patch_rc.yml b/.github/workflows/patch_rc.yml deleted file mode 100644 index 0a7754e1dc..0000000000 --- a/.github/workflows/patch_rc.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Patch Release Candidate - -on: - push: - branches: - - rc/* - tags-ignore: - - '*' - -jobs: - patch_rc: - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Analze commits since last rc release - id: analyze_commits - run: | - - CURRENT_VERSION=$(jq '.version' package.json -r) - echo "Current version: $CURRENT_VERSION" - LATEST_TAG=$(git tag -l "v${CURRENT_VERSION%.*}*" | sort -V | tail -n 1) - echo "Latest tag: $LATEST_TAG" - - echo "Commits since last rc release:" - git -P log --pretty=format:"%s" "${LATEST_TAG}..HEAD" - COMMITS=$(git log --pretty=format:"%s" "${LATEST_TAG}..HEAD") - - if [[ $COMMITS == *"feat:"* || $COMMITS == *"fix:"* ]]; then - echo "RC_RELEASE=true" >> $GITHUB_ENV - else - echo "No feat/fix commits found. Skipping release." - echo "RC_RELEASE=false" >> $GITHUB_ENV - fi - - - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Install dependencies - if: ${{ env.RC_RELEASE == 'true' }} - run: | - npm install standard-version - - - name: Release new RC version - if: ${{ env.RC_RELEASE == 'true' }} - env: - GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} - BOT_EMAIL: ${{ vars.BOT_EMAIL }} - BOT_USERNAME: ${{ vars.BOT_USERNAME }} - run: | - echo "Detected "feat"/"fix"/"chore: trigger" commits. Running standard-version..." - git config --global user.email $BOT_EMAIL - git config --global user.name $BOT_USERNAME - npm run release -- --prerelease rc --skip.changelog - - git push --follow-tags - TAG="v$(jq '.version' package.json -r)" - gh release create "$TAG" --title="Release: $TAG" --notes="Automated release for $TAG" --latest=false -p - - - name: Prepare chart - if: ${{ env.RC_RELEASE == 'true' }} - id: prepare_chart - env: - BOT_EMAIL: ${{ vars.BOT_EMAIL }} - BOT_USERNAME: ${{ vars.BOT_USERNAME }} - run: | - ci/scripts/prepare_chart_for_release.sh - - - name: Create and publish otomi chart release - if: ${{ github.event.inputs.dry_run == 'false' }} && ${{ env.RC_RELEASE == 'true' }} - id: chart_release - uses: helm/chart-releaser-action@v1.7.0 - with: - charts_dir: chart - skip_existing: true - mark_as_latest: false - env: - CR_TOKEN: '${{ secrets.BOT_TOKEN }}' diff --git a/package.json b/package.json index f5311ef0bd..e7260683a5 100644 --- a/package.json +++ b/package.json @@ -150,9 +150,6 @@ "lint:schema-versions": "cd ci && src/check-schema-version.mjs", "migrate-values": "ENV_DIR=/tmp/otomi-bootstrap-dev binzx/otomi migrate -ni", "prepare": "husky", - "release": "standard-version", - "release:github": "github-release-from-changelog", - "release:bump:minor": "standard-version --skip.changelog true --release-as minor", "score-templates": "binzx/otomi score-templates", "spellcheck": "cspell 'docs/**/*.md' 'values-schema.yaml' '*.md' '.values/README.md'", "tasks:copy-certs": "binzx/otomi task -n copyCerts", From 7496a3a88eec8364e051d41a9bbc3256b377b2ad Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:53:39 +0200 Subject: [PATCH 21/24] ci: derive version from git tags --- .github/workflows/cut-release-branch.yml | 55 +++------ .github/workflows/main.yml | 19 ++++ .github/workflows/release-from-branch.yml | 64 ++++------- CONTEXT.md | 18 ++- Dockerfile | 2 + ci/package.json | 10 +- ci/scripts/create_rc.sh | 52 --------- ci/scripts/prepare_chart_for_release.sh | 4 +- ci/src/release/auto-bump.ts | 24 ---- ci/src/release/bump-version.ts | 17 --- .../release/check-version-matches-branch.ts | 19 ---- ci/src/release/commit-release-branch.ts | 20 ---- ci/src/release/compute-dev-version.ts | 22 ++++ ci/src/release/compute-tag.test.ts | 18 ++- ci/src/release/compute-tag.ts | 24 ++-- ci/src/release/derive-release-branch.ts | 30 +++++ ci/src/release/open-main-bump-pr.ts | 46 -------- ci/src/release/promote-package-version.ts | 23 ---- ci/src/release/validate-version.ts | 10 -- ci/src/release/version.test.ts | 106 ++++++++++++++++++ ci/src/release/version.ts | 40 +++++++ ...001-git-tags-as-version-source-of-truth.md | 18 +++ package.json | 2 +- 23 files changed, 316 insertions(+), 327 deletions(-) delete mode 100755 ci/scripts/create_rc.sh delete mode 100644 ci/src/release/auto-bump.ts delete mode 100644 ci/src/release/bump-version.ts delete mode 100644 ci/src/release/check-version-matches-branch.ts delete mode 100644 ci/src/release/commit-release-branch.ts create mode 100644 ci/src/release/compute-dev-version.ts create mode 100644 ci/src/release/derive-release-branch.ts delete mode 100644 ci/src/release/open-main-bump-pr.ts delete mode 100644 ci/src/release/promote-package-version.ts delete mode 100644 ci/src/release/validate-version.ts create mode 100644 docs/adr/0001-git-tags-as-version-source-of-truth.md diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml index b2a9565094..4f88fb4141 100644 --- a/.github/workflows/cut-release-branch.yml +++ b/.github/workflows/cut-release-branch.yml @@ -3,17 +3,20 @@ name: Cut Release Branch on: workflow_dispatch: inputs: - minor_version: - description: 'Major.minor version to release, with v prefix (e.g. v1.4)' + bump_type: + description: 'Version bump type' required: true - type: string + type: choice + options: + - minor + - major base_branch: description: 'Branch to cut from' required: true default: 'main' type: string dry_run: - description: 'Dry run — skip all writes (git push, PRs)' + description: 'Dry run — skip all writes (git push)' required: true default: 'true' type: choice @@ -22,7 +25,6 @@ on: - 'false' env: - RELEASE_BRANCH: releases/${{ inputs.minor_version }} BOT_EMAIL: ${{ vars.BOT_EMAIL }} BOT_USERNAME: ${{ vars.BOT_USERNAME }} @@ -31,7 +33,6 @@ jobs: runs-on: ubuntu-22.04 permissions: contents: write - pull-requests: write steps: - name: Checkout @@ -50,11 +51,12 @@ jobs: working-directory: ci run: npm ci - - name: Validate version input + - name: Derive release branch from highest stable tag + id: derive-branch working-directory: ci - run: npm run release:validate-version + run: npm run release:derive-release-branch env: - MINOR_VERSION: ${{ inputs.minor_version }} + BUMP_TYPE: ${{ inputs.bump_type }} - name: Check release branch does not already exist working-directory: ci @@ -76,37 +78,10 @@ jobs: run: npm run release:configure-git - name: Create release branch + if: ${{ inputs.dry_run == 'false' }} working-directory: ci run: npm run release:create-release-branch - - name: Bump package.json to target version - working-directory: ci - run: npm run release:bump-version - env: - MINOR_VERSION: ${{ inputs.minor_version }} - REPO_ROOT: ${{ github.workspace }} - DRY_RUN: ${{ inputs.dry_run }} - - - name: Commit and push release branch - working-directory: ci - run: npm run release:commit-release-branch - env: - REPO_ROOT: ${{ github.workspace }} - DRY_RUN: ${{ inputs.dry_run }} - - # - name: Open release PR - # working-directory: ci - # run: npm run release:open-release-pr - # env: - # BASE_BRANCH: ${{ inputs.base_branch }} - # REPO_ROOT: ${{ github.workspace }} - # DRY_RUN: ${{ inputs.dry_run }} - - - name: Open main version bump PR - working-directory: ci - run: npm run release:open-main-bump-pr - env: - MINOR_VERSION: ${{ inputs.minor_version }} - BASE_BRANCH: ${{ inputs.base_branch }} - REPO_ROOT: ${{ github.workspace }} - DRY_RUN: ${{ inputs.dry_run }} + - name: Push release branch + if: ${{ inputs.dry_run == 'false' }} + run: git push origin "$RELEASE_BRANCH" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5e89c235c..0ed65f965b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,6 +38,24 @@ jobs: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Install CI dependencies + working-directory: ci + run: npm ci + + - name: Compute dev version + id: dev-version + working-directory: ci + run: npm run release:compute-dev-version + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Login to Github Packages @@ -52,6 +70,7 @@ jobs: push: true build-args: | APPS_REVISION=${{ env.APPS_REVISION }} + VERSION=${{ steps.dev-version.outputs.version }} context: . tags: | ${{ env.CACHE_REGISTRY }}/${{ env.CACHE_REPO }}:${{ env.TAG }} diff --git a/.github/workflows/release-from-branch.yml b/.github/workflows/release-from-branch.yml index 2140948936..d43599b430 100644 --- a/.github/workflows/release-from-branch.yml +++ b/.github/workflows/release-from-branch.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: release_branch: - description: 'Release cycle branch (e.g. release/v1.4)' + description: 'Release cycle branch (e.g. releases/v1.4)' required: true type: string promote_to_stable: @@ -41,6 +41,13 @@ jobs: pull-requests: write steps: + - name: Validate release branch + run: | + if [[ "${{ inputs.release_branch }}" != releases/* ]]; then + echo "::error::release_from_branch may only run on releases/* branches. Got: ${{ inputs.release_branch }}" + exit 1 + fi + - name: Checkout uses: actions/checkout@v6 with: @@ -57,12 +64,6 @@ jobs: working-directory: ci run: npm ci - - name: Check package.json version matches release branch - working-directory: ci - run: npm run release:check-version-matches-branch - env: - REPO_ROOT: ${{ github.workspace }} - - name: Check versions.yaml entries are semver-compatible working-directory: ci run: npm run release:check-versions-yaml @@ -71,25 +72,12 @@ jobs: STABLE: ${{ inputs.promote_to_stable }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Run tests in Docker (test gate) - uses: docker/build-push-action@v7 - with: - push: false - load: true - context: . - build-args: SKIP_TESTS=false - tags: ${{ env.LOCAL_CACHE_IMAGE }} - - name: Compute release tag id: compute-tag working-directory: ci run: npm run release:compute-tag env: PROMOTE_TO_STABLE: ${{ inputs.promote_to_stable }} - REPO_ROOT: ${{ github.workspace }} - name: Check tag does not already exist working-directory: ci @@ -97,19 +85,24 @@ jobs: env: RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Run tests in Docker (test gate) + uses: docker/build-push-action@v7 + with: + push: false + load: true + context: . + build-args: | + SKIP_TESTS=false + VERSION=${{ steps.compute-tag.outputs.tag }} + tags: ${{ env.LOCAL_CACHE_IMAGE }} + - name: Configure git bot identity working-directory: ci run: npm run release:configure-git - - name: Promote package.json to stable (if applicable) - working-directory: ci - run: npm run release:promote-package-version - env: - RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} - PROMOTE_TO_STABLE: ${{ inputs.promote_to_stable }} - REPO_ROOT: ${{ github.workspace }} - DRY_RUN: ${{ inputs.dry_run }} - - name: Create and push git tag working-directory: ci run: npm run release:tag-release @@ -145,11 +138,9 @@ jobs: - name: Prepare Helm chart if: ${{ inputs.dry_run == 'false' }} run: ci/scripts/prepare_chart_for_release.sh + env: + RELEASE_TAG: ${{ steps.compute-tag.outputs.tag }} - # chart-releaser-action has no RC awareness — it publishes whatever version - # prepare_chart_for_release.sh stamps into Chart.yaml (e.g. 1.4.0-rc.1). - # mark_as_latest: false prevents RC charts from becoming the default in - # `helm search repo`, which is the closest Helm equivalent to a prerelease flag. - name: Publish Helm chart if: ${{ inputs.dry_run == 'false' }} uses: helm/chart-releaser-action@v1.7.0 @@ -159,10 +150,3 @@ jobs: mark_as_latest: false env: CR_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # - name: Auto-bump version for next iteration - # working-directory: ci - # run: npm run release:auto-bump - # env: - # REPO_ROOT: ${{ github.workspace }} - # DRY_RUN: ${{ inputs.dry_run }} diff --git a/CONTEXT.md b/CONTEXT.md index 328d7ec227..eaa70a85ba 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,20 +2,28 @@ ## Release Cycle -A `release/v` branch (e.g., `release/v1.4`) that covers all versions in a major.minor series: release candidates (`v1.4.0-rc.1`, `v1.4.0-rc.2`), the stable cut (`v1.4.0`), and any subsequent patches (`v1.4.1-rc.1`, `v1.4.1`). One branch is created per cycle; it is never recreated. +A `releases/v` branch (e.g., `releases/v1.4`) that covers all versions in a major.minor series: release candidates (`v1.4.0-rc.1`, `v1.4.0-rc.2`), the stable cut (`v1.4.0`), and any subsequent patches (`v1.4.1-rc.1`, `v1.4.1`). One branch is created per cycle; it is never recreated. + +## Version Source of Truth + +Git tags are the sole source of truth for version. `package.json` always holds `0.0.0` and is never updated by release workflows. The canonical version is computed from git tags at build time and injected into the image via a Dockerfile `ARG VERSION`. ## Cut Release Branch -The `workflow_dispatch` GitHub Actions workflow that starts a Release Cycle. Accepts a `version` input (`1.4.0` or `1.4.0-rc.1`), runs tests via Docker build, bumps `package.json` to that version, creates `release/v`, and opens two PRs: one from `release/v` back to `base_branch` with the release checklist, and one bumping `base_branch` to the next minor development version (e.g., cutting `1.4.0-rc.1` also opens a PR bumping `main` to `1.5.0-rc.0`). +The `workflow_dispatch` GitHub Actions workflow that starts a Release Cycle. Accepts a `bump_type` input (`minor` or `major`) and a `base_branch`. Derives the new branch name by finding the highest stable git tag repo-wide and applying the requested bump (e.g., highest stable `v5.1.0` + `minor` → `releases/v5.2`). Runs tests via Docker build, then creates and pushes the release branch. Makes no commits, creates no tags, and opens no PRs. ## Release from Branch -The `workflow_dispatch` GitHub Actions workflow that publishes a release from an existing Release Cycle branch. Reads the current version from `package.json`, tags and releases it, then auto-bumps `package.json` to the next iteration. Has a `promote_to_stable` input that strips the RC prerelease suffix and advances to the next patch RC after tagging. Also publishes the Docker image and Helm chart. +The `workflow_dispatch` GitHub Actions workflow that publishes a release from an existing Release Cycle branch. Only permitted on `releases/*` branches. Derives the release tag from the highest git tag on that branch: for an RC run, increments the RC number (or starts at `rc.1` if no tags exist on the branch yet, deriving the version from the branch name); for a stable run (`promote_to_stable=true`), strips the RC suffix from the highest RC tag. Never reads or writes `package.json`. Publishes the Docker image and Helm chart. ## Release Candidate (RC) -A pre-release version in `..-rc.` format (e.g., `1.4.0-rc.1`). Produced by "Release from Branch" without `promote_to_stable`. Each RC run auto-increments `n` and commits the next RC version to the Release Cycle branch. +A pre-release version in `..-rc.` format (e.g., `1.4.0-rc.1`). Produced by "Release from Branch" without `promote_to_stable`. Each RC run derives the next tag from the highest existing RC tag on the branch. ## Stable Release -A version without a prerelease suffix (e.g., `1.4.0`, `1.4.1`). Produced by "Release from Branch" with `promote_to_stable=true`. After tagging stable, the branch is auto-bumped to the next patch RC (e.g., `1.4.1-rc.1`). +A version without a prerelease suffix (e.g., `1.4.0`, `1.4.1`). Produced by "Release from Branch" with `promote_to_stable=true`. Derived by stripping the RC suffix from the highest RC tag on the branch. + +## Dev Version + +The semver version stamped into Docker images built from `main` or feature branches. Computed as: take the highest git tag repo-wide, extract its base version (strip any RC suffix), increment the patch, append `-dev.` (e.g., highest tag `v6.0.0-rc.8` → dev version `6.0.1-dev.abc1234`). Always strictly greater than the highest published tag. Never pushed as a git tag. diff --git a/Dockerfile b/Dockerfile index 2762b28c06..16ff241694 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,8 @@ RUN npm config set update-notifier false RUN npm ci --ignore-scripts COPY --chown=app . . +ARG VERSION=0.0.0 +RUN npm version $VERSION --no-git-tag-version RUN npm run compile # Run tests with the CI-specific script that has proper Jest flags RUN set -e && \ diff --git a/ci/package.json b/ci/package.json index ac05c768f0..eaa7a7a379 100644 --- a/ci/package.json +++ b/ci/package.json @@ -37,21 +37,15 @@ "update-helm-chart-deps": "node src/update-helm-chart-deps.mjs", "check-schema-versions": "node src/check-schema-versions.mjs", "test": "jest", - "release:validate-version": "tsx src/release/validate-version.ts", "release:configure-git": "tsx src/release/configure-git.ts", + "release:derive-release-branch": "tsx src/release/derive-release-branch.ts", "release:check-branch-not-exists": "tsx src/release/check-branch-not-exists.ts", "release:create-release-branch": "tsx src/release/create-release-branch.ts", - "release:bump-version": "tsx src/release/bump-version.ts", - "release:commit-release-branch": "tsx src/release/commit-release-branch.ts", - "release:open-release-pr": "tsx src/release/open-release-pr.ts", - "release:open-main-bump-pr": "tsx src/release/open-main-bump-pr.ts", - "release:check-version-matches-branch": "tsx src/release/check-version-matches-branch.ts", "release:check-versions-yaml": "tsx src/release/check-versions-yaml.ts", "release:compute-tag": "tsx src/release/compute-tag.ts", + "release:compute-dev-version": "tsx src/release/compute-dev-version.ts", "release:check-tag-not-exists": "tsx src/release/check-tag-not-exists.ts", - "release:promote-package-version": "tsx src/release/promote-package-version.ts", "release:tag-release": "tsx src/release/tag-release.ts", - "release:auto-bump": "tsx src/release/auto-bump.ts", "release:create-github-release": "tsx src/release/create-github-release.ts", "release:docker-push": "tsx src/release/docker-push.ts" }, diff --git a/ci/scripts/create_rc.sh b/ci/scripts/create_rc.sh deleted file mode 100755 index 58605a8c41..0000000000 --- a/ci/scripts/create_rc.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# This script creates a release candidate tag for a given commit. -# Input: -# - COMMIT_SHA: The commit SHA to create the release candidate tag for. -# - DRY_RUN: If set to true, the script will not create the tag, but will print the commands that would be executed. -# - GITHUB_TOKEN: The GitHub token to use for authentication. -# - BOT_EMAIL: The email address to use for the git commit. -# - BOT_USERNAME: The username to use for the git commit. - -# Validate required environment variables -: "${COMMIT_SHA:?COMMIT_SHA is required}" -: "${BOT_EMAIL:?BOT_EMAIL is required}" -: "${BOT_USERNAME:?BOT_USERNAME is required}" -: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" - -# Configure Git -echo "Configuring Git..." -git config --global user.email "$BOT_EMAIL" -git config --global user.name "$BOT_USERNAME" - -# Reset to the specified commit -echo "Resetting to commit $COMMIT_SHA..." -git reset --hard "$COMMIT_SHA" - -# Determine the next version -echo "Determining the next version..." -npm run release -- --skip.commit --skip.tag --skip.changelog -new_version=$(jq -r '.version' package.json) -branch_name="rc/v${new_version%.*}" -release_branch_name="${branch_name//rc/release}" -echo "$branch_name" >> rc_branch_name.txt -git reset --hard "$COMMIT_SHA" - -echo "Creating branch $branch_name..." -git checkout -b "$branch_name" - -# Dry run or actual execution -if [ "$DRY_RUN" == "true" ]; then - echo -e "Running in dry run mode. No changes will be pushed." - npm run release -- --prerelease rc --skip.changelog -else - npm run release -- --prerelease rc --skip.changelog - git push -u origin "$branch_name" --follow-tags - git fetch --tags origin - rc_version="v$(jq -r '.version' package.json)" - echo "Creating GitHub release: $rc_version" - gh release create "$rc_version" --verify-tag --title="Release Candidate: $rc_version" --notes="Automated release for $rc_version" --latest=false -p -fi - -echo "Script completed successfully." \ No newline at end of file diff --git a/ci/scripts/prepare_chart_for_release.sh b/ci/scripts/prepare_chart_for_release.sh index 545fcffd9b..38c3b7d51f 100755 --- a/ci/scripts/prepare_chart_for_release.sh +++ b/ci/scripts/prepare_chart_for_release.sh @@ -1,8 +1,8 @@ #!/bin/bash set -euo pipefail -# Retrieve the app version from package.json -app_version=$(jq -r '.version' package.json) +# Retrieve the app version from the RELEASE_TAG env var (e.g. v6.0.0-rc.9 → 6.0.0-rc.9) +app_version="${RELEASE_TAG#v}" # Update Chart.yaml and values.yaml with the new app version sed -i "s/0.0.0-chart-version/$app_version/g" chart/apl/Chart.yaml diff --git a/ci/src/release/auto-bump.ts b/ci/src/release/auto-bump.ts deleted file mode 100644 index 29d4f8ea77..0000000000 --- a/ci/src/release/auto-bump.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { execSync } from 'child_process' -import { readFileSync } from 'fs' -import path from 'path' -import { incrementRc, nextPatchRc } from './version' - -const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') -const dryRun = process.env.DRY_RUN === 'true' - -const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) -const currentVersion: string = pkg.version -const isRc = currentVersion.includes('-rc.') -const nextVersion = isRc ? incrementRc(currentVersion) : nextPatchRc(currentVersion) - -console.log(`Auto-bumping from ${currentVersion} → ${nextVersion}`) - -if (dryRun) { - console.log(`[dry-run] Would bump package.json to ${nextVersion} and push`) - process.exit(0) -} - -execSync(`npm version ${nextVersion} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git commit -m "chore(release): bump to v${nextVersion}"`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git push`, { cwd: repoRoot, stdio: 'inherit' }) diff --git a/ci/src/release/bump-version.ts b/ci/src/release/bump-version.ts deleted file mode 100644 index b793396dc7..0000000000 --- a/ci/src/release/bump-version.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { execSync } from 'child_process' -import path from 'path' -import { cycleStartVersion, stripV } from './version' - -const minorVersion = process.env.MINOR_VERSION! -const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') -const dryRun = process.env.DRY_RUN === 'true' - -const version = cycleStartVersion(stripV(minorVersion)) - -if (dryRun) { - console.log(`[dry-run] Would run: npm version ${version} --no-git-tag-version`) - process.exit(0) -} - -execSync(`npm version ${version} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) -console.log(`Bumped package.json to ${version}`) diff --git a/ci/src/release/check-version-matches-branch.ts b/ci/src/release/check-version-matches-branch.ts deleted file mode 100644 index 5eeaacd3bb..0000000000 --- a/ci/src/release/check-version-matches-branch.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { readFileSync } from 'fs' -import path from 'path' -import { versionMatchesBranch } from './version' - -const releaseBranch = process.env.RELEASE_BRANCH! -const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - -const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) -const version: string = pkg.version - -if (!versionMatchesBranch(version, releaseBranch)) { - console.error( - `Version mismatch: package.json is at "${version}" but branch is "${releaseBranch}". ` + - `Expected major.minor to be "${releaseBranch.replace('releases/v', '')}".`, - ) - process.exit(1) -} - -console.log(`Version "${version}" matches branch "${releaseBranch}"`) diff --git a/ci/src/release/commit-release-branch.ts b/ci/src/release/commit-release-branch.ts deleted file mode 100644 index c905c496ca..0000000000 --- a/ci/src/release/commit-release-branch.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { execSync } from 'child_process' -import { readFileSync } from 'fs' -import path from 'path' - -const branch = process.env.RELEASE_BRANCH! -const dryRun = process.env.DRY_RUN === 'true' -const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - -const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) -const version: string = pkg.version - -if (dryRun) { - console.log(`[dry-run] Would commit and push ${branch} with version ${version}`) - process.exit(0) -} - -execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git commit -m "chore(release): bump to v${version}"`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git push -u origin "${branch}"`, { cwd: repoRoot, stdio: 'inherit' }) -console.log(`Committed and pushed ${branch}`) diff --git a/ci/src/release/compute-dev-version.ts b/ci/src/release/compute-dev-version.ts new file mode 100644 index 0000000000..10b45cbb6d --- /dev/null +++ b/ci/src/release/compute-dev-version.ts @@ -0,0 +1,22 @@ +import { appendFileSync } from 'fs' +import { execSync } from 'child_process' +import { computeDevVersion, highestTag } from './version' + +const tagsRaw = execSync('git tag', { encoding: 'utf8' }) +const tags = tagsRaw.trim().split('\n').filter(Boolean) +const highest = highestTag(tags) + +if (!highest) { + console.error('No tags found in repository — cannot compute dev version.') + process.exit(1) +} + +const shortSha = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim() +const version = computeDevVersion(highest, shortSha) + +console.log(`Highest tag: ${highest}`) +console.log(`Dev version: ${version}`) + +if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`) +} diff --git a/ci/src/release/compute-tag.test.ts b/ci/src/release/compute-tag.test.ts index db8e0d99d1..f6806c79c1 100644 --- a/ci/src/release/compute-tag.test.ts +++ b/ci/src/release/compute-tag.test.ts @@ -1,13 +1,19 @@ import { computeTag } from './compute-tag' describe('computeTag', () => { - it('returns the version as-is when not promoting to stable', () => { - expect(computeTag('1.4.0-rc.1', false)).toBe('v1.4.0-rc.1') - expect(computeTag('1.4.0-rc.9', false)).toBe('v1.4.0-rc.9') + it('returns next RC when branch has existing RC tags', () => { + expect(computeTag(['v6.1.0-rc.2', 'v6.1.0-rc.1'], 'releases/v6.1', false)).toBe('v6.1.0-rc.3') }) - it('strips the RC suffix when promoting to stable', () => { - expect(computeTag('1.4.0-rc.3', true)).toBe('v1.4.0') - expect(computeTag('6.0.0-rc.0', true)).toBe('v6.0.0') + it('starts at rc.1 from branch name when no tags on branch', () => { + expect(computeTag([], 'releases/v6.1', false)).toBe('v6.1.0-rc.1') + }) + + it('promotes highest RC to stable', () => { + expect(computeTag(['v6.1.0-rc.3', 'v6.1.0-rc.2'], 'releases/v6.1', true)).toBe('v6.1.0') + }) + + it('throws when promoting to stable with no RC tags', () => { + expect(() => computeTag([], 'releases/v6.1', true)).toThrow() }) }) diff --git a/ci/src/release/compute-tag.ts b/ci/src/release/compute-tag.ts index 42000a7bf3..7335b82aac 100644 --- a/ci/src/release/compute-tag.ts +++ b/ci/src/release/compute-tag.ts @@ -1,19 +1,19 @@ import { appendFileSync } from 'fs' -import { readFileSync } from 'fs' -import path from 'path' -import { promoteToStable } from './version' +import { execSync } from 'child_process' +import { computeNextRcTag, computeStableTag } from './version' -export function computeTag(version: string, promote: boolean): string { - return `v${promote ? promoteToStable(version) : version}` +export function computeTag(branchTags: string[], branchName: string, promote: boolean): string { + return promote ? computeStableTag(branchTags) : computeNextRcTag(branchTags, branchName) } -async function main() { - const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) - const version: string = pkg.version +if (require.main === module) { const promote = process.env.PROMOTE_TO_STABLE === 'true' - const tag = computeTag(version, promote) + const branchName = process.env.RELEASE_BRANCH! + + const tagsRaw = execSync('git tag --merged HEAD', { encoding: 'utf8' }) + const branchTags = tagsRaw.trim().split('\n').filter(Boolean) + const tag = computeTag(branchTags, branchName, promote) console.log(`Computed tag: ${tag}`) if (process.env.GITHUB_OUTPUT) { @@ -21,7 +21,3 @@ async function main() { appendFileSync(process.env.GITHUB_OUTPUT, `is_prerelease=${tag.includes('-rc.')}\n`) } } - -if (require.main === module) { - main().catch((err) => { console.error(err.message); process.exit(1) }) -} diff --git a/ci/src/release/derive-release-branch.ts b/ci/src/release/derive-release-branch.ts new file mode 100644 index 0000000000..729ea1b1ec --- /dev/null +++ b/ci/src/release/derive-release-branch.ts @@ -0,0 +1,30 @@ +import { appendFileSync, writeFileSync } from 'fs' +import { execSync } from 'child_process' +import { computeReleaseBranchName, highestStableTag } from './version' + +const bumpType = process.env.BUMP_TYPE as 'minor' | 'major' + +if (bumpType !== 'minor' && bumpType !== 'major') { + console.error(`Invalid BUMP_TYPE: "${bumpType}". Must be "minor" or "major".`) + process.exit(1) +} + +const tagsRaw = execSync('git tag', { encoding: 'utf8' }) +const tags = tagsRaw.trim().split('\n').filter(Boolean) +const highest = highestStableTag(tags) + +if (!highest) { + console.error('No stable tags found in repository — cannot derive release branch name.') + process.exit(1) +} + +const branch = computeReleaseBranchName(highest, bumpType) +console.log(`Highest stable tag: ${highest}`) +console.log(`Derived release branch: ${branch}`) + +if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `branch=${branch}\n`) +} +if (process.env.GITHUB_ENV) { + appendFileSync(process.env.GITHUB_ENV, `RELEASE_BRANCH=${branch}\n`) +} diff --git a/ci/src/release/open-main-bump-pr.ts b/ci/src/release/open-main-bump-pr.ts deleted file mode 100644 index 73d5cf05bb..0000000000 --- a/ci/src/release/open-main-bump-pr.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { execSync } from 'child_process' -import { unlinkSync, writeFileSync } from 'fs' -import { tmpdir } from 'os' -import path, { join } from 'path' -import { cycleStartVersion, nextMainVersion, stripV } from './version' - -async function main() { - const minorVersion = process.env.MINOR_VERSION! - const baseBranch = process.env.BASE_BRANCH! - const dryRun = process.env.DRY_RUN === 'true' - const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - - const nextVersion = nextMainVersion(cycleStartVersion(stripV(minorVersion))) - const bumpBranch = `chore/bump-main-${minorVersion}` - const title = `chore: bump main after cutting releases/${minorVersion}` - const body = `Automated version bump after cutting the \`releases/${minorVersion}\` release cycle branch.\n\n- Bumps \`package.json\` to \`${nextVersion}\`` - - if (dryRun) { - console.log('[dry-run] Would create main bump PR:') - console.log(` branch: ${bumpBranch}`) - console.log(` title: ${title}`) - console.log(` next version: ${nextVersion}`) - return - } - - execSync(`git checkout "${baseBranch}"`, { cwd: repoRoot, stdio: 'inherit' }) - execSync(`git pull`, { cwd: repoRoot, stdio: 'inherit' }) - execSync(`git checkout -b "${bumpBranch}"`, { cwd: repoRoot, stdio: 'inherit' }) - execSync(`npm version ${nextVersion} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) - execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) - execSync(`git commit -m "chore(release): bump main to v${nextVersion}"`, { cwd: repoRoot, stdio: 'inherit' }) - execSync(`git push -u origin "${bumpBranch}"`, { cwd: repoRoot, stdio: 'inherit' }) - - const bodyFile = join(tmpdir(), `pr-body-${Date.now()}.md`) - writeFileSync(bodyFile, body) - try { - execSync(`gh pr create --title "${title}" --body-file "${bodyFile}" --base "${baseBranch}" --head "${bumpBranch}"`, { - cwd: repoRoot, - stdio: 'inherit', - }) - } finally { - unlinkSync(bodyFile) - } -} - -main().catch((err) => { console.error(err.message); process.exit(1) }) diff --git a/ci/src/release/promote-package-version.ts b/ci/src/release/promote-package-version.ts deleted file mode 100644 index b31df981e8..0000000000 --- a/ci/src/release/promote-package-version.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { execSync } from 'child_process' -import path from 'path' - -const releaseTag = process.env.RELEASE_TAG! -const promoteToStable = process.env.PROMOTE_TO_STABLE === 'true' -const dryRun = process.env.DRY_RUN === 'true' -const repoRoot = process.env.REPO_ROOT ?? path.resolve(__dirname, '../../..') - -if (!promoteToStable) { - console.log('Not promoting to stable — skipping') - process.exit(0) -} - -if (dryRun) { - console.log(`[dry-run] Would promote package.json to ${releaseTag} and push`) - process.exit(0) -} - -execSync(`npm version ${releaseTag} --no-git-tag-version`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git add package.json package-lock.json`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git commit -m "chore(release): promote to ${releaseTag}"`, { cwd: repoRoot, stdio: 'inherit' }) -execSync(`git push`, { cwd: repoRoot, stdio: 'inherit' }) -console.log(`Promoted package.json to ${releaseTag} and pushed`) diff --git a/ci/src/release/validate-version.ts b/ci/src/release/validate-version.ts deleted file mode 100644 index 24520e0c3e..0000000000 --- a/ci/src/release/validate-version.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { validateMinorVersion } from './version' - -const minorVersion = process.env.MINOR_VERSION ?? '' - -if (!validateMinorVersion(minorVersion)) { - console.error(`Invalid minor version: "${minorVersion}". Expected format: v1.4`) - process.exit(1) -} - -console.log(`Minor version "${minorVersion}" is valid`) diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index c7b131a5af..75d2e8187d 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -1,4 +1,10 @@ import { + computeDevVersion, + computeNextRcTag, + computeReleaseBranchName, + computeStableTag, + highestStableTag, + highestTag, cycleStartVersion, incrementRc, isHighestStableTag, @@ -211,3 +217,103 @@ describe('isHighestStableTag', () => { expect(isHighestStableTag('v1.4.0', ['v1.4.0-rc.1'])).toBe(true) }) }) + +describe('computeStableTag', () => { + it('promotes the highest RC tag to stable', () => { + expect(computeStableTag(['v6.1.0-rc.3', 'v6.1.0-rc.2', 'v6.1.0-rc.1'])).toBe('v6.1.0') + }) + + it('handles a single RC tag', () => { + expect(computeStableTag(['v6.1.1-rc.1'])).toBe('v6.1.1') + }) + + it('ignores stable tags when finding the RC to promote', () => { + expect(computeStableTag(['v6.1.0', 'v6.1.1-rc.1'])).toBe('v6.1.1') + }) + + it('throws when no RC tags exist', () => { + expect(() => computeStableTag([])).toThrow() + expect(() => computeStableTag(['v6.1.0'])).toThrow() + }) +}) + +describe('computeNextRcTag', () => { + it('increments the RC counter when RC tags exist on the branch', () => { + const tags = ['v6.1.0-rc.1', 'v6.1.0-rc.2', 'v5.1.0'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.0-rc.3') + }) + + it('starts at rc.1 when no tags exist at all', () => { + expect(computeNextRcTag([], 'releases/v6.1')).toBe('v6.1.0-rc.1') + }) + + it('starts at rc.1 when branch has no tags in its own series', () => { + const tags = ['v5.1.0', 'v6.0.0-rc.8'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.0-rc.1') + }) + + it('handles patch-level RC after a stable has been cut', () => { + const tags = ['v6.1.0', 'v6.1.1-rc.1'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.2') + }) + + it('ignores RC tags from other series on the same branch', () => { + const tags = ['v6.1.0-rc.3', 'v6.0.0-rc.8'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.0-rc.4') + }) +}) + +describe('computeDevVersion', () => { + it.each([ + ['v6.0.0-rc.8', 'abc1234', '6.0.1-dev.abc1234'], + ['v6.0.0', 'abc1234', '6.0.1-dev.abc1234'], + ['v5.1.0', 'deadbee', '5.1.1-dev.deadbee'], + ['v4.15.4', 'cafe000', '4.15.5-dev.cafe000'], + ] as const)('computeDevVersion(%s, %s) → %s', (tag, sha, expected) => { + expect(computeDevVersion(tag, sha)).toBe(expected) + }) +}) + +describe('highestTag', () => { + it('returns the highest tag including RCs', () => { + expect(highestTag(['v5.1.0', 'v6.0.0-rc.8', 'v6.0.0-rc.1'])).toBe('v6.0.0-rc.8') + }) + + it('returns a stable tag when it is the highest overall', () => { + expect(highestTag(['v6.0.0', 'v5.1.0', 'v6.0.0-rc.8'])).toBe('v6.0.0') + }) + + it('returns null on empty list', () => { + expect(highestTag([])).toBeNull() + }) +}) + +describe('highestStableTag', () => { + it('returns the highest stable tag ignoring RCs', () => { + expect(highestStableTag(['v5.1.0', 'v5.0.1', 'v6.0.0-rc.8'])).toBe('v5.1.0') + }) + + it('returns null when only RC tags exist', () => { + expect(highestStableTag(['v6.0.0-rc.8', 'v6.0.0-rc.1'])).toBeNull() + }) + + it('returns null on empty list', () => { + expect(highestStableTag([])).toBeNull() + }) + + it('handles a single stable tag', () => { + expect(highestStableTag(['v1.0.0'])).toBe('v1.0.0') + }) +}) + +describe('computeReleaseBranchName', () => { + it.each([ + ['v5.1.0', 'minor', 'releases/v5.2'], + ['v5.1.0', 'major', 'releases/v6.0'], + ['v6.0.0', 'minor', 'releases/v6.1'], + ['v4.15.4', 'minor', 'releases/v4.16'], + ['v4.15.4', 'major', 'releases/v5.0'], + ] as const)('computeReleaseBranchName(%s, %s) → %s', (tag, bump, expected) => { + expect(computeReleaseBranchName(tag, bump)).toBe(expected) + }) +}) diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index 18f0bd818a..db2f74b36b 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -78,7 +78,47 @@ export function previousRcTag(currentTag: string, tags: string[]): string | null return sameSeries.length > 0 ? sameSeries[0] : null } +export function computeStableTag(branchTags: string[]): string { + const rcs = branchTags.filter((t) => t.includes('-rc.')).sort((a, b) => semver.rcompare(a, b)) + if (rcs.length === 0) throw new Error('No RC tags on branch — cannot promote to stable without a prior RC') + return `v${promoteToStable(stripV(rcs[0]))}` +} + +export function computeNextRcTag(branchTags: string[], branchName: string): string { + const [major, minor] = stripV(branchName.replace('releases/', '')).split('.') + const seriesRcs = branchTags + .filter((t) => t.includes('-rc.')) + .filter((t) => { + const [m, n] = stripV(t).split('-rc.')[0].split('.') + return m === major && n === minor + }) + .sort((a, b) => semver.rcompare(a, b)) + if (seriesRcs.length === 0) return `v${major}.${minor}.0-rc.1` + return `v${incrementRc(stripV(seriesRcs[0]))}` +} + +export function computeDevVersion(highestTag: string, shortSha: string): string { + const [major, minor, patch] = promoteToStable(stripV(highestTag)).split('.').map(Number) + return `${major}.${minor}.${patch + 1}-dev.${shortSha}` +} + +export function highestTag(tags: string[]): string | null { + const valid = tags.filter((t) => semver.valid(t)).sort((a, b) => semver.rcompare(a, b)) + return valid.length > 0 ? valid[0] : null +} + +export function highestStableTag(tags: string[]): string | null { + const stable = tags.filter((t) => !t.includes('-rc.')).sort((a, b) => semver.rcompare(a, b)) + return stable.length > 0 ? stable[0] : null +} + export function isHighestStableTag(newTag: string, existingTags: string[]): boolean { const stableTags = existingTags.filter((t) => !t.includes('-rc.')) return stableTags.every((t) => semver.gt(newTag, t)) } + +export function computeReleaseBranchName(tag: string, bumpType: 'minor' | 'major'): string { + const [major, minor] = stripV(tag).split('.').map(Number) + const [newMajor, newMinor] = bumpType === 'major' ? [major + 1, 0] : [major, minor + 1] + return `releases/v${newMajor}.${newMinor}` +} diff --git a/docs/adr/0001-git-tags-as-version-source-of-truth.md b/docs/adr/0001-git-tags-as-version-source-of-truth.md new file mode 100644 index 0000000000..e972ef3480 --- /dev/null +++ b/docs/adr/0001-git-tags-as-version-source-of-truth.md @@ -0,0 +1,18 @@ +# Git tags as the sole version source of truth + +Git tags are the canonical version for every build and release. `package.json` permanently holds `0.0.0` and is never updated by release workflows. The correct version is computed from git tags at build time and injected into the image via `ARG VERSION` in the Dockerfile. + +## Why + +The previous approach stored the version in `package.json` and committed it back to the branch on every RC bump and stable promotion. This created several problems: the file could drift from the actual tags, the commit created a chicken-and-egg dependency between the build (which needed the version) and the tag (which was created after the build), and it added noise commits to the release branch history with no informational value beyond what the tag already expressed. + +## Considered options + +- **`package.json` as source of truth** (previous approach) — version committed to branch, read by build and scripts. Rejected because of drift risk, spurious commits, and the circular dependency between build and tag. +- **`git describe` at container startup** — version resolved at runtime. Rejected because it requires shipping git into the container and cannot be baked into artifacts (Helm chart, GitHub release metadata) without an extra indirection. + +## Consequences + +- `package.json` always shows `0.0.0`. This is intentional. Do not "fix" it. +- All CI scripts that previously read `pkg.version` must instead query git tags or receive the version as an environment variable. +- `release-from-branch` is the only workflow permitted to create version tags, and only from `releases/*` branches. diff --git a/package.json b/package.json index e7260683a5..640c812285 100644 --- a/package.json +++ b/package.json @@ -170,5 +170,5 @@ } }, "type": "commonjs", - "version": "6.0.0-rc.2" + "version": "0.0.0" } From 42f6875b2c0748d8a5562c5febfea7413fc8708f Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:36:20 +0200 Subject: [PATCH 22/24] chore: ci/package-lock.json --- ci/package-lock.json | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/ci/package-lock.json b/ci/package-lock.json index 7c24fab7f1..3cc7592560 100644 --- a/ci/package-lock.json +++ b/ci/package-lock.json @@ -548,9 +548,9 @@ "license": "MIT" }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -1943,6 +1943,40 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", From 2d2c38a2d07e130011a873687c74dc62bddb5438 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:37:49 +0200 Subject: [PATCH 23/24] chore: ci/package-lock.json --- ci/package-lock.json | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/ci/package-lock.json b/ci/package-lock.json index 3cc7592560..55fdcd1ca3 100644 --- a/ci/package-lock.json +++ b/ci/package-lock.json @@ -61,7 +61,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -547,6 +546,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -554,6 +578,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2262,7 +2287,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3201,7 +3225,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -4842,7 +4865,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From eb1558a96fb374cfdad06869a5ebc4c275e12591 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:39:09 +0200 Subject: [PATCH 24/24] ci: rework --- ci/src/release/configure-git.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/src/release/configure-git.ts b/ci/src/release/configure-git.ts index b4ceacc2cf..aa053b90c0 100644 --- a/ci/src/release/configure-git.ts +++ b/ci/src/release/configure-git.ts @@ -5,4 +5,4 @@ const username = process.env.BOT_USERNAME! execSync(`git config --global user.email "${email}"`, { stdio: 'inherit' }) execSync(`git config --global user.name "${username}"`, { stdio: 'inherit' }) -console.log(`Git identity configured: ${username} <${email}>`) +console.log(`Git identity configured.`)