From 6103c6e2a6f7cd1fad5008fdf676c11e3d9c1e8e Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Thu, 29 Oct 2026 22:00:31 +0100 Subject: [PATCH 1/4] feat(release): orchestrate single-package and Lerna releases --- .github/workflows/__shared-ci.yml | 15 + .github/workflows/release.md | 363 +- .github/workflows/release.yml | 228 + README.md | 10 +- actions/release/ReleaseArtifacts.js | 129 + actions/release/ReleaseProject.js | 275 + actions/release/finalize/README.md | 79 + actions/release/finalize/action.yml | 55 + actions/release/prepare/README.md | 81 + actions/release/prepare/action.yml | 91 + actions/release/restore-package/README.md | 78 + actions/release/restore-package/action.yml | 35 + docs/README.md | 6 + docs/adr/0001-release-orchestration.md | 60 + docs/development/release-tests.md | 23 + docs/usage/custom-releases.md | 251 + docs/usage/examples/reactionable-cli.yml | 62 + docs/usage/examples/reactionable.yml | 54 + docs/usage/releases.md | 157 + tests/release/artifacts.test.js | 199 + tests/release/package-lock.json | 5499 ++++++++++++++++++++ tests/release/package.json | 10 + tests/release/project.test.js | 462 ++ 23 files changed, 7965 insertions(+), 257 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 actions/release/ReleaseArtifacts.js create mode 100644 actions/release/ReleaseProject.js create mode 100644 actions/release/finalize/README.md create mode 100644 actions/release/finalize/action.yml create mode 100644 actions/release/prepare/README.md create mode 100644 actions/release/prepare/action.yml create mode 100644 actions/release/restore-package/README.md create mode 100644 actions/release/restore-package/action.yml create mode 100644 docs/README.md create mode 100644 docs/adr/0001-release-orchestration.md create mode 100644 docs/development/release-tests.md create mode 100644 docs/usage/custom-releases.md create mode 100644 docs/usage/examples/reactionable-cli.yml create mode 100644 docs/usage/examples/reactionable.yml create mode 100644 docs/usage/releases.md create mode 100644 tests/release/artifacts.test.js create mode 100644 tests/release/package-lock.json create mode 100644 tests/release/package.json create mode 100644 tests/release/project.test.js diff --git a/.github/workflows/__shared-ci.yml b/.github/workflows/__shared-ci.yml index a164873..f3be608 100644 --- a/.github/workflows/__shared-ci.yml +++ b/.github/workflows/__shared-ci.yml @@ -6,6 +6,21 @@ on: permissions: {} jobs: + test-release: + name: Test release preparation and recovery + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: $/actions/setup-node + with: + working-directory: tests/release + - run: npm test + working-directory: tests/release + linter: uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@44f6108cef0e9884ea7735f3d62932c2310745d4 # 0.39.1 permissions: diff --git a/.github/workflows/release.md b/.github/workflows/release.md index 267919b..3d1bd3e 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -1,261 +1,112 @@ -# Release a Node.js package - -Publish a package to npm and attach the same tarball to a GitHub release: - -**Plan version → Run CI → Package build output → Publish to npm → Create GitHub release** - -The workflow below releases a single package from the repository root using stable versions such as `1.2.3`. -It sets the version during packaging. If your build reads the package version or you commit version changes, -use [Commit the version before building](#commit-the-version-before-building). - -## Set up - -1. Configure [release labels and version rules](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/prepare-release.md). - These determine the version and release notes. -2. Check the package's `package.json`: set `name`, `files`, and `repository.url`, and ensure `private` is not `true`. -3. Configure [npm trusted publishing](../../actions/publish/README.md#npm-trusted-publishing) for your repository and workflow filename, `release.yml`. - The example uses GitHub-hosted runners and `id-token: write`; no npm token is needed. - For a new npm package, publish its first version before configuring the trusted publisher. -4. Save the workflow below as `.github/workflows/release.yml` in your project. Before running it: - - Replace every `` with the **same `ci-github-nodejs` commit containing `actions/publish`**. - The `ci-github-publish` actions are already pinned to `1.0.3`. - - Set the `build`, `lint:ci`, and `test:ci` script names and the `dist/` output path to match your project. - See [CI options](continuous-integration.md) if you need a different setup. - -## Workflow - -```yaml -name: Release - -on: - schedule: - - cron: "25 8 * * 1" - workflow_dispatch: - inputs: - force: - description: Release even when no relevant changes are detected - type: boolean - required: false - default: false - -permissions: {} - -concurrency: - group: release-${{ github.repository }} - cancel-in-progress: false - -jobs: - plan: - if: github.ref_name == github.event.repository.default_branch - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - should-release: ${{ steps.plan.outputs.has-changes == 'true' || (github.event_name == 'workflow_dispatch' && inputs.force) }} - tag: ${{ steps.plan.outputs.tag }} - version: ${{ steps.version.outputs.version }} - steps: - - name: Plan release - id: plan - uses: hoverkraft-tech/ci-github-publish/actions/release/plan@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 - - - id: version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - RELEASE_TAG: ${{ steps.plan.outputs.tag }} - with: - script: | - // This example uses tags such as v1.2.3 or 1.2.3. - const version = process.env.RELEASE_TAG.replace(/^v/, ''); - if (!/^\d+\.\d+\.\d+$/.test(version)) { - return core.setFailed('Expected a stable SemVer tag; adapt the mapping for prereleases or monorepos'); - } - core.setOutput('version', version); - - ci: - needs: plan - if: needs.plan.outputs.should-release == 'true' - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@ - permissions: - contents: read - packages: read - pull-requests: write - id-token: write - security-events: write - with: - build: '{"commands":["build"],"artifact":{"paths":["dist/"]}}' - lint: '{"command":"lint:ci"}' - test: '{"command":"test:ci"}' - - release: - needs: [plan, ci] - if: needs.plan.outputs.should-release == 'true' - runs-on: ubuntu-latest - permissions: - actions: read - contents: write - pull-requests: read - id-token: write - steps: - - name: Package build output - id: package - uses: hoverkraft-tech/ci-github-nodejs/actions/package@ - with: - version: ${{ needs.plan.outputs.version }} - build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} - # The reusable CI workflow preserves absolute paths in build artifacts. - build-artifact-path: / - - # Add any package smoke tests before publishing. - - name: Check package publishing - uses: hoverkraft-tech/ci-github-nodejs/actions/publish@ - with: - package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} - dry-run: "true" - provenance: "false" - - # Wrap the raw .tgz in a ZIP artifact for release/create. - - id: release-assets - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: release-assets - path: ${{ steps.package.outputs.package-tarball-path }} - if-no-files-found: error - - - id: publish-package - name: Publish package - uses: hoverkraft-tech/ci-github-nodejs/actions/publish@ - with: - package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} - tag: latest - - - name: Create GitHub release - uses: hoverkraft-tech/ci-github-publish/actions/release/create@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 - with: - tag: ${{ needs.plan.outputs.tag }} - target-sha: ${{ github.sha }} - release-artifact-id: ${{ steps.release-assets.outputs.artifact-id }} - publish: "true" -``` - -[Package](../../actions/package/README.md) checks out the source, installs dependencies, restores the CI build output, -and creates the tarball. [Publish](../../actions/publish/README.md) publishes that tarball. -The package version is changed locally; it is not committed. GitHub's source archive therefore keeps the version from the source commit. -If `package.json` already contains the planned version, omit the Package action's `version` input. - -## Generate the changelog - -`release/create` generates the GitHub release notes with Release Drafter from the merged pull requests since the previous matching release. -No separate changelog command is needed for the workflow above. `release/plan` calculates the version without creating a release. - -Use the [prepare-release workflow](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/prepare-release.md) -to label pull requests from their Conventional Commit titles. Its default rules select a major version for breaking changes, -a minor version for features, and a patch version for other changes. Check the labels before merging. -To customize categories, version rules, or the release-note template, add `.github/release-config.yml`. -For monorepos, use the same `working-directory` and `include-paths` values in `release/plan` and `release/create`; -an existing repository configuration takes precedence over those generated defaults. - -### Add an AI summary - -Store the provider credential as the `OPENAI_API_KEY` Actions secret, then add these entries to the **Create GitHub release** step. -Merge `changelog-summary` into its existing `with` mapping: - -```yaml -env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} -with: - changelog-summary: | - { - "llmAuth": ${{ toJSON(env.OPENAI_API_KEY) }}, - "llmProvider": "openai" - } -``` - -The action drafts the changelog, generates a release summary and breaking-change section, and prepends them to the full changelog before publishing. -The summary also uses GitHub evidence, so keep `contents: write` and `pull-requests: read` on the release job. -Use `toJSON` to encode the credential safely. Omit `changelog-summary` to publish the generated notes without an AI summary. -See [summarizer inputs](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/actions/release/summarize-changelog/README.md) -for `llmModel`, `llmConfig`, and custom summary templates. - -To review the notes before publication, set `publish: "false"`, review the draft, then publish it with `release/update`. -If summary generation fails, the action does not publish the GitHub release. The npm package may already be published; -follow [Recover a failed release](#recover-a-failed-release) instead of rerunning npm publication. - -### Keep a changelog in the repository - -Release notes are stored in the GitHub release body; these actions do not write or commit `CHANGELOG.md`. -If the source archive or npm tarball must contain it, generate and review the changelog on the release branch, -commit it with the version update, and build from that merged commit as described in [Commit the version before building](#commit-the-version-before-building). -Keep the planned tag fixed through preparation and publication. - -## Run a release - -In **Actions → Release → Run workflow**, select the default branch. The workflow also runs every Monday at 08:25 UTC. -Releases are serialized so two runs cannot publish the same package concurrently. - -| Situation | Result | -| ---------------------------------------------- | ---------------------------------------------------------------------------------- | -| Relevant changes since the last GitHub release | Plan the next version, run CI, and release. | -| No relevant changes | Skip CI and publishing successfully. | -| Manual run with `force` checked | Release the planned version even without changes; CI must still pass. | -| No previous matching GitHub release | Plan the first GitHub release. Registry authentication must already be configured. | - -The dry run checks the tarball with `npm publish --dry-run`. It does not verify registry credentials or trusted publishing access. -For a first trial without publishing, keep the dry-run step and remove the **Publish package** and **Create GitHub release** steps. - -## Commit the version before building - -Use this when the build embeds the version or the released source must include updated version files or a changelog: - -1. Plan the version with `release/plan`, using the same no-change and `force` rules as the workflow above. -2. Update the version on a release branch, for example with `npm version 1.2.3 --no-git-tag-version`. - Commit the package manifest, lockfile, changelog, and any generated version files through a pull request. -3. After merging, run CI and packaging **from the merged commit**. Omit the Package action's `version` input; the version is already in `package.json`. -4. Publish the tarball, then create the GitHub release with the planned `tag` and merged commit as `target-sha`. - -Keep the planned tag throughout this process; do not calculate another version after merging. -If you use a second workflow for publication, read the committed version instead of calling `release/plan` again, -and skip it when that version is already released. - -The Package action performs its own checkout of the workflow's source ref. -Checking out the merged commit in an earlier step does not change that ref: start the packaging workflow at the merged commit. -See the [release preparation example](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md#example-3-validate-source-update-files-create-release-artifacts-and-release) -for automating the pull request and merge. - -## Other setups - -- **GitHub Packages or registry tokens:** use the [authentication examples](../../actions/publish/README.md#registry-tokens-and-github-packages). - The `github-token` input only authenticates artifact downloads; registry credentials use `NODE_AUTH_TOKEN`. -- **Prereleases:** set `prerelease: "true"` on `release/plan` and `release/create`, and `tag: next` on both Publish steps. - Update the version conversion to accept your full prerelease version, such as `1.2.3-rc.1`. - The npm distribution tag (`next`) is separate from the Git release tag (`v1.2.3-rc.1`). -- **Monorepos:** scope CI and both release actions with `working-directory`. - For Package, set `working-directory` to the dependency installation root and `package-directory` to the package's relative path. - Convert prefixed Git tags such as `my-package/v1.2.3` to npm versions, and use a separate concurrency group and artifact name per package. -- **Drafts and signing:** create a draft with `publish: "false"`, then use `release/update` to attach assets and publish after npm publication succeeds. - See the [draft release example](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md#example-2-validate-source-create-release-artifacts-and-release). - Use `release/delete` with `draft-only: "true"` to clean up a temporary draft on failure **only if the npm package has not been published**. + -## Recover a failed release +# GitHub Reusable Workflow: Node.js Release -Check the registry before retrying: an npm package version cannot be overwritten, and a failed run may still have published it. +
+ Node.js Release +
-| Where it failed | What to do | -| ------------------------------------------------- | ------------------------------------------------------------------------ | -| Before npm publication | Fix the failure and rerun the workflow. | -| npm published, but GitHub release creation failed | Run only release creation with the original tag, source SHA, and assets. | -| npm published, but a draft was not finalized | Keep the draft and retry `release/update`. | - -After npm publication, do not rerun the whole release job or plan a new version to finish the same release. -Deleting a draft does not undo npm publication. - -## Migrate from the reusable release workflow - -Replace calls to `ci-github-nodejs/.github/workflows/release.yml` with a job that uses [the Publish action](../../actions/publish/README.md#usage): +--- -- Move `runs-on` and permissions to the caller job. -- Move the publishing inputs to the action step, quoting `provenance` and `dry-run` as `"true"` or `"false"`. -- Move the optional `github-token` secret to the action's `github-token` input. + + -Use the workflow above when you also need version planning, CI, and a GitHub release. +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Release a single npm package or fixed-version Lerna workspaces without consumer scripts. + +Keep workflow_dispatch and schedule triggers in the calling workflow. +Release Please prepares a release PR; merge it and run again to publish. +Lerna prepares and atomically pushes versions and changelogs directly. +Preparation dispatches the caller workflow at the release tag so CI, packaging +and npm provenance all refer to the same committed source. +See [the release guide](../../docs/usage/releases.md) for setup, examples and recovery. + +### Permissions + +- **`actions`**: `write` +- **`contents`**: `write` +- **`id-token`**: `write` +- **`issues`**: `write` +- **`packages`**: `read` +- **`pull-requests`**: `write` +- **`security-events`**: `write` + + + + +## Inputs + +### Workflow Call Inputs + +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -------------------------------------------- | +| **`versioning`** | Version preparation tool. Use release-please for a single public package or lerna for fixed-version workspaces. Versions are inferred from Conventional Commits. | **false** | **string** | `release-please` | +| **`working-directory`** | Project directory relative to the caller repository root. | **false** | **string** | `.` | +| **`build`** | Build commands and artifact paths, using the continuous-integration workflow's build format. Artifacts must include the package's compiled files. | **false** | **string** | `{"commands":["build"],"artifact":["dist"]}` | +| **`lint`** | Lint options forwarded to continuous-integration. Set to an empty string to disable. | **false** | **string** | `true` | +| **`test`** | Test options forwarded to continuous-integration. Set to an empty string to disable. | **false** | **string** | `true` | +| **`code-ql`** | CodeQL options forwarded to continuous-integration. Set to an empty string to disable. | **false** | **string** | `typescript` | +| **`ai-summary`** | Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. | **false** | **boolean** | `false` | +| **`environment`** | GitHub environment for source preparation, AI and npm publishing. It must permit the default branch and release tags. Configure npm trusted publishing for this environment and the calling workflow. | **false** | **string** | `release` | + + + + +## Secrets + +| **Secret** | **Description** | **Required** | +| -------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------ | +| **`OPENAI_API_KEY`** | OpenAI API key, required when ai-summary is enabled. | **false** | +| **`github-token`** | Optional GitHub App token for release PR CI. Defaults to GITHUB_TOKEN; see the guide for its PR event restrictions. | **false** | + + + + +## Outputs + +| **Output** | **Description** | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| **`status`** | no-changes, awaiting-review, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. | +| **`tag`** | Prepared release tag. | +| **`release-url`** | Published GitHub release URL from the tag run. | +| **`build-artifact-id`** | CI build artifact ID from the tag run, reusable for documentation deployment. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft-tech + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f1c7313 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,228 @@ +# Release a single npm package or fixed-version Lerna workspaces without consumer scripts. +# +# Keep workflow_dispatch and schedule triggers in the calling workflow. +# Release Please prepares a release PR; merge it and run again to publish. +# Lerna prepares and atomically pushes versions and changelogs directly. +# Preparation dispatches the caller workflow at the release tag so CI, packaging +# and npm provenance all refer to the same committed source. +# See [the release guide](../../docs/usage/releases.md) for setup, examples and recovery. +name: Node.js Release + +on: + workflow_call: + inputs: + versioning: + description: Version preparation tool. Use release-please for a single public package or lerna for fixed-version workspaces. Versions are inferred from Conventional Commits. + type: string + default: release-please + working-directory: + description: Project directory relative to the caller repository root. + type: string + default: . + build: + description: Build commands and artifact paths, using the continuous-integration workflow's build format. Artifacts must include the package's compiled files. + type: string + default: '{"commands":["build"],"artifact":["dist"]}' + lint: + description: Lint options forwarded to continuous-integration. Set to an empty string to disable. + type: string + default: "true" + test: + description: Test options forwarded to continuous-integration. Set to an empty string to disable. + type: string + default: "true" + code-ql: + description: CodeQL options forwarded to continuous-integration. Set to an empty string to disable. + type: string + default: typescript + ai-summary: + description: Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. + type: boolean + default: false + environment: + description: GitHub environment for source preparation, AI and npm publishing. It must permit the default branch and release tags. Configure npm trusted publishing for this environment and the calling workflow. + type: string + default: release + secrets: + OPENAI_API_KEY: + description: OpenAI API key, required when ai-summary is enabled. + required: false + github-token: + description: Optional GitHub App token for release PR CI. Defaults to GITHUB_TOKEN; see the guide for its PR event restrictions. + required: false + outputs: + status: + description: no-changes, awaiting-review, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. + value: ${{ jobs.publish.outputs.status || jobs.prepare.outputs.status }} + tag: + description: Prepared release tag. + value: ${{ jobs.prepare.outputs.tag }} + release-url: + description: Published GitHub release URL from the tag run. + value: ${{ jobs.publish.outputs.url }} + build-artifact-id: + description: CI build artifact ID from the tag run, reusable for documentation deployment. + value: ${{ jobs.ci.outputs.build-artifact-id }} + +permissions: {} + +concurrency: + group: node-release-${{ github.repository }}-${{ inputs.working-directory }} + cancel-in-progress: false + +jobs: + prepare: + name: Prepare release + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: ${{ inputs.environment }} + permissions: + actions: write + contents: write + issues: write + pull-requests: write + outputs: + status: ${{ steps.prepare.outputs.status }} + tag: ${{ steps.prepare.outputs.tag }} + packages: ${{ steps.prepare.outputs.packages }} + published: ${{ steps.prepare.outputs.published }} + steps: + - name: Validate AI configuration + if: inputs.ai-summary + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + if [ -z "$OPENAI_API_KEY" ]; then + echo "OPENAI_API_KEY is required when ai-summary is enabled." >&2 + exit 1 + fi + - id: prepare + uses: $/actions/release/prepare + with: + versioning: ${{ inputs.versioning }} + working-directory: ${{ inputs.working-directory }} + github-token: ${{ secrets.github-token || github.token }} + + ci: + needs: prepare + if: needs.prepare.outputs.status == 'ready' + uses: $/.github/workflows/continuous-integration.yml + permissions: + contents: read + packages: read + pull-requests: write + id-token: write + security-events: write + with: + working-directory: ${{ inputs.working-directory }} + build: ${{ inputs.build }} + lint: ${{ inputs.lint }} + test: ${{ inputs.test }} + code-ql: ${{ inputs.code-ql }} + + draft: + name: Draft release notes + needs: [prepare, ci] + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: ${{ inputs.environment }} + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Create draft and optional AI summary + if: needs.prepare.outputs.published != 'true' + uses: hoverkraft-tech/ci-github-publish/actions/release/create@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + with: + tag: ${{ needs.prepare.outputs.tag }} + target-sha: ${{ github.sha }} + working-directory: ${{ inputs.working-directory != '.' && inputs.working-directory || '' }} + publish: "false" + changelog-summary: ${{ inputs.ai-summary && format('{{"llmAuth":{0},"llmProvider":"openai"}}', toJSON(env.OPENAI_API_KEY)) || '' }} + + packages: + name: Publish ${{ matrix.package.name }} + needs: [prepare, ci, draft] + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: ${{ inputs.environment }} + permissions: + actions: read + contents: read + id-token: write + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.prepare.outputs.packages) }} + steps: + - name: Package the tested release commit + id: package + uses: $/actions/package + with: + working-directory: ${{ inputs.working-directory }} + package-directory: ${{ matrix.package.directory }} + build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} + build-artifact-path: / + artifact-name: npm-${{ matrix.package.slug }}-${{ github.run_attempt }} + - name: Validate publication + uses: $/actions/publish + with: + package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} + dry-run: "true" + provenance: "false" + - id: registry + uses: $/actions/release/restore-package + with: + name: ${{ matrix.package.name }} + version: ${{ matrix.package.version }} + tarball-path: ${{ steps.package.outputs.package-tarball-path }} + - name: Publish missing npm version + if: steps.registry.outputs.published != 'true' + uses: $/actions/publish + with: + package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} + tag: latest + - name: Prepare release asset + env: + PACKAGE_TARBALL: ${{ steps.package.outputs.package-tarball-path }} + ASSET_NAME: ${{ matrix.package.slug }}-${{ matrix.package.version }}.tgz + run: | + mkdir -p "$RUNNER_TEMP/release-assets" + cp "$PACKAGE_TARBALL" "$RUNNER_TEMP/release-assets/$ASSET_NAME" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-assets-${{ matrix.package.slug }} + path: ${{ runner.temp }}/release-assets/*.tgz + overwrite: true + if-no-files-found: error + + publish: + name: Publish GitHub release + needs: [prepare, packages] + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: ${{ inputs.environment }} + permissions: + contents: write + issues: write + pull-requests: read + outputs: + url: ${{ steps.finalize.outputs.url }} + status: ${{ steps.finalize.outcome == 'success' && 'published' || '' }} + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: release-assets-* + merge-multiple: true + path: ${{ runner.temp }}/release-assets + - id: finalize + uses: $/actions/release/finalize + with: + tag: ${{ needs.prepare.outputs.tag }} + asset-directory: ${{ runner.temp }}/release-assets + versioning: ${{ inputs.versioning }} diff --git a/README.md b/README.md index 007f1a3..f0cfbda 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ _Actions for continuous integration steps: build, lint, and test._ #### - [Publish](actions/publish/README.md) +### Release Actions + +- [Prepare a release](actions/release/prepare/README.md) +- [Restore a published package](actions/release/restore-package/README.md) +- [Finalize a release](actions/release/finalize/README.md) + ### Dependencies _Actions dedicated to caching and validating Node.js dependencies._ @@ -63,7 +69,9 @@ _Actions focused on discovering and preparing the Node.js environment._ ### Release -- [Release](.github/workflows/release.md) — guide to Node.js project releases using `ci-github-publish` release actions and the package/publish actions. +- [Release packages](docs/usage/releases.md) — single-package and Lerna releases through one reusable workflow. +- [Release workflow reference](.github/workflows/release.md) — inputs, secrets and outputs. +- [Custom release pipelines](docs/usage/custom-releases.md) — compose the package, publish and GitHub release actions. ## Contributing diff --git a/actions/release/ReleaseArtifacts.js b/actions/release/ReleaseArtifacts.js new file mode 100644 index 0000000..e7853a1 --- /dev/null +++ b/actions/release/ReleaseArtifacts.js @@ -0,0 +1,129 @@ +const { createHash } = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); + +class ReleaseArtifacts { + constructor(fetchPackage = fetch) { + this.fetchPackage = fetchPackage; + } + + static async findRelease(github, repo, tag) { + try { + const { data } = await github.rest.repos.getReleaseByTag({ + ...repo, + tag, + }); + return data; + } catch (error) { + if (error.status !== 404) throw error; + const releases = await github.paginate(github.rest.repos.listReleases, { + ...repo, + per_page: 100, + }); + return releases.find((release) => release.tag_name === tag); + } + } + + async restorePackage({ name, version, tarballPath }) { + const response = await this.fetchPackage( + `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, + ); + if (response.status === 404) return false; + if (!response.ok) + throw new Error(`npm registry lookup failed: HTTP ${response.status}`); + const metadata = await response.json(); + if (metadata.name !== name || metadata.version !== version) + throw new Error("npm registry returned an unexpected package version."); + const url = new URL(metadata.dist.tarball); + if (url.origin !== "https://registry.npmjs.org") + throw new Error( + "Published package tarball must come from the npm registry.", + ); + const tarballResponse = await this.fetchPackage(url.href); + if (!tarballResponse.ok) + throw new Error( + `npm tarball download failed: HTTP ${tarballResponse.status}`, + ); + const tarball = Buffer.from(await tarballResponse.arrayBuffer()); + const integrity = `sha512-${createHash("sha512").update(tarball).digest("base64")}`; + if (metadata.dist.integrity !== integrity) + throw new Error( + "Published package tarball failed integrity verification.", + ); + fs.writeFileSync(tarballPath, tarball); + return true; + } + + async attachAssets({ github, repo, tag, directory }) { + const release = await ReleaseArtifacts.findRelease(github, repo, tag); + if (!release) throw new Error(`No release found for ${tag}.`); + const assets = await github.paginate(github.rest.repos.listReleaseAssets, { + ...repo, + release_id: release.id, + per_page: 100, + }); + const files = fs + .readdirSync(directory) + .filter((name) => name.endsWith(".tgz")); + if (!files.length) + throw new Error("No package tarballs available to attach."); + for (const name of files) { + const data = fs.readFileSync(path.join(directory, name)); + const digest = `sha256:${createHash("sha256").update(data).digest("hex")}`; + const existing = assets.find((asset) => asset.name === name); + if (existing) { + if (existing.digest !== digest) + throw new Error( + `Release asset ${name} has a different or unavailable digest.`, + ); + continue; + } + if (!release.draft) + throw new Error(`Published release is missing expected asset ${name}.`); + await github.rest.repos.uploadReleaseAsset({ + ...repo, + release_id: release.id, + name, + data, + headers: { + "content-type": "application/gzip", + "content-length": data.length, + }, + }); + } + return release; + } + + async markReleaseRequests({ github, context }) { + const issues = await github.paginate(github.rest.issues.listForRepo, { + ...context.repo, + labels: "autorelease: pending", + state: "closed", + per_page: 100, + }); + for (const issue of issues.filter((item) => item.pull_request)) { + const { data } = await github.rest.pulls.get({ + ...context.repo, + pull_number: issue.number, + }); + if ( + data.merged_at && + data.merge_commit_sha === context.sha && + data.base.ref === context.payload.repository.default_branch + ) { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: issue.number, + labels: ["autorelease: tagged"], + }); + await github.rest.issues.removeLabel({ + ...context.repo, + issue_number: issue.number, + name: "autorelease: pending", + }); + } + } + } +} + +module.exports = { ReleaseArtifacts }; diff --git a/actions/release/ReleaseProject.js b/actions/release/ReleaseProject.js new file mode 100644 index 0000000..9407e8b --- /dev/null +++ b/actions/release/ReleaseProject.js @@ -0,0 +1,275 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); +const { ReleaseArtifacts } = require("./ReleaseArtifacts.js"); + +class ReleaseProject { + constructor({ + workspace, + workingDirectory = ".", + versioning, + github, + context, + }) { + this.workspace = fs.realpathSync(workspace); + this.directory = fs.realpathSync(path.resolve(workspace, workingDirectory)); + this.relativeDirectory = path.relative(this.workspace, this.directory); + if ( + this.relativeDirectory.startsWith("..") || + path.isAbsolute(this.relativeDirectory) + ) { + throw new Error( + "Release working directory must be inside the repository.", + ); + } + if (!["lerna", "release-please"].includes(versioning)) { + throw new Error("versioning must be lerna or release-please."); + } + this.versioning = versioning; + this.github = github; + this.context = context; + const manifest = this.readJson("package.json"); + if ( + versioning === "release-please" && + (manifest.workspaces || manifest.private) + ) { + throw new Error( + "release-please mode requires one public package; use lerna for fixed-version workspaces.", + ); + } + this.version(); + } + + readJson(file) { + return JSON.parse(fs.readFileSync(path.join(this.directory, file), "utf8")); + } + + run(command, args) { + return execFileSync(command, args, { + cwd: this.directory, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, CI: "true" }, + }).trim(); + } + + git(...args) { + return this.run("git", args); + } + + version() { + const { version } = this.readJson( + this.versioning === "lerna" ? "lerna.json" : "package.json", + ); + if ( + typeof version !== "string" || + !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version) + ) { + throw new Error( + "Releases require a stable version; independent Lerna versions and prereleases are not supported.", + ); + } + return version; + } + + packages(tag) { + const version = this.version(); + if (tag !== `v${version}`) + throw new Error("Release tag must match the committed package version."); + const candidates = + this.versioning === "lerna" + ? JSON.parse( + this.run("npm", [ + "exec", + "--no", + "--", + "lerna", + "list", + "--all", + "--json", + "--loglevel", + "silent", + ]), + ) + : [{ ...this.readJson("package.json"), location: this.directory }]; + const packages = candidates + .filter((item) => !item.private && item.version === version) + .map((item) => { + if ( + typeof item.name !== "string" || + !/^(?:@[a-z0-9-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(item.name) + ) { + throw new Error("Release packages must have valid npm names."); + } + const directory = path.relative( + this.directory, + fs.realpathSync(item.location), + ); + if (directory.startsWith("..") || path.isAbsolute(directory)) + throw new Error("Package is outside the release working directory."); + return { + name: item.name, + version, + directory: directory || ".", + slug: item.name.replace(/^@/, "").replace("/", "-"), + }; + }); + if ( + !packages.length || + new Set(packages.map((item) => item.slug)).size !== packages.length + ) { + throw new Error( + "Expected uniquely named public packages at the release version.", + ); + } + return packages; + } + + tagCommit(tag) { + try { + return this.git("rev-parse", "--verify", `${tag}^{commit}`); + } catch { + return undefined; + } + } + + async releaseRequests(state = "all") { + const issues = await this.github.paginate( + this.github.rest.issues.listForRepo, + { + ...this.context.repo, + labels: "autorelease: pending", + state, + per_page: 100, + }, + ); + const requests = []; + for (const issue of issues.filter((item) => item.pull_request)) { + const { data } = await this.github.rest.pulls.get({ + ...this.context.repo, + pull_number: issue.number, + }); + if (data.base.ref === this.context.payload.repository.default_branch) + requests.push(data); + } + return requests.sort((a, b) => + (a.merged_at || "").localeCompare(b.merged_at || ""), + ); + } + + async prepareReleasePlease() { + const requests = await this.releaseRequests(); + const merged = requests.find((request) => request.merged_at); + if (!merged) { + const open = requests.find((request) => request.state === "open"); + return { + status: open ? "awaiting-review" : "no-changes", + url: open?.html_url || "", + }; + } + this.git( + "merge-base", + "--is-ancestor", + merged.merge_commit_sha, + this.context.sha, + ); + this.git("checkout", "--detach", merged.merge_commit_sha); + const tag = `v${this.version()}`; + const existing = this.tagCommit(tag); + if (existing && existing !== merged.merge_commit_sha) + throw new Error(`Existing tag ${tag} points to another commit.`); + if (!existing) this.git("tag", tag, merged.merge_commit_sha); + this.git("push", "origin", `refs/tags/${tag}`); + return { tag }; + } + + prepareLerna() { + const branch = this.context.payload.repository.default_branch; + this.git("switch", "--force-create", branch, this.context.sha); + this.git("config", "user.name", "github-actions[bot]"); + this.git( + "config", + "user.email", + "41898282+github-actions[bot]@users.noreply.github.com", + ); + let tag = `v${this.version()}`; + if (this.tagCommit(tag) !== this.git("rev-parse", "HEAD")) { + this.run("npm", [ + "exec", + "--no", + "--", + "lerna", + "version", + "--conventional-commits", + "--no-push", + "--yes", + "--message", + "chore(release): %s", + ]); + if (this.git("rev-parse", "HEAD") === this.context.sha) + return { status: "no-changes" }; + tag = `v${this.version()}`; + } + if (this.tagCommit(tag) !== this.git("rev-parse", "HEAD")) + throw new Error("Release tag does not match the prepared commit."); + this.git( + "push", + "--atomic", + "origin", + `HEAD:refs/heads/${branch}`, + `refs/tags/${tag}`, + ); + return { tag }; + } + + async prepare({ workflowRef, findRelease }) { + const { ref, payload, repo } = this.context; + if (ref.startsWith("refs/tags/")) { + const tag = ref.slice("refs/tags/".length); + const packages = this.packages(tag); + const release = await findRelease(this.github, repo, tag); + return { + status: "ready", + tag, + packages, + published: Boolean(release && !release.draft), + }; + } + if (ref !== `refs/heads/${payload.repository.default_branch}`) { + throw new Error( + "Run releases from the default branch or an existing release tag.", + ); + } + const result = + this.versioning === "lerna" + ? this.prepareLerna() + : await this.prepareReleasePlease(); + if (!result.tag) return result; + const release = await findRelease(this.github, repo, result.tag); + if (release && !release.draft) { + if (this.versioning === "release-please") { + await new ReleaseArtifacts().markReleaseRequests({ + github: this.github, + context: { ...this.context, sha: this.git("rev-parse", "HEAD") }, + }); + } + return { ...result, status: "released", url: release.html_url }; + } + const prefix = `${repo.owner}/${repo.repo}/`; + const workflow = workflowRef.startsWith(prefix) + ? workflowRef.slice(prefix.length).split("@")[0] + : ""; + if (!/^\.github\/workflows\/[^/]+\.ya?ml$/.test(workflow)) + throw new Error( + "Cannot determine the caller workflow for publication dispatch.", + ); + await this.github.rest.actions.createWorkflowDispatch({ + ...repo, + workflow_id: workflow, + ref: result.tag, + }); + return { ...result, status: "dispatched" }; + } +} + +module.exports = { ReleaseProject }; diff --git a/actions/release/finalize/README.md b/actions/release/finalize/README.md new file mode 100644 index 0000000..dfe95a7 --- /dev/null +++ b/actions/release/finalize/README.md @@ -0,0 +1,79 @@ + + +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItdGFnIiBjb2xvcj0iYmx1ZSI+PHBhdGggZD0iTTIwLjU5IDEzLjQxbC03LjE3IDcuMTdhMiAyIDAgMCAxLTIuODMgMEwyIDEyVjJoMTBsOC41OSA4LjU5YTIgMiAwIDAgMSAwIDIuODJ6Ij48L3BhdGg+PGxpbmUgeDE9IjciIHkxPSI3IiB4Mj0iNy4wMSIgeTI9IjciPjwvbGluZT48L3N2Zz4=) GitHub Action: Finalize Node.js release + +
+ Finalize Node.js release +
+ +--- + + + + +[![Marketplace](https://img.shields.io/badge/Marketplace-finalize--node.js--release-blue?logo=github-actions)](https://github.com/marketplace/actions/finalize-node.js-release) +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Attach published npm tarballs without overwriting existing assets, publish the draft and mark its release PR as released. + + + + +## Inputs + +| **Input** | **Description** | **Required** | **Default** | +| --------------------- | -------------------------------------------------- | ------------ | --------------------- | +| **`tag`** | Committed release tag. | **true** | - | +| **`asset-directory`** | Directory containing the published npm tarballs. | **true** | - | +| **`versioning`** | Version preparation tool, release-please or lerna. | **false** | `release-please` | +| **`github-token`** | Token with contents and issues write permissions. | **false** | `${{ github.token }}` | + + + + + + +## Outputs + +| **Output** | **Description** | +| ---------- | ----------------------------- | +| **`url`** | Published GitHub release URL. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/actions/release/finalize/action.yml b/actions/release/finalize/action.yml new file mode 100644 index 0000000..53e10f6 --- /dev/null +++ b/actions/release/finalize/action.yml @@ -0,0 +1,55 @@ +name: Finalize Node.js release +description: Attach published npm tarballs without overwriting existing assets, publish the draft and mark its release PR as released. +author: hoverkraft +branding: + icon: tag + color: blue +inputs: + tag: + description: Committed release tag. + required: true + asset-directory: + description: Directory containing the published npm tarballs. + required: true + versioning: + description: Version preparation tool, release-please or lerna. + default: release-please + github-token: + description: Token with contents and issues write permissions. + default: ${{ github.token }} +outputs: + url: + description: Published GitHub release URL. + value: ${{ steps.assets.outputs.url }} +runs: + using: composite + steps: + - id: assets + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + RELEASE_TAG: ${{ inputs.tag }} + ASSET_DIRECTORY: ${{ inputs.asset-directory }} + with: + github-token: ${{ inputs.github-token }} + script: | + const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); + const release = await new ReleaseArtifacts().attachAssets({ github, repo: context.repo, tag: process.env.RELEASE_TAG, directory: process.env.ASSET_DIRECTORY }); + core.setOutput('url', release.html_url); + core.setOutput('published', String(!release.draft)); + - uses: hoverkraft-tech/ci-github-publish/actions/release/update@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + if: steps.assets.outputs.published != 'true' + with: + tag: ${{ inputs.tag }} + publish: "true" + github-token: ${{ inputs.github-token }} + - name: Complete release PR + if: inputs.versioning == 'release-please' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + with: + github-token: ${{ inputs.github-token }} + script: | + const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); + await new ReleaseArtifacts().markReleaseRequests({ github, context }); diff --git a/actions/release/prepare/README.md b/actions/release/prepare/README.md new file mode 100644 index 0000000..98c7891 --- /dev/null +++ b/actions/release/prepare/README.md @@ -0,0 +1,81 @@ + + +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZ2l0LWNvbW1pdCIgY29sb3I9ImJsdWUiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiPjwvY2lyY2xlPjxsaW5lIHgxPSIxLjA1IiB5MT0iMTIiIHgyPSI3IiB5Mj0iMTIiPjwvbGluZT48bGluZSB4MT0iMTcuMDEiIHkxPSIxMiIgeDI9IjIyLjk2IiB5Mj0iMTIiPjwvbGluZT48L3N2Zz4=) GitHub Action: Prepare Node.js release + +
+ Prepare Node.js release +
+ +--- + + + + +[![Marketplace](https://img.shields.io/badge/Marketplace-prepare--node.js--release-blue?logo=github-actions)](https://github.com/marketplace/actions/prepare-node.js-release) +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Prepare a single-package Release Please PR or a fixed-version Lerna release, and dispatch publication from the committed tag. + + + + +## Inputs + +| **Input** | **Description** | **Required** | **Default** | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | +| **`versioning`** | Version preparation tool, release-please for one package or lerna for fixed-version workspaces. | **false** | `release-please` | +| **`working-directory`** | Project directory relative to the caller repository root. | **false** | `.` | +| **`github-token`** | Token with contents, issues, pull-requests and actions write permissions. An app token allows CI to run on release PRs. | **false** | `${{ github.token }}` | + + + + + + +## Outputs + +| **Output** | **Description** | +| --------------- | ------------------------------------------------------------------------------------------------------------ | +| **`status`** | One of no-changes, awaiting-review, dispatched, ready or released. Only ready should start publication jobs. | +| **`tag`** | Stable release tag, empty when preparation has no release to publish. | +| **`packages`** | JSON array of public packages at the committed release version, with name, version, directory and slug. | +| **`published`** | Whether the tag already has a published GitHub release. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/actions/release/prepare/action.yml b/actions/release/prepare/action.yml new file mode 100644 index 0000000..f3d9871 --- /dev/null +++ b/actions/release/prepare/action.yml @@ -0,0 +1,91 @@ +name: Prepare Node.js release +description: Prepare a single-package Release Please PR or a fixed-version Lerna release, and dispatch publication from the committed tag. +author: hoverkraft +branding: + icon: git-commit + color: blue +inputs: + versioning: + description: Version preparation tool, release-please for one package or lerna for fixed-version workspaces. + default: release-please + working-directory: + description: Project directory relative to the caller repository root. + default: . + github-token: + description: Token with contents, issues, pull-requests and actions write permissions. An app token allows CI to run on release PRs. + default: ${{ github.token }} +outputs: + status: + description: One of no-changes, awaiting-review, dispatched, ready or released. Only ready should start publication jobs. + value: ${{ steps.prepare.outputs.status }} + tag: + description: Stable release tag, empty when preparation has no release to publish. + value: ${{ steps.prepare.outputs.tag }} + packages: + description: JSON array of public packages at the committed release version, with name, version, directory and slug. + value: ${{ steps.prepare.outputs.packages }} + published: + description: Whether the tag already has a published GitHub release. + value: ${{ steps.prepare.outputs.published }} +runs: + using: composite + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + token: ${{ inputs.github-token }} + # The preparation step pushes version commits and tags atomically. + persist-credentials: true + - name: Validate release project + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + VERSIONING: ${{ inputs.versioning }} + with: + script: | + const { ReleaseProject } = require(`${process.env.ACTION_PATH}/../ReleaseProject.js`); + if (context.ref !== `refs/heads/${context.payload.repository.default_branch}` && !context.ref.startsWith('refs/tags/v')) { + throw new Error('Run releases from the default branch or an existing release tag.'); + } + new ReleaseProject({ workspace: process.env.GITHUB_WORKSPACE, workingDirectory: process.env.WORKING_DIRECTORY, versioning: process.env.VERSIONING, github, context }); + - uses: $/actions/setup-node + if: inputs.versioning == 'lerna' + with: + working-directory: ${{ inputs.working-directory }} + - name: Prepare single-package release PR + if: inputs.versioning == 'release-please' && github.ref_type == 'branch' + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ inputs.github-token }} + release-type: node + path: ${{ inputs.working-directory }} + target-branch: ${{ github.event.repository.default_branch }} + include-component-in-tag: false + skip-github-release: true + - name: Prepare release source + id: prepare + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + VERSIONING: ${{ inputs.versioning }} + CALLER_WORKFLOW_REF: ${{ github.workflow_ref }} + with: + github-token: ${{ inputs.github-token }} + script: | + const { ReleaseProject } = require(`${process.env.ACTION_PATH}/../ReleaseProject.js`); + const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); + const project = new ReleaseProject({ workspace: process.env.GITHUB_WORKSPACE, workingDirectory: process.env.WORKING_DIRECTORY, versioning: process.env.VERSIONING, github, context }); + const result = await project.prepare({ workflowRef: process.env.CALLER_WORKFLOW_REF, findRelease: ReleaseArtifacts.findRelease }); + for (const [name, value] of Object.entries(result)) core.setOutput(name, value); + core.setOutput('packages', result.packages || []); + core.setOutput('published', String(Boolean(result.published))); + const messages = { + 'no-changes': 'No changes to release.', + 'awaiting-review': `Merge the release PR, then run this workflow again: ${result.url}`, + dispatched: `Publication dispatched for ${result.tag}. Follow the new workflow run for that tag.`, + ready: `Publishing committed release ${result.tag}.`, + released: `${result.tag} is already released.`, + }; + await core.summary.addRaw(messages[result.status]).write(); diff --git a/actions/release/restore-package/README.md b/actions/release/restore-package/README.md new file mode 100644 index 0000000..7178e8b --- /dev/null +++ b/actions/release/restore-package/README.md @@ -0,0 +1,78 @@ + + +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItcGFja2FnZSIgY29sb3I9ImJsdWUiPjxsaW5lIHgxPSIxNi41IiB5MT0iOS40IiB4Mj0iNy41IiB5Mj0iNC4yMSI+PC9saW5lPjxwYXRoIGQ9Ik0yMSAxNlY4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNnoiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIzLjI3IDYuOTYgMTIgMTIuMDEgMjAuNzMgNi45NiI+PC9wb2x5bGluZT48bGluZSB4MT0iMTIiIHkxPSIyMi4wOCIgeDI9IjEyIiB5Mj0iMTIiPjwvbGluZT48L3N2Zz4=) GitHub Action: Restore published Node.js package + +
+ Restore published Node.js package +
+ +--- + + + + +[![Marketplace](https://img.shields.io/badge/Marketplace-restore--published--node.js--package-blue?logo=github-actions)](https://github.com/marketplace/actions/restore-published-node.js-package) +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Check an exact npm version and restore its original tarball with integrity verification when resuming publication. + + + + +## Inputs + +| **Input** | **Description** | **Required** | **Default** | +| ------------------ | ---------------------------------------------------------------------------- | ------------ | ----------- | +| **`name`** | Expected npm package name. | **true** | - | +| **`version`** | Expected committed package version. | **true** | - | +| **`tarball-path`** | Local tarball path to replace with the original published bytes on recovery. | **true** | - | + + + + + + +## Outputs + +| **Output** | **Description** | +| --------------- | ----------------------------------------------------------------------------- | +| **`published`** | Whether the exact version already exists on npm and its tarball was restored. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/actions/release/restore-package/action.yml b/actions/release/restore-package/action.yml new file mode 100644 index 0000000..4b2c891 --- /dev/null +++ b/actions/release/restore-package/action.yml @@ -0,0 +1,35 @@ +name: Restore published Node.js package +description: Check an exact npm version and restore its original tarball with integrity verification when resuming publication. +author: hoverkraft +branding: + icon: package + color: blue +inputs: + name: + description: Expected npm package name. + required: true + version: + description: Expected committed package version. + required: true + tarball-path: + description: Local tarball path to replace with the original published bytes on recovery. + required: true +outputs: + published: + description: Whether the exact version already exists on npm and its tarball was restored. + value: ${{ steps.restore.outputs.published }} +runs: + using: composite + steps: + - id: restore + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + PACKAGE_NAME: ${{ inputs.name }} + PACKAGE_VERSION: ${{ inputs.version }} + TARBALL_PATH: ${{ inputs.tarball-path }} + with: + script: | + const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); + const published = await new ReleaseArtifacts().restorePackage({ name: process.env.PACKAGE_NAME, version: process.env.PACKAGE_VERSION, tarballPath: process.env.TARBALL_PATH }); + core.setOutput('published', String(published)); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7e2ff81 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +# Documentation + +- [Release a single package or Lerna workspaces](usage/releases.md) +- [Compose a custom release pipeline](usage/custom-releases.md) +- [Validate release automation](development/release-tests.md) +- [Release orchestration decision](adr/0001-release-orchestration.md) diff --git a/docs/adr/0001-release-orchestration.md b/docs/adr/0001-release-orchestration.md new file mode 100644 index 0000000..879bcce --- /dev/null +++ b/docs/adr/0001-release-orchestration.md @@ -0,0 +1,60 @@ +# ADR 0001: Shared Node.js release orchestration + +- Status: accepted +- Date: 2026-10-29 + +## Context + +The Package and Publish actions handle a single tarball. Consumers still need +to prepare versions, select workspaces, start CI at the versioned source and +recover partial releases. Reactionable's first integration added custom scripts +for those responsibilities. A single-package consumer, reactionable-cli, uses +Release Please and has a different source preparation policy. + +The Node.js release guide at commit +`b7eadcff0bdbd690f4688e7ce3f31d7ba0e39865` documents composition of the actions, +including committing versions before building and targeted recovery. The former +reusable publish workflow was removed in 0.25; the new workflow covers source +preparation and the complete release lifecycle instead of restoring that interface. + +## Decision + +Provide an optional reusable Release workflow, retaining the composable actions. +Consumers own triggers, build options, credentials, environment rules and optional +documentation deployment. HoverKraft owns preparation, publication and recovery. + +Support one public package through Release Please and fixed-version workspaces +through the consumer's installed Lerna. Keep preparation policies explicit: +Release Please requires a merged release PR; Lerna pushes versions directly. +Both infer versions from Conventional Commits without a user-supplied version. + +Use Release Please's PR creation action with GitHub release creation disabled. +Detect merged pending release PRs explicitly, tag their merge commits and mark +them released only after npm and GitHub publication succeed. Release Please v5.0.0 +was verified on the decision date; its breaking change is the Node 24 runtime. + +Dispatch the caller workflow at the resulting tag using `GITHUB_TOKEN` or an +explicit token. The Package action performs its own checkout, so starting a new +run ensures CI, package manifests, source archives and provenance use that commit. +The workflow serializes both runs and reports when publication has been dispatched. + +Preserve successful npm publishes on retry. Restore their original tarballs, +verify registry integrity and never replace conflicting GitHub assets. Keep +published release notes intact. Reuse the existing release create/update actions +for AI notes and final publication. Do not introduce a second SemVer engine. + +## Consequences + +Consumers need no release scripts. The single-package path retains a review step; +the Lerna path requires branch rules that permit its atomic push. An app token is +needed when release PR creation must trigger normal PR CI. Neither mode bypasses +repository protection rules. + +Preparation success and publication success belong to separate runs. A tag can +remain after CI or service failure, and npm publication is not atomic across +packages. Recovery operates on the original tag and package versions. + +The initial scope is stable public npm packages on GitHub.com hosted Linux +runners. Independent workspace versions, custom registries and prereleases remain +available through custom composition. New consumer pins require the upstream +workflow implementation to be available on GitHub. diff --git a/docs/development/release-tests.md b/docs/development/release-tests.md new file mode 100644 index 0000000..2d26328 --- /dev/null +++ b/docs/development/release-tests.md @@ -0,0 +1,23 @@ +# Validate release automation + +Install the test dependencies and run the release suite: + +```sh +npm ci --prefix tests/release +npm test --prefix tests/release +``` + +The suite uses temporary local Git repositories and offline npm commands. It +executes the installed Lerna, checks dependent package and lockfile changes, and +tests no-change behavior, tag collisions and concurrent branch pushes. A single +package fixture models a merged Release Please PR, builds a CLI, packs it, installs +the tarball and executes its entry point. + +Registry and GitHub responses are mocked for partial publication, integrity +failures, draft lookup, asset recovery and release PR labels. Tests do not call +OpenAI, publish packages or create remote releases. The suite runs in the shared +CI workflow. + +Run `make lint` for repository linting. Generate changed action and workflow +readmes with the existing CI Dokumentor tool used by `release-actions.yml`; edit +metadata rather than generated files. The user guide lives in `docs/usage/`. diff --git a/docs/usage/custom-releases.md b/docs/usage/custom-releases.md new file mode 100644 index 0000000..8abf4db --- /dev/null +++ b/docs/usage/custom-releases.md @@ -0,0 +1,251 @@ +# Release a Node.js package + +Publish a package to npm and attach the same tarball to a GitHub release: + +**Plan version → Run CI → Package build output → Publish to npm → Create GitHub release** + +The workflow below releases a single package from the repository root using stable versions such as `1.2.3`. +It sets the version during packaging. If your build reads the package version or you commit version changes, +use [Commit the version before building](#commit-the-version-before-building). + +## Set up + +1. Configure [release labels and version rules](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/prepare-release.md). + These determine the version and release notes. +2. Check the package's `package.json`: set `name`, `files`, and `repository.url`, and ensure `private` is not `true`. +3. Configure [npm trusted publishing](../../actions/publish/README.md#npm-trusted-publishing) for your repository and workflow filename, `release.yml`. + The example uses GitHub-hosted runners and `id-token: write`; no npm token is needed. + For a new npm package, publish its first version before configuring the trusted publisher. +4. Save the workflow below as `.github/workflows/release.yml` in your project. Before running it: + - Replace every `` with the **same `ci-github-nodejs` commit containing `actions/publish`**. + The `ci-github-publish` actions are already pinned to `1.0.3`. + - Set the `build`, `lint:ci`, and `test:ci` script names and the `dist/` output path to match your project. + See [CI options](../../.github/workflows/continuous-integration.md) if you need a different setup. + +## Workflow + +```yaml +name: Release + +on: + schedule: + - cron: "25 8 * * 1" + workflow_dispatch: + inputs: + force: + description: Release even when no relevant changes are detected + type: boolean + required: false + default: false + +permissions: {} + +concurrency: + group: release-${{ github.repository }} + cancel-in-progress: false + +jobs: + plan: + if: github.ref_name == github.event.repository.default_branch + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + should-release: ${{ steps.plan.outputs.has-changes == 'true' || (github.event_name == 'workflow_dispatch' && inputs.force) }} + tag: ${{ steps.plan.outputs.tag }} + version: ${{ steps.version.outputs.version }} + steps: + - name: Plan release + id: plan + uses: hoverkraft-tech/ci-github-publish/actions/release/plan@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + + - id: version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RELEASE_TAG: ${{ steps.plan.outputs.tag }} + with: + script: | + // This example uses tags such as v1.2.3 or 1.2.3. + const version = process.env.RELEASE_TAG.replace(/^v/, ''); + if (!/^\d+\.\d+\.\d+$/.test(version)) { + return core.setFailed('Expected a stable SemVer tag; adapt the mapping for prereleases or monorepos'); + } + core.setOutput('version', version); + + ci: + needs: plan + if: needs.plan.outputs.should-release == 'true' + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@ + permissions: + contents: read + packages: read + pull-requests: write + id-token: write + security-events: write + with: + build: '{"commands":["build"],"artifact":{"paths":["dist/"]}}' + lint: '{"command":"lint:ci"}' + test: '{"command":"test:ci"}' + + release: + needs: [plan, ci] + if: needs.plan.outputs.should-release == 'true' + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + pull-requests: read + id-token: write + steps: + - name: Package build output + id: package + uses: hoverkraft-tech/ci-github-nodejs/actions/package@ + with: + version: ${{ needs.plan.outputs.version }} + build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} + # The reusable CI workflow preserves absolute paths in build artifacts. + build-artifact-path: / + + # Add any package smoke tests before publishing. + - name: Check package publishing + uses: hoverkraft-tech/ci-github-nodejs/actions/publish@ + with: + package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} + dry-run: "true" + provenance: "false" + + # Wrap the raw .tgz in a ZIP artifact for release/create. + - id: release-assets + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-assets + path: ${{ steps.package.outputs.package-tarball-path }} + if-no-files-found: error + + - id: publish-package + name: Publish package + uses: hoverkraft-tech/ci-github-nodejs/actions/publish@ + with: + package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} + tag: latest + + - name: Create GitHub release + uses: hoverkraft-tech/ci-github-publish/actions/release/create@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + with: + tag: ${{ needs.plan.outputs.tag }} + target-sha: ${{ github.sha }} + release-artifact-id: ${{ steps.release-assets.outputs.artifact-id }} + publish: "true" +``` + +[Package](../../actions/package/README.md) checks out the source, installs dependencies, restores the CI build output, +and creates the tarball. [Publish](../../actions/publish/README.md) publishes that tarball. +The package version is changed locally; it is not committed. GitHub's source archive therefore keeps the version from the source commit. +If `package.json` already contains the planned version, omit the Package action's `version` input. + +## Generate the changelog + +`release/create` generates the GitHub release notes with Release Drafter from the merged pull requests since the previous matching release. +No separate changelog command is needed for the workflow above. `release/plan` calculates the version without creating a release. + +Use the [prepare-release workflow](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/prepare-release.md) +to label pull requests from their Conventional Commit titles. Its default rules select a major version for breaking changes, +a minor version for features, and a patch version for other changes. Check the labels before merging. +To customize categories, version rules, or the release-note template, add `.github/release-config.yml`. +For monorepos, use the same `working-directory` and `include-paths` values in `release/plan` and `release/create`; +an existing repository configuration takes precedence over those generated defaults. + +### Add an AI summary + +Store the provider credential as the `OPENAI_API_KEY` Actions secret, then add these entries to the **Create GitHub release** step. +Merge `changelog-summary` into its existing `with` mapping: + +```yaml +env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} +with: + changelog-summary: | + { + "llmAuth": ${{ toJSON(env.OPENAI_API_KEY) }}, + "llmProvider": "openai" + } +``` + +The action drafts the changelog, generates a release summary and breaking-change section, and prepends them to the full changelog before publishing. +The summary also uses GitHub evidence, so keep `contents: write` and `pull-requests: read` on the release job. +Use `toJSON` to encode the credential safely. Omit `changelog-summary` to publish the generated notes without an AI summary. +See [summarizer inputs](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/actions/release/summarize-changelog/README.md) +for `llmModel`, `llmConfig`, and custom summary templates. + +To review the notes before publication, set `publish: "false"`, review the draft, then publish it with `release/update`. +If summary generation fails, the action does not publish the GitHub release. The npm package may already be published; +follow [Recover a failed release](#recover-a-failed-release) instead of rerunning npm publication. + +### Keep a changelog in the repository + +Release notes are stored in the GitHub release body; these actions do not write or commit `CHANGELOG.md`. +If the source archive or npm tarball must contain it, generate and review the changelog on the release branch, +commit it with the version update, and build from that merged commit as described in [Commit the version before building](#commit-the-version-before-building). +Keep the planned tag fixed through preparation and publication. + +## Run a release + +In **Actions → Release → Run workflow**, select the default branch. The workflow also runs every Monday at 08:25 UTC. +Releases are serialized so two runs cannot publish the same package concurrently. + +| Situation | Result | +| ---------------------------------------------- | ---------------------------------------------------------------------------------- | +| Relevant changes since the last GitHub release | Plan the next version, run CI, and release. | +| No relevant changes | Skip CI and publishing successfully. | +| Manual run with `force` checked | Release the planned version even without changes; CI must still pass. | +| No previous matching GitHub release | Plan the first GitHub release. Registry authentication must already be configured. | + +The dry run checks the tarball with `npm publish --dry-run`. It does not verify registry credentials or trusted publishing access. +For a first trial without publishing, keep the dry-run step and remove the **Publish package** and **Create GitHub release** steps. + +## Commit the version before building + +Use this when the build embeds the version or the released source must include updated version files or a changelog: + +1. Plan the version with `release/plan`, using the same no-change and `force` rules as the workflow above. +2. Update the version on a release branch, for example with `npm version 1.2.3 --no-git-tag-version`. + Commit the package manifest, lockfile, changelog, and any generated version files through a pull request. +3. After merging, run CI and packaging **from the merged commit**. Omit the Package action's `version` input; the version is already in `package.json`. +4. Publish the tarball, then create the GitHub release with the planned `tag` and merged commit as `target-sha`. + +Keep the planned tag throughout this process; do not calculate another version after merging. +If you use a second workflow for publication, read the committed version instead of calling `release/plan` again, +and skip it when that version is already released. + +The Package action performs its own checkout of the workflow's source ref. +Checking out the merged commit in an earlier step does not change that ref: start the packaging workflow at the merged commit. +See the [release preparation example](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md#example-3-validate-source-update-files-create-release-artifacts-and-release) +for automating the pull request and merge. + +## Other setups + +- **GitHub Packages or registry tokens:** use the [authentication examples](../../actions/publish/README.md#registry-tokens-and-github-packages). + The `github-token` input only authenticates artifact downloads; registry credentials use `NODE_AUTH_TOKEN`. +- **Prereleases:** set `prerelease: "true"` on `release/plan` and `release/create`, and `tag: next` on both Publish steps. + Update the version conversion to accept your full prerelease version, such as `1.2.3-rc.1`. + The npm distribution tag (`next`) is separate from the Git release tag (`v1.2.3-rc.1`). +- **Monorepos:** scope CI and both release actions with `working-directory`. + For Package, set `working-directory` to the dependency installation root and `package-directory` to the package's relative path. + Convert prefixed Git tags such as `my-package/v1.2.3` to npm versions, and use a separate concurrency group and artifact name per package. +- **Drafts and signing:** create a draft with `publish: "false"`, then use `release/update` to attach assets and publish after npm publication succeeds. + See the [draft release example](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md#example-2-validate-source-create-release-artifacts-and-release). + Use `release/delete` with `draft-only: "true"` to clean up a temporary draft on failure **only if the npm package has not been published**. + +## Recover a failed release + +Check the registry before retrying: an npm package version cannot be overwritten, and a failed run may still have published it. + +| Where it failed | What to do | +| ------------------------------------------------- | ------------------------------------------------------------------------ | +| Before npm publication | Fix the failure and rerun the workflow. | +| npm published, but GitHub release creation failed | Run only release creation with the original tag, source SHA, and assets. | +| npm published, but a draft was not finalized | Keep the draft and retry `release/update`. | + +After npm publication, do not rerun the whole release job or plan a new version to finish the same release. +Deleting a draft does not undo npm publication. diff --git a/docs/usage/examples/reactionable-cli.yml b/docs/usage/examples/reactionable-cli.yml new file mode 100644 index 0000000..074e415 --- /dev/null +++ b/docs/usage/examples/reactionable-cli.yml @@ -0,0 +1,62 @@ +# Replace with a published ci-github-nodejs commit containing the Release workflow. +name: Release + +on: + workflow_dispatch: + schedule: + - cron: "25 8 * * 1" + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + permissions: + actions: write + contents: write + issues: write + pull-requests: write + packages: read + security-events: write + id-token: write + with: + versioning: release-please + environment: github-pages + build: | + {"commands":["build","build:doc"],"artifact":["dist","build"]} + test: '{"coverage":"codecov"}' + ai-summary: true + secrets: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + pages: + name: Deploy documentation + needs: release + if: needs.release.outputs.status == 'published' + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.url }} + outputs: + url: ${{ steps.deployment.outputs.url }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - id: deployment + uses: hoverkraft-tech/ci-github-publish/actions/deploy/github-pages@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + with: + build-artifact-id: ${{ needs.release.outputs.build-artifact-id }} + build-path: build/docs + + deploy-checks: + needs: pages + uses: hoverkraft-tech/ci-github-publish/.github/workflows/deploy-checks.yml@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + permissions: + contents: read + with: + url: ${{ needs.pages.outputs.url }} diff --git a/docs/usage/examples/reactionable.yml b/docs/usage/examples/reactionable.yml new file mode 100644 index 0000000..42f6285 --- /dev/null +++ b/docs/usage/examples/reactionable.yml @@ -0,0 +1,54 @@ +# Replace with a published ci-github-nodejs commit containing the Release workflow. +name: Release + +on: + workflow_dispatch: + schedule: + - cron: "25 8 * * 1" + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + permissions: + actions: write + contents: write + issues: write + pull-requests: write + packages: read + security-events: write + id-token: write + with: + versioning: lerna + environment: github-pages + build: | + {"commands":["build:ci","storybook:build","docs:build"],"artifact":["packages/**/lib","build","packages/**/docs"]} + test: '{"coverage":"codecov"}' + ai-summary: true + secrets: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + pages: + name: Deploy documentation + needs: release + if: needs.release.outputs.status == 'published' + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.url }} + outputs: + url: ${{ steps.deployment.outputs.url }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - id: deployment + uses: hoverkraft-tech/ci-github-publish/actions/deploy/github-pages@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + with: + build-artifact-id: ${{ needs.release.outputs.build-artifact-id }} + build-path: build/storybook diff --git a/docs/usage/releases.md b/docs/usage/releases.md new file mode 100644 index 0000000..0c0d803 --- /dev/null +++ b/docs/usage/releases.md @@ -0,0 +1,157 @@ +# Release Node.js packages + +Use the [Release workflow](../../.github/workflows/release.md) to release one public +npm package or fixed-version Lerna workspaces. The calling repository supplies +triggers and build settings. HoverKraft prepares versions, runs CI on the release +commit, packages the build output, publishes to npm and creates the GitHub release. +No custom release scripts are needed in the calling repository. + +Complete caller examples, including documentation deployment: + +- [Single-package CLI](examples/reactionable-cli.yml) +- [Lerna monorepo](examples/reactionable.yml) + +## Configure the repository + +1. Set each public package's `name`, `repository.url`, and package contents (`files` + or `.npmignore`). Include compiled output and any CLI entry points. +2. Configure [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) + for each package. Use the **calling repository and workflow filename**, such + as `release.yml`, and the environment passed below. Allow direct `npm publish`. + Publish the first version of a new npm package before configuring its publisher. +3. Allow the default branch and `v*` tags in the GitHub environment. Its name + defaults to `release`; an existing environment can be passed explicitly. +4. Store `OPENAI_API_KEY` as a repository or environment secret when enabling AI + summaries, and pass it explicitly as shown below. +5. Replace `` with an immutable `ci-github-nodejs` commit containing this + reusable workflow. Keep `workflow_dispatch` with no required inputs: preparation + uses it to start publication at the generated tag. + +GitHub-hosted Linux runners and public packages on npmjs.org are supported. +Private packages, custom registries, prereleases and independent Lerna versions +require a [custom pipeline](custom-releases.md). + +## Single package + +This configuration fits a TypeScript CLI such as `reactionable-cli`. Release +Please computes versions from Conventional Commits and prepares a release PR +containing `package.json`, the lockfile and `CHANGELOG.md` updates. It does not +publish until that PR is merged and the workflow runs again, manually or on schedule. + +```yaml +name: Release + +on: + workflow_dispatch: + schedule: + - cron: "25 8 * * 1" + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + permissions: + actions: write + contents: write + issues: write + pull-requests: write + packages: read + security-events: write + id-token: write + with: + versioning: release-please + environment: github-pages + build: | + { + "commands": ["build", "build:doc"], + "artifact": ["dist", "build"] + } + ai-summary: true + secrets: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} +``` + +The defaults use Release Please and `npm run build` with `dist` as the build +artifact. Omit `versioning` and `build` when those defaults match your project. +The existing CI workflow's `lint`, `test` and `code-ql` options are also available. + +GitHub must allow Actions to create PRs. Release PRs created with `GITHUB_TOKEN` +do not trigger normal PR workflows. If required checks depend on those events, +pass a GitHub App token through the optional `github-token` secret. Grant the app +contents, issues, pull requests and actions write access. The workflow never +merges a release PR or bypasses branch protection. + +## Fixed-version Lerna workspaces + +Keep the same triggers, permissions and secrets. Change the inputs to match a +repository such as `reactionable`: + +```yaml +with: + versioning: lerna + environment: github-pages + build: | + { + "commands": ["build:ci", "storybook:build", "docs:build"], + "artifact": ["packages/**/lib", "build", "packages/**/docs"] + } + test: '{"coverage":"codecov"}' + ai-summary: true +``` + +Install Lerna in the project and use a stable fixed version in `lerna.json`. +The workflow runs the installed Lerna with `--conventional-commits`; it does not +implement another version calculator. Lerna updates package versions, internal +dependency ranges, lockfiles and changelogs. Its existing versioning settings, +including pre-1.0 behavior, apply. + +This mode commits directly to the default branch and pushes the commit and tag +atomically. Repository rules must permit that push. If the branch advances during +preparation, the push fails without pushing the new tag. Existing tags are never +overwritten. Only public workspaces at the new release version are published; +unchanged packages can retain their previous versions. + +## Run and resume releases + +Run the calling workflow from the default branch or wait for its schedule. +Preparation reports one of these outcomes in the job summary: + +| Status | Meaning | +| ----------------- | ------------------------------------------------------------- | +| `no-changes` | Nothing needs releasing; no CI or publishing starts. | +| `awaiting-review` | Merge the linked Release Please PR, then run again. | +| `dispatched` | The version tag exists; follow the new run for that tag. | +| `released` | This version already has a published GitHub release. | +| `published` | The tag run published all packages and finalized the release. | + +Both preparation and publication share a concurrency group inside the reusable +workflow. Do not use that same group in the caller. A successful preparation run +does not imply publication succeeded: its tag run performs CI and publication. + +AI generation runs before npm publication. A draft becomes public only after +every package and release asset succeeds. AI summaries accompany the full release +notes and should be checked for accuracy. A published release keeps its notes on retry. + +To resume a release after its tag was pushed, dispatch the **calling** workflow +at the original tag: + +```sh +gh workflow run release.yml --ref v1.2.3 +``` + +The tag must contain the caller workflow and match the committed version. A tag +run never computes another version. CI rebuilds that source. Already published npm +versions are skipped and their original tarballs are downloaded with SHA-512 +verification. Existing GitHub assets are reused only when SHA-256 digests match; +conflicting or missing digests fail instead of replacing assets. + +npm workspace publication cannot be atomic, so partial publication is possible. +An npm registry error stops recovery; only an absent version permits publishing. +If CI fails because source code needs changing, make a new commit and release. +Do not move an existing release tag. + +The `release-url`, `tag`, `status` and `build-artifact-id` workflow outputs support +follow-up jobs. For documentation deployment, require `status == 'published'` +and consume `build-artifact-id` from that tag run. Documentation deployment remains +an independent consumer job. diff --git a/tests/release/artifacts.test.js b/tests/release/artifacts.test.js new file mode 100644 index 0000000..20b7cca --- /dev/null +++ b/tests/release/artifacts.test.js @@ -0,0 +1,199 @@ +const assert = require("node:assert/strict"); +const { createHash } = require("node:crypto"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { test } = require("node:test"); +const { + ReleaseArtifacts, +} = require("../../actions/release/ReleaseArtifacts.js"); +const restorePackage = (inputs, fetchPackage) => + new ReleaseArtifacts(fetchPackage).restorePackage(inputs); + +function directory(t) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nodejs-release-assets-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + return root; +} + +test("finds drafts when the release-by-tag endpoint does not return them", async () => { + const draft = { id: 1, tag_name: "v1.2.0", draft: true }; + const github = { + rest: { + repos: { + getReleaseByTag: async () => { + throw Object.assign(new Error("Not found"), { status: 404 }); + }, + }, + }, + paginate: async () => [draft], + }; + assert.equal(await ReleaseArtifacts.findRelease(github, {}, "v1.2.0"), draft); + assert.equal( + await ReleaseArtifacts.findRelease(github, {}, "v1.3.0"), + undefined, + ); + github.rest.repos.getReleaseByTag = async () => { + throw Object.assign(new Error("Forbidden"), { status: 403 }); + }; + await assert.rejects( + ReleaseArtifacts.findRelease(github, {}, "v1.2.0"), + /Forbidden/, + ); +}); + +function registryFixture(t) { + const root = directory(t); + const tarballPath = path.join(root, "package.tgz"); + fs.writeFileSync(tarballPath, "new build"); + const published = Buffer.from("the exact bytes published to npm"); + const metadata = { + name: "@example/core", + version: "1.2.0", + dist: { + tarball: "https://registry.npmjs.org/@example/core/-/core-1.2.0.tgz", + integrity: `sha512-${createHash("sha512").update(published).digest("base64")}`, + }, + }; + return { + tarballPath, + published, + metadata, + inputs: { name: metadata.name, version: metadata.version, tarballPath }, + }; +} + +test("allows publication only when the npm version is absent", async (t) => { + const f = registryFixture(t); + assert.equal( + await restorePackage( + f.inputs, + async () => new Response(null, { status: 404 }), + ), + false, + ); + assert.equal(fs.readFileSync(f.tarballPath, "utf8"), "new build"); + await assert.rejects( + restorePackage(f.inputs, async () => new Response(null, { status: 503 })), + /HTTP 503/, + ); +}); + +test("recovers the original npm tarball for an already published version", async (t) => { + const f = registryFixture(t); + const requested = []; + const published = await restorePackage(f.inputs, async (url) => { + requested.push(url); + return requested.length === 1 + ? Response.json(f.metadata) + : new Response(f.published); + }); + assert.equal(published, true); + assert.deepEqual(fs.readFileSync(f.tarballPath), f.published); + assert.deepEqual(requested, [ + "https://registry.npmjs.org/%40example%2Fcore/1.2.0", + f.metadata.dist.tarball, + ]); +}); + +test("rejects corrupted tarballs without replacing the package file", async (t) => { + const f = registryFixture(t); + let requests = 0; + await assert.rejects( + restorePackage(f.inputs, async () => + ++requests === 1 ? Response.json(f.metadata) : new Response("corrupt"), + ), + /integrity verification/, + ); + assert.equal(fs.readFileSync(f.tarballPath, "utf8"), "new build"); +}); + +test("rejects unexpected registry identities, download hosts, and failed downloads", async (t) => { + const f = registryFixture(t); + await assert.rejects( + restorePackage(f.inputs, async () => + Response.json({ ...f.metadata, version: "9.0.0" }), + ), + /unexpected package version/, + ); + await assert.rejects( + restorePackage(f.inputs, async () => + Response.json({ + ...f.metadata, + dist: { tarball: "https://example.invalid/package.tgz" }, + }), + ), + /npm registry/, + ); + let requests = 0; + await assert.rejects( + restorePackage(f.inputs, async () => + ++requests === 1 + ? Response.json(f.metadata) + : new Response(null, { status: 503 }), + ), + /download failed/, + ); +}); + +function assetsFixture(t) { + const root = directory(t); + const data = Buffer.from("published package"); + fs.writeFileSync(path.join(root, "example-core-1.2.0.tgz"), data); + const release = { id: 1, draft: true }; + const assets = []; + const uploaded = []; + const github = { + rest: { + repos: { + getReleaseByTag: async () => ({ data: release }), + uploadReleaseAsset: async (asset) => uploaded.push(asset), + }, + }, + paginate: async () => assets, + }; + return { + release, + assets, + uploaded, + data, + args: { + github, + repo: { owner: "example", repo: "project" }, + tag: "v1.2.0", + directory: root, + }, + }; +} + +test("uploads the exact package bytes and skips an identical existing release asset", async (t) => { + const f = assetsFixture(t); + await new ReleaseArtifacts().attachAssets(f.args); + assert.equal(f.uploaded.length, 1); + assert.deepEqual(f.uploaded[0].data, f.data); + f.assets.push({ + name: "example-core-1.2.0.tgz", + digest: `sha256:${createHash("sha256").update(f.data).digest("hex")}`, + }); + await new ReleaseArtifacts().attachAssets(f.args); + assert.equal(f.uploaded.length, 1); + f.release.draft = false; + await new ReleaseArtifacts().attachAssets(f.args); + assert.equal(f.uploaded.length, 1); +}); + +test("does not overwrite mismatched assets or modify an incomplete published release", async (t) => { + const f = assetsFixture(t); + f.assets.push({ name: "example-core-1.2.0.tgz", digest: "sha256:different" }); + await assert.rejects( + new ReleaseArtifacts().attachAssets(f.args), + /different or unavailable digest/, + ); + f.assets.length = 0; + f.release.draft = false; + await assert.rejects( + new ReleaseArtifacts().attachAssets(f.args), + /missing expected asset/, + ); + assert.equal(f.uploaded.length, 0); +}); diff --git a/tests/release/package-lock.json b/tests/release/package-lock.json new file mode 100644 index 0000000..90f9ebb --- /dev/null +++ b/tests/release/package-lock.json @@ -0,0 +1,5499 @@ +{ + "name": "nodejs-release-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nodejs-release-tests", + "devDependencies": { + "lerna": "10.1.1" + } + }, + "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/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/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@conventional-changelog/template": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.4.0.tgz", + "integrity": "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.8.tgz", + "integrity": "sha512-NY/S1EalQhlonqhwwsarrcnkH7s9ByQuvjWYdk1jB2Gb9EPQJjKkhCTWjM3yQ0UpHaLm1XbIL89b+32G8rhO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/checkbox/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", + "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.6.tgz", + "integrity": "sha512-Z5SOeRC5VNkINfKN4rY8BSaYKb3GYi8iSEqZZRqogsE2GhBBToQ0Xw25SbI+4bcatLzU009mZ5d9EuJNiJuUCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/external-editor": "^3.0.7", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.8.tgz", + "integrity": "sha512-dMEnTkpLqzM/sbILwL7QJgx9iX5hoEM8+bh6shgwd9mYJamdOnth7y2mMaFd08Glj9AXEFKllWcGYyBqnJsHrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.7.tgz", + "integrity": "sha512-enNqTJpojgISCG+a5acjnfJ02hcRTFUBVm8uXiCY40PRPZtSzFnNLskOTU+kDUUEODYma90syxuea1rRtUFh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.9.tgz", + "integrity": "sha512-2Vd5T7lTxfgJMpAd3aOu6bQpbUnCU7tBgvTQIZkP/7juJzmRmTM2t5XfOCuirFduWLTSJI/WZp32Cq1E01TVeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/input/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.6.tgz", + "integrity": "sha512-22LnGGCDm1M/pKXxxF8n6ppr/1IztdsxqeqeBRUMTTNnJTkjIBiAkewiDM4u/gjtazj/FvZS+nk9AbO/vEcBFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.5.tgz", + "integrity": "sha512-0v0wLQA4uhWbybmdH6oPl7ooxCiymNylkvi2gCSc+mA5ZQB6QoJLVhoHq12T82lhozpp9fpMJnJkC1nJrU/A+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.5", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.5.tgz", + "integrity": "sha512-l/IWwC+QhOyXgxkZloOh+n5quGzc1m5ZawyzBmoizCZaxbJeLRdS3pStoqg/JVInJRXaG5aHMhjK6H6oYiBC9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.8", + "@inquirer/confirm": "^6.3.5", + "@inquirer/editor": "^5.3.6", + "@inquirer/expand": "^5.1.8", + "@inquirer/input": "^5.1.9", + "@inquirer/number": "^4.2.6", + "@inquirer/password": "^5.2.5", + "@inquirer/rawlist": "^5.3.8", + "@inquirer/search": "^4.3.6", + "@inquirer/select": "^5.2.8" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.8.tgz", + "integrity": "sha512-h+6Y4pPvYvuRbksn1kIBdD9tCYD+E468W5sIcVfLmoLP5IasnY0r33N+r+46ej/P8b2PCREjyKR+MnnfPGdTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.6.tgz", + "integrity": "sha512-JZj9ttkIvuaFQIc2oxsyqC1ad05WL02ku6W/INVKTCzNaVq1KkhP3AfTRC0m7OWlKZyw+EWwPBFZfYiG9jF6Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.8.tgz", + "integrity": "sha512-BklkkAAOpjdCfuQw5fVGcDuhKxA7T1iBpn6b2JUvDDdkIsO3Ftlg+WnWUCVpkUBFagL2o036OFW+xL4lC9wIPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", + "integrity": "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emnapi/core": "^1.1.0", + "@emnapi/runtime": "^1.1.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-5.0.2.tgz", + "integrity": "sha512-EkzGmEsgbQ1rqWkRJe2P0oQHx/ylZozDUNPMXCklLuSFL3GY+QyEfBUjhjCsgGXzh4OGpnHvkboSQgczjP/jJg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^9.0.0", + "http-proxy-agent": "^9.0.0", + "https-proxy-agent": "^9.0.0", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^10.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/arborist": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-10.0.3.tgz", + "integrity": "sha512-QuGpR01YV/4RuJ+5S0W5qh4uljQygmk7b50Jkx+zLErOMMUpKhpbLu/TqQlKfsvuR3SOwXIn3ht+G4bcxrv1WA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^6.0.0", + "@npmcli/installed-package-contents": "^5.0.0", + "@npmcli/map-workspaces": "^6.0.0", + "@npmcli/metavuln-calculator": "^10.0.0", + "@npmcli/name-from-folder": "^5.0.0", + "@npmcli/node-gyp": "^6.0.0", + "@npmcli/package-json": "^8.0.0", + "@npmcli/query": "^5.0.0", + "@npmcli/redact": "^5.0.0", + "@npmcli/run-script": "^11.0.0", + "bin-links": "^7.0.0", + "cacache": "^21.0.1", + "common-ancestor-path": "^2.0.0", + "diff": "^8.0.2", + "hosted-git-info": "^10.1.1", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^10.0.1", + "npm-install-checks": "^9.0.0", + "npm-package-arg": "^14.0.0", + "npm-pick-manifest": "^12.0.0", + "npm-registry-fetch": "^20.0.1", + "pacote": "^22.0.0", + "parse-conflict-json": "^6.0.0", + "proc-log": "^7.0.0", + "proggy": "^4.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^14.0.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^8.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-6.0.0.tgz", + "integrity": "sha512-AheOs4swKka/XLtht6xxJDPezlQ7K2IYQ9Y8lST4JLDjnralnWuMM9AE2CdVcgQJ5omrXhsRzM7F7aYmeZBvKQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-8.0.0.tgz", + "integrity": "sha512-5P1oo+TbxZNAiiMBtpzHA8QyEGh5D69LYLexNWJEDXLdxnAZvT/SLitGJBXxjtCE4ftAcFOS/Tu2185MeIjooQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^10.0.0", + "ini": "^7.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^12.0.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "which": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-5.0.0.tgz", + "integrity": "sha512-6Ay12sf2Lh7U1ifvnS1mq7TZFeh/rXHMXye+kV7jQrANIubaoVcleeh4HdFumxhsRYwm9OaHycB5lYmSwGrcIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-6.0.0.tgz", + "integrity": "sha512-Dsex7XBea0BoFRbfALMbNuaNqeI3kbZJxiBMQhFvPUNluv0yEeKMd5FLIXy0NCsOuGMYQkLa+i219CadnoqLbg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^5.0.0", + "@npmcli/package-json": "^8.0.0", + "glob": "^13.0.0", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-10.0.0.tgz", + "integrity": "sha512-fC56lpGOae+unrFx6hoiWeWa0/4q+bLhOQTSrB65qcy+Zm34i1OdBcevAQFJJdUB/3i9uXw7PjdrrRjt0KSKXw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cacache": "^21.0.1", + "json-parse-even-better-errors": "^6.0.0", + "pacote": "^22.0.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-5.0.0.tgz", + "integrity": "sha512-Mua2i5asmOn4sGGAsANS9Guamo7sgUd4BihhQWzlNGz6l/Ki23CzC2oGe4I94r2k/39ImCjDLbyu/rSJr/SXoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-6.0.0.tgz", + "integrity": "sha512-MFakpea4pcZNlHSTbMi15HK8RY8zl2UpgDtxhZCWOer+KRN3x7HFIMk/fKpOMgR55L4LIcA2qn8IHeyABhIFtw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-8.0.0.tgz", + "integrity": "sha512-agNZzYQ18MR0wKp3Emg1q5QbcC8CXigYp3Z3CvB0Sax9Ge9aF4cVyyuSG+5SbACSrZUKTvMjVULWiE1RJA38wg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^8.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^10.1.1", + "json-parse-even-better-errors": "^6.0.0", + "proc-log": "^7.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-10.0.1.tgz", + "integrity": "sha512-IstFcKAoEIZ7olS9iTwBsyNOZkIturafO2da9ocSP3LifRlm1+sxuo8IVM5phg5sNe0tNYLT0ZqCBRTLNzz0AA==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-5.0.0.tgz", + "integrity": "sha512-8TZWfTQOsODpLqo9SVhVjHovmKXNpevHU0gO9e+y4V4fRIOneiXy0u0sMP9LmS71XivrEWfZWg50ReH4WRT4aQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-5.0.0.tgz", + "integrity": "sha512-3zcN5Q3yEmeyxXBzqB6fXPQFzYa2ROsGFSr69W0ArXIAGJqxl/aFECOVPD2kbkYPm0U/EHxFKgclK3UA9WQg5A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-11.0.0.tgz", + "integrity": "sha512-leBRl6F5F0TvWut8m1/aZcMTUHi2vXjKeMJ/Ik1lW7Q7Yy16Dhtkklu+cEqQww1p1NeLnUNzV3+uwpzqRcy9vw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^6.0.0", + "@npmcli/package-json": "^8.0.0", + "@npmcli/promise-spawn": "^10.0.0", + "node-gyp": "^13.0.0", + "proc-log": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@nx/devkit": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-23.3.1.tgz", + "integrity": "sha512-Cw6YISZUjit22Trfo/p6rnrNklIKHYgRyp2IyOgr0i9naDgO3Ufllib9D/v0SInwX2zKevaWJWMyreqVvHRU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ejs": "5.0.1", + "minimatch": "10.2.5", + "semver": "^7.6.3", + "tslib": "^2.3.0", + "yargs-parser": "21.1.1" + }, + "peerDependencies": { + "nx": ">= 22 <= 24 || ^23.0.0-0" + } + }, + "node_modules/@nx/devkit/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-23.3.1.tgz", + "integrity": "sha512-T3KOTrUIdWaqlIIMyusbpKHujXTumoltkYwBjqjw9Py6Fb3c8Eb7uvWVW6dP8ODknqr9oEcTlk+C8Nid+bunAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-23.3.1.tgz", + "integrity": "sha512-GnbMF9xEGIdUI/7KoQdcTILWmRUrMLbbFi3CO8KgKcA1Be7eaoeKdgBQoCQ2yF7r7DyQOR1PXp7sF3s7gqUATQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-23.3.1.tgz", + "integrity": "sha512-RzLx1pVL/MQQ/fSreMFIufAfsrj/TpD1+69n5kBcr3An2FaqKdQ6BhKLWRlAeOSKq6Su75Y0la8e2AU5zlmdpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-23.3.1.tgz", + "integrity": "sha512-/K7ig9xL4FMO2eQovTSLtk4QR7d0phbKoWf1wYQZ2r0ZrHNo2gPYmS8wU0Ci4d6oSiqa2fdeHKVDtEvWldTBaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-23.3.1.tgz", + "integrity": "sha512-Uj3xl5aLXUV6EQ/vWbZBqMqZO9K58CxRIsMTISC1+LxjvGnf4uUL73ObTi3K1eih3f1CVCGsU95LF3Kf6T2Frw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-23.3.1.tgz", + "integrity": "sha512-woeNHutXYgiWU0SQnvKQ4VNgVbqwy1uer6bS0HF2RbfN80HglsjhtzTAUZY2pZiWBLbHcHZwTNmDGTAXSNMLAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-23.3.1.tgz", + "integrity": "sha512-GckqfXo7BAIrcR07CoI0IM06h1wbUim+4AyUYosokrI4Gx1zHZFRwrE3vpPrMpSKVVYZD7BGQp2bhZBPvgGaxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-23.3.1.tgz", + "integrity": "sha512-QjOj5S/X39OxVykltLFSvNxaZtnI9uRk0Uqfyt69kOrZn+/Ap2SM/bT70Xqn8PFxj5hfA0viI2vU1AlOM4JQxg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-23.3.1.tgz", + "integrity": "sha512-Jf5kc3zhek8Fz06DaMxaJTJxgNJIN2tddE8yxt+XaIHtulmW+MMjy6Z91QPKPf9iZA70N+oT9vwgvYPmOsdO8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-23.3.1.tgz", + "integrity": "sha512-RKQ5t6u4fgOWQyVt61y5Jn/xzXCYxqXOKz2okDbuWccBShQddF8jsqRaLMZDlGT5d8CJTLIso8Y9BiY12/7x7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.8.tgz", + "integrity": "sha512-L7y8eYc+AwxGr2PWI4WFt1VG4TiJ66c26BD16mXpYIlXxG0SMigM1+m4aTSlYyBr5BlQsGAlz8uDCoZN4SEMcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.5", + "@octokit/request": "^10.0.16", + "@octokit/request-error": "^7.1.2", + "@octokit/types": "^18.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.5.tgz", + "integrity": "sha512-iXa654H3yFafF/ieHkukfbgWo2rmXD2ceD0ZOtrPhw1bc3FDch1d9N/TNs0FQ1/cIbwb7kspUX8jzIs8nzb9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^18.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.5.tgz", + "integrity": "sha512-bt/hm03LeU6Vy7FwTrkkC9p3XGT/lBwClglMqxBSe5/q0E5CdJTXeAqEI0vlw89/LF/G6tryTIH8HirZ3prMVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.16", + "@octokit/types": "^18.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz", + "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-enterprise-rest": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz", + "integrity": "sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.16.tgz", + "integrity": "sha512-A0zWGjHzISIb+9ccG8s0dq7LKO5zVpJLRICjgUb+sJxEWqn8RUHB1rD3AE51+PECvXHIxqZ1VVvs4fHTSD9nUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.5", + "@octokit/request-error": "^7.1.2", + "@octokit/types": "^18.0.0", + "content-type": "^3.0.0", + "json-with-bigint": "^3.5.12", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.2.tgz", + "integrity": "sha512-XZRuT3xZ84D3gYErI1DZvhJ33dCWVV6uzBtWkaBB4TvA/L6eOeTZodxLFVB44bBEEo3vEx7y00UfX1tBLrtLRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^18.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz", + "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^29.0.1" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sigstore/bundle": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-5.0.0.tgz", + "integrity": "sha512-wefjygudENbzbQMks1t5u34EP0fFoD0XvaEP7DOUP/sXKvogzEJYFw5E6pegGyp3onGWzVEYKVa3bNZWyTYX+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-4.0.1.tgz", + "integrity": "sha512-9v5hRjujn5NXq8o7XFEUgLyAtdr5Iisb4pzM05u3K61IS5q3hP3luWAndk0RkPPLTUFoTbg7Vb84UQ1ZQeajWQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.2.tgz", + "integrity": "sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-5.0.0.tgz", + "integrity": "sha512-DSFivqz9/i5AkwZ5fq0YdjaJlc4o1WeS2Zffon0kqtChx0vy4W9NOjkEet9bF2vkzOufX72eVH8kZBIGtcBp1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^5.0.0", + "@sigstore/core": "^4.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^16.0.0", + "proc-log": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-5.0.0.tgz", + "integrity": "sha512-Zyqg9tcHps3uRAlKHLNmsW4ohsUZAjb9G+31r7lg0ICh/JOcadzmJsIRdjKljlRHpaR0K4aJ2kXXIdywdcdMlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-4.1.2.tgz", + "integrity": "sha512-BfD9eLrz3A/DG58aSgfgZYmIR6V9Yw96QVN/frtu2bEH7ctSTk2TDvHU12un3JsDPBTqTNkqkIkucjxljpOFqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^5.0.0", + "@sigstore/core": "^4.0.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@simple-libs/child-process-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@simple-libs/hosted-git-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/hosted-git-info/-/hosted-git-info-2.0.0.tgz", + "integrity": "sha512-55XwK/GYgV58PuN8lZFhI1qRxdKoMLJocXl/yM1EPqazFDmM4QWY7q6BxPCPDNKTNunj794DSD4h0H7SrqUdMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@simple-libs/normalize-package-data": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/normalize-package-data/-/normalize-package-data-1.0.0.tgz", + "integrity": "sha512-/EOmFBekV9tGee8gac/k+5Ox3twkypNqh5TfxA9vTkmcJkjm6f1xqrlstDNOrEhTvnYyVtU6Du2ZhY/IV1+9GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/hosted-git-info": "^2.0.0", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@simple-libs/stream-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-5.0.1.tgz", + "integrity": "sha512-wacy5twbJk36R0dTcGE3qmPNNH095bFselAYNrVCzf0Byd0t3+Qzk3+3gn/rHGP10FuHBVk4swg4aczr1c2z/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.2.6" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/parse-path": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", + "integrity": "sha512-LriObC2+KYZD3FzCrgWGv/qufdUy4eXrxcLgQMfYXgPbLIecKIsVBaQgUPmxSSLcjmYbDTQbMgr6qr6l/eb7Bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", + "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/abbrev": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "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/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/argue-cli": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-3.2.0.tgz", + "integrity": "sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bin-links": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-7.0.1.tgz", + "integrity": "sha512-lgxd7FtxW02eSuafotQOGEQf1iGfDExMz2BRaL+e1tU5xTLx+vvSVA5oHIO6B1tR91LDKcWqYGsktUHdFMtHHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^9.0.0", + "npm-normalize-package-bin": "^6.0.0", + "proc-log": "^7.0.0", + "read-cmd-shim": "^7.0.0", + "write-file-atomic": "^8.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacache": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-21.0.1.tgz", + "integrity": "sha512-pTwz/uj3Jyp6WXdJ6fWhR+7LVxVs6RyroQSn7KJwHsSxXuyGSp0pcMVcwSwTpCFq1X2YG8QBe0W+vN+cr0SwzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^6.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^14.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "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/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/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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/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/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cmd-shim": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-9.0.2.tgz", + "integrity": "sha512-xVHoI+wNrM4tDB9iC1idf/8D0tYnVimlBp/5zHW+x1sGjjRD69NvR9th3Z1JAYGN/BTW4he6aZFYV6kzy+k+jw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "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/columnify": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", + "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/content-type": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-3.1.1.tgz", + "integrity": "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/conventional-changelog": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-8.1.3.tgz", + "integrity": "sha512-fOMwLCxm46znJKPB08jLxl1FD8SOZOQHktlzCVhvhhVUfq0KgQSryoYXsRRHs1aRlZPR1/QK5wztnY8sBNC2AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@conventional-changelog/git-client": "^3.1.2", + "@simple-libs/hosted-git-info": "^2.0.0", + "@simple-libs/normalize-package-data": "^1.0.0", + "argue-cli": "^3.1.0", + "conventional-changelog-preset-loader": "^6.0.1", + "conventional-changelog-writer": "^9.2.1", + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.2", + "fd-package-json": "^2.0.0" + }, + "bin": { + "conventional-changelog": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.4.0.tgz", + "integrity": "sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@conventional-changelog/template": "^1.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-changelog-preset-loader": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-6.0.1.tgz", + "integrity": "sha512-GZ8E3RQzXQb3JFy0pKl8oeSf7ENIhvAMvJr+PlWkavZOesLHHdhkfNJ4SvW35eo1Fu2+EyVxWQ1tVvtzAG2iWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-9.3.0.tgz", + "integrity": "sha512-lxiECDlvy880e7JIYAtAhxg0wD/CGCVbZCdeua5aOlQVFTEd39zAeOzwK22i/FO4xnEOURK1gbUTETNEYW7zxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@conventional-changelog/template": "^1.4.0", + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0", + "conventional-commits-filter": "^6.0.1", + "verkit": "^0.5.0" + }, + "bin": { + "conventional-changelog-writer": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-changelog/node_modules/@conventional-changelog/git-client": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.2.0.tgz", + "integrity": "sha512-sD/E0vzpkoDz/dqM1Hhv0ihC4KBw6RXjefeO8SG1XrX+wGkMxe1pKeedkKDPCtr2JjuhbuS9aJNgOvnT8hbeBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", + "verkit": "^0.5.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.3" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/conventional-changelog/node_modules/conventional-commits-parser": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.3.tgz", + "integrity": "sha512-Q9oWm1o9uy/oEsZQIraoFMZ4RuPD+79KgwWj6mJVP356tazolCB0piRLZ5Kz9te9p027uSEcUW+Gu44Eh+X79g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-commits-filter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-6.0.1.tgz", + "integrity": "sha512-cs+LadpH7Kpw0M3k8wurk+sOVVDAENA0iK4OBOrkL94j5lEVYRJ4j3zd2bhY9qgzyrPqthdcYT3axzRN7AliMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-commits-parser": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz", + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-recommended-bump": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-12.1.0.tgz", + "integrity": "sha512-HTNG3kEZXOOfKwrEx54ljURU9bG4nVPQzXMyCSeUcA/PE8EpNpQNujSrbXNBI8QZyN35zM+pVw+FuQk4KqHSHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@conventional-changelog/git-client": "^3.1.0", + "argue-cli": "^3.1.0", + "conventional-changelog-preset-loader": "^6.0.1", + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.0" + }, + "bin": { + "conventional-recommended-bump": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-recommended-bump/node_modules/@conventional-changelog/git-client": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.2.0.tgz", + "integrity": "sha512-sD/E0vzpkoDz/dqM1Hhv0ihC4KBw6RXjefeO8SG1XrX+wGkMxe1pKeedkKDPCtr2JjuhbuS9aJNgOvnT8hbeBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", + "verkit": "^0.5.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.3" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/conventional-recommended-bump/node_modules/conventional-commits-parser": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.3.tgz", + "integrity": "sha512-Q9oWm1o9uy/oEsZQIraoFMZ4RuPD+79KgwWj6mJVP356tazolCB0piRLZ5Kz9te9p027uSEcUW+Gu44Eh+X79g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ejs": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", + "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-10.0.1.tgz", + "integrity": "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "figures": "^6.1.0", + "get-stream": "^9.0.1", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.3.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "which-command": "^0.1.0", + "yoctocolors": "^2.1.2" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.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-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-up": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", + "integrity": "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^9.2.0" + } + }, + "node_modules/git-url-parse": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-16.1.0.tgz", + "integrity": "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-up": "^8.1.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.8", + "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/hosted-git-info": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-9.0.0.tgz", + "integrity": "sha512-tCBEZV2z2FNpIDl2vrhiWzIHzs4qOAuIDEO85eS02vZ3L1U3P56qpPL8GuGGAijDktAEaq2swMkO/Fmbo7YmfQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/ini": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz", + "integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/init-package-json": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-9.0.0.tgz", + "integrity": "sha512-aYDqz4jgX1plB2GDCFYStlOh67QXVpAIeNgDq8rRGVDYMvLE39yYlf8+CjYY2chVOVfqLsSIycFYznHWozZ9CA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^8.0.0", + "npm-package-arg": "^14.0.0", + "promzard": "^4.0.0", + "read": "^6.0.0", + "semver": "^7.7.2", + "validate-npm-package-name": "^8.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/inquirer": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-14.2.2.tgz", + "integrity": "sha512-WGerZIIb7mTtaFFskkD/P0europ8xBWf1OXACMiugzKPSmVzoqhcPSuSUzbzMch/t3rf1ncBT3v/0WU73cLqJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/prompts": "^8.7.2", + "@inquirer/type": "4.1.1", + "mute-stream": "^3.0.0", + "run-async": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/ip-address": { + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-ssh": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.1.tgz", + "integrity": "sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "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/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": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", + "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/json-stringify-nice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json-with-bigint": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz", + "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==", + "dev": true, + "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/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/just-diff": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/just-diff-apply": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lerna": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-10.1.1.tgz", + "integrity": "sha512-WDGaMGDGj5jV0n6Yx2zuBDWH0ym6toUE6w+maaPLqeMd3ncDRN+NJMcVWImVdUKCIHRk2Xk+X7QQkFXdHKsscA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@npmcli/arborist": "10.0.3", + "@npmcli/package-json": "8.0.0", + "@npmcli/run-script": "11.0.0", + "@nx/devkit": ">=23.1.0 < 24.0.0", + "@octokit/plugin-enterprise-rest": "6.0.1", + "@octokit/rest": "22.0.1", + "ci-info": "4.4.0", + "columnify": "1.6.0", + "conventional-changelog": "8.1.3", + "conventional-changelog-angular": "9.4.0", + "conventional-commits-filter": "6.0.1", + "conventional-commits-parser": "7.1.2", + "conventional-recommended-bump": "12.1.0", + "cosmiconfig": "9.0.2", + "dedent": "1.7.2", + "envinfo": "7.21.0", + "execa": "10.0.1", + "fs-extra": "^11.2.0", + "git-url-parse": "16.1.0", + "handlebars": "4.7.10", + "import-local": "3.2.0", + "ini": "^7.0.0", + "init-package-json": "9.0.0", + "inquirer": "14.2.2", + "js-yaml": "5.4.2", + "libnpmaccess": "11.0.0", + "libnpmpublish": "12.0.1", + "load-json-file": "7.0.1", + "make-fetch-happen": "16.0.1", + "minimatch": "10.2.6", + "npm-package-arg": "14.0.0", + "npm-packlist": "11.3.0", + "npm-registry-fetch": "20.0.1", + "nx": ">=23.1.0 < 24.0.0", + "p-map": "7.0.8", + "p-queue": "9.3.3", + "pacote": "22.0.0", + "semver": "7.8.5", + "signal-exit": "4.1.0", + "ssri": "14.0.0", + "string-width": "^8.0.0", + "tar": "7.5.22", + "tinyglobby": "0.2.17", + "validate-npm-package-name": "8.0.0", + "write-file-atomic": "8.0.0", + "yargs": "18.2.0" + }, + "bin": { + "lerna": "dist/cli.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || ^26.0.0" + } + }, + "node_modules/libnpmaccess": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-11.0.0.tgz", + "integrity": "sha512-qku5OaTtCv7f4xq5lmZMh4H6pd9WRalww3WF4PEk/aFo1Bpi7/IRCzy28EQBvyrprhzZGifbpM378U4waB9/0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^14.0.0", + "npm-registry-fetch": "^20.0.1" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/libnpmpublish": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-12.0.1.tgz", + "integrity": "sha512-gG32Hgikbb8mx+c+b2MKseuQcwKbiIl7wM4a5+scxuRtu/Q7L/fbywMyLD4naUfL0yexdKWzfi6kglMz6BGOWQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^8.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^14.0.0", + "npm-registry-fetch": "^20.0.1", + "proc-log": "^7.0.0", + "semver": "^7.3.7", + "sigstore": "^5.0.0", + "ssri": "^14.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/load-json-file": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", + "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-fetch-happen": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-16.0.1.tgz", + "integrity": "sha512-uUv1yxHzaKVVEPfcFeGSNov/Cehjv08ovlY8ImTljgL7Q+SiA0dAYLQ6SYVa2kkKqNj4Y3aZEI7xv2teadie0A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^5.0.0", + "@npmcli/redact": "^5.0.0", + "cacache": "^21.0.0", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^6.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^7.0.0", + "ssri": "^14.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "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": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "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/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-6.0.0.tgz", + "integrity": "sha512-AWI8bKapGmgx/J0E6IGYSKj8TiHebZkmKWSs8raPSw8KXwgEAJ+Bw3+LSdXHR6T/RHKAWCOYk2MiLrYluaUU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "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/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-gyp": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-13.1.0.tgz", + "integrity": "sha512-FpNzOLFACKG0oRBYbZ5WOeWD1EqRTIWX90MaRIWTO18lVYPx8fqIjrdS3pihjnCkSmgop6zVGkSqg1vXZLksuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^10.0.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "tar": "^7.5.7", + "tinyglobby": "^0.2.12", + "undici": "^8.4.1", + "which": "^7.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/nopt": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^5.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-bundled": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-6.0.0.tgz", + "integrity": "sha512-EqdodKEW6pYM+dPxA66TZQfMEqVDiuzjDM9edSjuPI1mXUbUJwVxkgqMZSJvs8RTXz2CGq8HUol/AffTZX5g8w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-9.0.0.tgz", + "integrity": "sha512-t05Izcgi7p15cpldqoiXYpjzlkTTvBw33sgjmL/JjcvtV0ydbm2O4iEXO8A6smqComu5FAQhUas86HTMQ6Z1Uw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", + "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-14.0.0.tgz", + "integrity": "sha512-69XQh3k+dtGa1p+7RaR57IuG3rCko96xr/nUfN4yDYBXbTYICiWcOpsFKLN2GtGE9cyIljE+f1exnaYt9MvM+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^10.1.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^8.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-11.3.0.tgz", + "integrity": "sha512-cS1yVkyriZgQAbiK8PtwhZHEtsFOsKHsCg5Ww2ONckAvXIspgqd6o4WirOzvkupU24iMRZ4xtO4kb2iK2rbnag==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^13.0.6", + "ignore-walk": "^9.0.0", + "proc-log": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-12.0.0.tgz", + "integrity": "sha512-8Fs3YLrnNOhrCdPNZy18MzNgVC58LTDAFzq1FdZO/p3BHeCC/coz+t4F5Pxabys8HJpyTUorMea26GkXsb4J/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^9.0.0", + "npm-normalize-package-bin": "^6.0.0", + "npm-package-arg": "^14.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-20.0.1.tgz", + "integrity": "sha512-vzc1svxw/kw1IRjFsLi6gaxe1Olqm88V0tIfu2u5raL0b1gChe6ZEXNkyUlKxUC7s/egt5NxZHkbY18tMKKLfQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^5.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^16.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^6.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^14.0.0", + "proc-log": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx": { + "version": "23.3.1", + "resolved": "https://registry.npmjs.org/nx/-/nx-23.3.1.tgz", + "integrity": "sha512-dufPxsiUazuSejFYdtLAotKXeda9ufz3B9gVtsrvgxkjWjpAM23rOpIJoqZj3FFauig64lYsaMX+nX2hh+YhYQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "@clack/prompts": "1.7.0", + "@emnapi/core": "1.4.5", + "@emnapi/runtime": "1.4.5", + "@emnapi/wasi-threads": "1.0.4", + "@jest/diff-sequences": "30.0.1", + "@napi-rs/wasm-runtime": "0.2.4", + "@tybys/wasm-util": "0.9.0", + "@yarnpkg/lockfile": "1.1.0", + "@zkochan/js-yaml": "0.0.7", + "ansi-regex": "5.0.1", + "ansi-styles": "4.3.0", + "argparse": "2.0.1", + "balanced-match": "4.0.3", + "base64-js": "1.5.1", + "bl": "4.1.0", + "brace-expansion": "5.0.12", + "buffer": "5.7.1", + "bundle-name": "4.1.0", + "chalk": "4.1.2", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "8.0.1", + "clone": "1.0.4", + "color-convert": "2.0.1", + "color-name": "1.1.4", + "default-browser": "5.2.1", + "default-browser-id": "5.0.0", + "defaults": "1.0.4", + "define-lazy-prop": "3.0.0", + "dotenv": "16.4.7", + "dotenv-expand": "12.0.3", + "ejs": "5.0.1", + "emoji-regex": "8.0.0", + "end-of-stream": "1.4.5", + "escalade": "3.2.0", + "escape-string-regexp": "1.0.5", + "fast-string-truncated-width": "3.0.3", + "fast-string-width": "3.0.2", + "fast-wrap-ansi": "0.2.0", + "figures": "3.2.0", + "fs-constants": "1.0.0", + "get-caller-file": "2.0.5", + "has-flag": "4.0.0", + "ieee754": "1.2.1", + "ignore": "7.0.6", + "inherits": "2.0.4", + "is-docker": "3.0.0", + "is-fullwidth-code-point": "3.0.0", + "is-inside-container": "1.0.0", + "is-interactive": "1.0.0", + "is-unicode-supported": "0.1.0", + "is-wsl": "3.1.0", + "isexe": "2.0.0", + "json5": "2.2.3", + "jsonc-parser": "3.3.1", + "lines-and-columns": "2.0.3", + "log-symbols": "4.1.0", + "mimic-fn": "2.1.0", + "minimatch": "10.2.5", + "minimist": "1.2.8", + "npm-run-path": "4.0.1", + "once": "1.4.0", + "onetime": "5.1.2", + "open": "10.1.0", + "ora": "5.4.1", + "path-key": "3.1.1", + "picocolors": "1.1.1", + "readable-stream": "3.6.2", + "require-directory": "2.1.1", + "resolve.exports": "2.0.3", + "restore-cursor": "3.1.0", + "run-applescript": "7.0.0", + "safe-buffer": "5.2.1", + "semver": "7.8.5", + "signal-exit": "3.0.7", + "sisteransi": "1.0.5", + "smol-toml": "1.9.0", + "string_decoder": "1.3.0", + "string-width": "4.2.3", + "strip-ansi": "6.0.1", + "strip-bom": "3.0.0", + "supports-color": "7.2.0", + "tar-stream": "2.2.0", + "tmp": "0.2.7", + "tsconfig-paths": "4.2.0", + "tslib": "2.8.1", + "undici": "7.29.0", + "util-deprecate": "1.0.2", + "wcwidth": "1.0.1", + "which": "3.0.1", + "wrap-ansi": "7.0.0", + "wrappy": "1.0.2", + "y18n": "5.0.8", + "yaml": "2.9.0", + "yargs": "17.7.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "dist/bin/nx.js", + "nx-cloud": "dist/bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "23.3.1", + "@nx/nx-darwin-x64": "23.3.1", + "@nx/nx-freebsd-x64": "23.3.1", + "@nx/nx-linux-arm-gnueabihf": "23.3.1", + "@nx/nx-linux-arm64-gnu": "23.3.1", + "@nx/nx-linux-arm64-musl": "23.3.1", + "@nx/nx-linux-x64-gnu": "23.3.1", + "@nx/nx-linux-x64-musl": "23.3.1", + "@nx/nx-win32-arm64-msvc": "23.3.1", + "@nx/nx-win32-x64-msvc": "23.3.1" + }, + "peerDependencies": { + "@swc-node/register": "^1.11.1", + "@swc/core": "^1.15.8" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/nx/node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/nx/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nx/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/nx/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/nx/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/nx/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/nx/node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/nx/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nx/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/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/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-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-map": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.8.tgz", + "integrity": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "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/pacote": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-22.0.0.tgz", + "integrity": "sha512-++VqeOZeL03uGM2MFLk96jGCSt1owBGkyFKoPr+trwNlZhCpjN2RrvwYxt8nTbs1wNMqSFYurq0TafVWkAIHig==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^8.0.0", + "@npmcli/installed-package-contents": "^5.0.0", + "@npmcli/package-json": "^8.0.0", + "@npmcli/promise-spawn": "^10.0.0", + "@npmcli/run-script": "^11.0.0", + "cacache": "^21.0.1", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^14.0.0", + "npm-packlist": "^11.2.0", + "npm-pick-manifest": "^12.0.0", + "npm-registry-fetch": "^20.0.1", + "proc-log": "^7.0.0", + "sigstore": "^5.0.0", + "ssri": "^14.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-conflict-json": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-6.0.0.tgz", + "integrity": "sha512-JIdsITvN0yVRpYxiip9DpCduYi75AryIDSCf6ez/Yul6PV46L0B/8SxSvLiMmWiUZKopLrD1BZgexxCfufoT9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^6.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.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/parse-json/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/parse-json/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/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-path": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz", + "integrity": "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", + "integrity": "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-path": "^7.0.0", + "parse-path": "^7.0.0" + }, + "engines": { + "node": ">=14.13.0" + } + }, + "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-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/postcss-selector-parser": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/proggy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/proggy/-/proggy-4.0.0.tgz", + "integrity": "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-all-reject-late": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-call-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", + "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", + "dev": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promzard": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/promzard/-/promzard-4.0.0.tgz", + "integrity": "sha512-zqf38XGznB+ECFHQPGt0RNQCh0YF+r5iac0EZloSQlEWDxt0vCS39PEtUShBJOZU0tcJ+USPvmnyGo+3CEFwcQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "read": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/protocols": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", + "integrity": "sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/read": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read/-/read-6.0.0.tgz", + "integrity": "sha512-Tg/LaEke7h9SDbp0xJJj53oa4s1VPPySVnxQF9nUS9DgA25M/ZCLk7RVF1kHjytC87XbkgQKmQ/79+DQgEIqLA==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^4.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/read-cmd-shim": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-7.0.1.tgz", + "integrity": "sha512-GhAsW4iJS0xNqv8P6HocT3gb5HCKHtEBKLIN7fE2oWr27jGIEihu48lXyGGeexkbiLv1XAbwyXvgjYMfT81kZA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/read/node_modules/mute-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-4.0.0.tgz", + "integrity": "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/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-cwd/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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/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/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/sigstore": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-5.0.0.tgz", + "integrity": "sha512-hJqJfoG/e4qFQaauQL00c6J6FrHLBGKtkFvW3JbTSIEFOhLrSjdSM/gWd/yUOfYo/gsERehTXGC1VZWX+9X4Dg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^5.0.0", + "@sigstore/core": "^4.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^5.0.0", + "@sigstore/tuf": "^5.0.0", + "@sigstore/verify": "^4.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/smol-toml": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", + "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, + "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/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-14.0.0.tgz", + "integrity": "sha512-jQxKI0yx0ZnTKrqjKkLDV2DXkBQn3k49JVmVqDGcDwKDtGDbImD/GXsq04KD0VVzCQQ9wZJYal3RwR1GzWTSow==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/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": { + "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-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/treeverse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", + "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-6.0.1.tgz", + "integrity": "sha512-o59N1PU8xk0bBV3HpJ4jjwldIeze17yXW1XTv02CEEKdC3E/tnJDU3CPWDNT4IZpkx9Srv9v1DJZ+JTdo46THg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gar/promise-retry": "^1.0.3", + "@tufjs/models": "5.0.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "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": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-name": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-8.0.0.tgz", + "integrity": "sha512-SCv6OOV6Xj2/3cXy3dGmADluJTNcL3o7hZAglNPTe+WYuEuvxgJzxPrSDLZhF+CwyQOubqgecjMmTJGMVLWjYQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/verkit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/which-command": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz", + "integrity": "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==", + "dev": true, + "license": "MIT", + "bin": { + "which-command": "cli.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/which-command?sponsor=1" + } + }, + "node_modules/which/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "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/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/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": "8.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-8.0.0.tgz", + "integrity": "sha512-dYwyZredl67GyLLIHJnRM3h2PcOmN5SkcgC7eM5DPDEOEl6dLFqVrMg3F1Ea32usj4VSVZtd2H4MtKTNOf6nPg==", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.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/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "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": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/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/yargs/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/yargs/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/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/yargs/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yargs/node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/release/package.json b/tests/release/package.json new file mode 100644 index 0000000..5a885d0 --- /dev/null +++ b/tests/release/package.json @@ -0,0 +1,10 @@ +{ + "name": "nodejs-release-tests", + "private": true, + "scripts": { + "test": "node --test *.test.js" + }, + "devDependencies": { + "lerna": "10.1.1" + } +} diff --git a/tests/release/project.test.js b/tests/release/project.test.js new file mode 100644 index 0000000..60ff578 --- /dev/null +++ b/tests/release/project.test.js @@ -0,0 +1,462 @@ +const assert = require("node:assert/strict"); +const { execFileSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { test } = require("node:test"); +const { ReleaseProject } = require("../../actions/release/ReleaseProject.js"); +const { + ReleaseArtifacts, +} = require("../../actions/release/ReleaseArtifacts.js"); + +Object.assign(process.env, { + CI: "true", + NX_DAEMON: "false", + NX_ISOLATE_PLUGINS: "false", + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_NOSYSTEM: "1", + npm_config_offline: "true", + npm_config_audit: "false", + npm_config_fund: "false", +}); + +function fixture(t, versioning = "release-please") { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nodejs-release-")); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const directory = path.join(root, "project"); + const remote = path.join(root, "remote.git"); + fs.mkdirSync(directory); + const run = (command, args, cwd = directory) => + execFileSync(command, args, { + cwd, + encoding: "utf8", + stdio: "pipe", + }).trim(); + const git = (...args) => run("git", args); + const write = (file, content) => { + fs.mkdirSync(path.dirname(path.join(directory, file)), { recursive: true }); + fs.writeFileSync(path.join(directory, file), content); + }; + const json = (file, value) => + write(file, `${JSON.stringify(value, null, 2)}\n`); + const commit = (message) => { + git("add", "."); + git("commit", "-m", message); + return git("rev-parse", "HEAD"); + }; + git("init", "--bare", remote); + git("init", "-b", "main"); + git("config", "user.name", "Release test"); + git("config", "user.email", "test@example.invalid"); + git("remote", "add", "origin", remote); + write(".gitignore", "node_modules/\n.nx/\ndist/\n*.tgz\n"); + if (versioning === "lerna") { + json("package.json", { + name: "workspaces-test", + private: true, + workspaces: ["packages/*"], + }); + json("lerna.json", { version: "1.0.0", npmClient: "npm" }); + json("packages/core/package.json", { + name: "@example/core", + version: "1.0.0", + }); + json("packages/ui/package.json", { + name: "@example/ui", + version: "1.0.0", + dependencies: { "@example/core": "^1.0.0" }, + }); + json("packages/private/package.json", { + name: "@example/private", + version: "1.0.0", + private: true, + }); + write("packages/core/index.js", "module.exports = 1;\n"); + run("npm", ["install", "--package-lock-only", "--ignore-scripts"]); + fs.symlinkSync( + path.join(__dirname, "node_modules"), + path.join(directory, "node_modules"), + "dir", + ); + } else { + json("package.json", { + name: "@example/cli", + version: "1.0.0", + type: "module", + main: "dist/index.js", + bin: { "example-cli": "bin/example-cli" }, + files: ["dist", "bin", "CHANGELOG.md"], + scripts: { build: "node build.js" }, + }); + write( + "bin/example-cli", + "#!/usr/bin/env node\nimport { version } from '../dist/index.js';\nconsole.log(version);\n", + ); + fs.chmodSync(path.join(directory, "bin/example-cli"), 0o755); + write( + "build.js", + "import fs from 'node:fs';\nconst { version } = JSON.parse(fs.readFileSync('package.json'));\nfs.mkdirSync('dist', { recursive: true });\nfs.writeFileSync('dist/index.js', 'export const version = ' + JSON.stringify(version) + ';');\n", + ); + run("npm", ["install", "--package-lock-only", "--ignore-scripts"]); + } + commit("chore: initial"); + git("tag", "-a", "v1.0.0", "-m", "v1.0.0"); + git("push", "origin", "main", "--tags"); + const requests = []; + const dispatches = []; + const labels = []; + const github = { + rest: { + issues: { + listForRepo: () => {}, + addLabels: async (args) => labels.push(args), + removeLabel: async (args) => labels.push(args), + }, + pulls: { + get: async ({ pull_number }) => ({ + data: requests.find((request) => request.number === pull_number), + }), + }, + actions: { + createWorkflowDispatch: async (args) => dispatches.push(args), + }, + }, + paginate: async () => + requests.map((request) => ({ number: request.number, pull_request: {} })), + }; + const context = { + repo: { owner: "example", repo: "project" }, + payload: { repository: { default_branch: "main" } }, + ref: "refs/heads/main", + sha: git("rev-parse", "HEAD"), + }; + const project = new ReleaseProject({ + workspace: directory, + versioning, + github, + context, + }); + const prepare = () => + project.prepare({ + workflowRef: + "example/project/.github/workflows/release.yml@refs/heads/main", + findRelease: async () => undefined, + }); + return { + root, + directory, + remote, + run, + git, + write, + json, + commit, + requests, + dispatches, + labels, + github, + context, + project, + prepare, + }; +} + +function releaseRequest(f, properties = {}) { + const request = { + number: 1, + state: "open", + html_url: "https://example.invalid/pull/1", + base: { ref: "main" }, + ...properties, + }; + f.requests.push(request); + return request; +} + +test("single package waits for its release PR without dispatching publication", async (t) => { + const f = fixture(t); + assert.equal((await f.prepare()).status, "no-changes"); + const request = releaseRequest(f); + assert.deepEqual(await f.prepare(), { + status: "awaiting-review", + url: request.html_url, + }); + assert.equal(f.dispatches.length, 0); +}); + +test("single package publishes the merged release commit and packs an executable CLI", async (t) => { + const f = fixture(t); + f.run("npm", [ + "version", + "1.1.0", + "--no-git-tag-version", + "--ignore-scripts", + ]); + f.write("CHANGELOG.md", "# 1.1.0\n\n* Add a command.\n"); + const releaseSha = f.commit("chore(main): release 1.1.0"); + releaseRequest(f, { + state: "closed", + merged_at: "2026-01-01T00:00:00Z", + merge_commit_sha: releaseSha, + }); + f.write("later.txt", "Later changes must not enter the release."); + f.context.sha = f.commit("feat: later change"); + f.git("push", "origin", "main"); + assert.deepEqual(await f.prepare(), { tag: "v1.1.0", status: "dispatched" }); + assert.equal(f.project.tagCommit("v1.1.0"), releaseSha); + assert.deepEqual(f.dispatches, [ + { + owner: "example", + repo: "project", + workflow_id: ".github/workflows/release.yml", + ref: "v1.1.0", + }, + ]); + assert.equal(fs.existsSync(path.join(f.directory, "later.txt")), false); + f.context.ref = "refs/tags/v1.1.0"; + f.context.sha = releaseSha; + const ready = await f.prepare(); + assert.equal(ready.status, "ready"); + assert.deepEqual(ready.packages, [ + { + name: "@example/cli", + version: "1.1.0", + directory: ".", + slug: "example-cli", + }, + ]); + f.run("npm", ["run", "build"]); + const [packed] = JSON.parse( + f.run("npm", ["pack", "--json", "--pack-destination", f.root]), + ); + const files = packed.files.map((file) => file.path); + for (const file of [ + "bin/example-cli", + "dist/index.js", + "CHANGELOG.md", + "package.json", + ]) + assert.ok(files.includes(file)); + const installed = path.join(f.root, "installed"); + fs.mkdirSync(installed); + f.run( + "npm", + ["install", path.join(f.root, packed.filename), "--ignore-scripts"], + installed, + ); + assert.equal( + f.run("node", [ + path.join(installed, "node_modules/@example/cli/bin/example-cli"), + ]), + "1.1.0", + ); + await new ReleaseArtifacts().markReleaseRequests({ + github: f.github, + context: f.context, + }); + assert.deepEqual( + f.labels.map((item) => item.labels || item.name), + [["autorelease: tagged"], "autorelease: pending"], + ); +}); + +test("a single-package tag collision preserves the remote tag", async (t) => { + const f = fixture(t); + f.write("CHANGELOG.md", "a merged PR with an already-used version"); + f.context.sha = f.commit("chore(main): release 1.0.0"); + releaseRequest(f, { + state: "closed", + merged_at: "2026-01-01T00:00:00Z", + merge_commit_sha: f.context.sha, + }); + const original = f.project.tagCommit("v1.0.0"); + await assert.rejects(f.prepare(), /another commit/); + assert.equal(f.project.tagCommit("v1.0.0"), original); + assert.equal(f.dispatches.length, 0); +}); + +test("unmerged or unrelated release PRs cannot mark a release as complete", async (t) => { + const f = fixture(t); + releaseRequest(f, { merge_commit_sha: f.context.sha }); + releaseRequest(f, { + number: 2, + state: "closed", + merged_at: "2026-01-01", + merge_commit_sha: "other", + }); + releaseRequest(f, { + number: 3, + state: "closed", + merged_at: "2026-01-01", + merge_commit_sha: f.context.sha, + base: { ref: "other" }, + }); + await new ReleaseArtifacts().markReleaseRequests({ + github: f.github, + context: f.context, + }); + assert.equal(f.labels.length, 0); +}); + +test("branch recovery completes PR labels after publication succeeded", async (t) => { + const f = fixture(t); + f.run("npm", [ + "version", + "1.0.1", + "--no-git-tag-version", + "--ignore-scripts", + ]); + f.context.sha = f.commit("chore(main): release 1.0.1"); + releaseRequest(f, { + state: "closed", + merged_at: "2026-01-01", + merge_commit_sha: f.context.sha, + }); + const result = await f.project.prepare({ + findRelease: async () => ({ + draft: false, + html_url: "https://example.invalid/release", + }), + }); + assert.equal(result.status, "released"); + assert.equal(f.labels.length, 2); + assert.equal(f.dispatches.length, 0); +}); + +for (const [message, version] of [ + ["fix: correct output", "1.0.1"], + ["feat: add output", "1.1.0"], + ["feat: change output\n\nBREAKING CHANGE: output is now structured", "2.0.0"], +]) { + test(`Lerna infers ${version}, updates dependent packages and resumes the tag`, async (t) => { + const f = fixture(t, "lerna"); + f.write("packages/core/index.js", "module.exports = 2;\n"); + f.context.sha = f.commit(message); + f.git("push", "origin", "main"); + assert.deepEqual(await f.prepare(), { + tag: `v${version}`, + status: "dispatched", + }); + assert.equal(f.project.readJson("lerna.json").version, version); + assert.equal( + f.project.readJson("packages/ui/package.json").dependencies[ + "@example/core" + ], + `^${version}`, + ); + assert.equal( + f.project.readJson("package-lock.json").packages["packages/core"].version, + version, + ); + assert.match( + fs.readFileSync( + path.join(f.directory, "packages/core/CHANGELOG.md"), + "utf8", + ), + new RegExp(version.replaceAll(".", "\\.")), + ); + f.context.sha = f.git("rev-parse", "HEAD"); + f.context.ref = `refs/tags/v${version}`; + const ready = await f.prepare(); + assert.equal(ready.status, "ready"); + assert.deepEqual(ready.packages.map((item) => item.name).sort(), [ + "@example/core", + "@example/ui", + ]); + assert.equal(f.git("rev-parse", "HEAD"), f.context.sha); + }); +} + +test("Lerna skips a commit that changes no packages", async (t) => { + const f = fixture(t, "lerna"); + f.write("README.md", "Documentation only."); + f.context.sha = f.commit("docs: describe setup"); + f.git("push", "origin", "main"); + assert.equal((await f.prepare()).status, "no-changes"); + assert.equal(f.dispatches.length, 0); +}); + +test("Lerna does not overwrite an existing version tag", async (t) => { + const f = fixture(t, "lerna"); + f.git("tag", "v1.1.0"); + f.git("push", "origin", "refs/tags/v1.1.0"); + const original = f.project.tagCommit("v1.1.0"); + f.write("packages/core/index.js", "module.exports = 2;\n"); + f.context.sha = f.commit("feat: add output"); + f.git("push", "origin", "main"); + await assert.rejects(f.prepare()); + assert.equal(f.project.tagCommit("v1.1.0"), original); + assert.equal( + f.run("git", ["rev-parse", "refs/heads/main"], f.remote), + f.context.sha, + ); +}); + +test("an advancing default branch rejects both the Lerna commit and tag push", async (t) => { + const f = fixture(t, "lerna"); + f.write("packages/core/index.js", "module.exports = 2;\n"); + f.context.sha = f.commit("feat: add output"); + f.git("push", "origin", "main"); + f.write("later.txt", "concurrent main commit"); + const advanced = f.commit("docs: update main"); + f.git("push", "origin", "main"); + await assert.rejects(f.prepare()); + assert.equal( + f.run("git", ["rev-parse", "refs/heads/main"], f.remote), + advanced, + ); + assert.throws(() => + f.run("git", ["rev-parse", "refs/tags/v1.1.0"], f.remote), + ); +}); + +test("published releases skip dispatch and tag recovery preserves their notes", async (t) => { + const f = fixture(t, "lerna"); + const release = { draft: false, html_url: "https://example.invalid/release" }; + const result = await f.project.prepare({ + workflowRef: "", + findRelease: async () => release, + }); + assert.equal(result.status, "released"); + assert.equal(f.dispatches.length, 0); + f.context.ref = "refs/tags/v1.0.0"; + assert.equal( + (await f.project.prepare({ findRelease: async () => release })).published, + true, + ); +}); + +test("rejects unsupported layouts, versions, refs and mismatched tags", async (t) => { + const f = fixture(t); + assert.throws( + () => new ReleaseProject({ workspace: f.directory, versioning: "unknown" }), + /versioning/, + ); + assert.throws( + () => + new ReleaseProject({ + workspace: f.directory, + workingDirectory: "..", + versioning: "release-please", + }), + /inside/, + ); + f.context.ref = "refs/heads/feature"; + await assert.rejects(f.prepare(), /default branch/); + f.context.ref = "refs/tags/v9.0.0"; + await assert.rejects(f.prepare(), /committed package version/); + f.json("package.json", { name: "private", private: true, version: "1.0.0" }); + assert.throws( + () => + new ReleaseProject({ + workspace: f.directory, + versioning: "release-please", + }), + /one public package/, + ); + f.json("lerna.json", { version: "independent" }); + assert.throws( + () => new ReleaseProject({ workspace: f.directory, versioning: "lerna" }), + /stable version/, + ); +}); From 24737e2e4fed6923b599a505bc3aa8d478897bc9 Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Sat, 31 Oct 2026 11:35:02 +0100 Subject: [PATCH 2/4] refactor(release): separate CI checks from publication --- .github/workflows/release.md | 23 +++---- .github/workflows/release.yml | 55 +++++++--------- actions/release/ReleaseBuild.js | 57 +++++++++++++++++ actions/release/build/README.md | 78 +++++++++++++++++++++++ actions/release/build/action.yml | 44 +++++++++++++ docs/README.md | 1 + docs/adr/0001-release-orchestration.md | 3 + docs/adr/0002-separate-ci-from-release.md | 38 +++++++++++ docs/development/release-tests.md | 2 + docs/usage/custom-releases.md | 49 +++++++------- docs/usage/examples/reactionable-cli.yml | 2 - docs/usage/examples/reactionable.yml | 2 - docs/usage/releases.md | 32 +++++++--- tests/release/build.test.js | 60 +++++++++++++++++ 14 files changed, 367 insertions(+), 79 deletions(-) create mode 100644 actions/release/ReleaseBuild.js create mode 100644 actions/release/build/README.md create mode 100644 actions/release/build/action.yml create mode 100644 docs/adr/0002-separate-ci-from-release.md create mode 100644 tests/release/build.test.js diff --git a/.github/workflows/release.md b/.github/workflows/release.md index 3d1bd3e..0f68d5c 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -26,8 +26,9 @@ Release a single npm package or fixed-version Lerna workspaces without consumer Keep workflow_dispatch and schedule triggers in the calling workflow. Release Please prepares a release PR; merge it and run again to publish. Lerna prepares and atomically pushes versions and changelogs directly. -Preparation dispatches the caller workflow at the release tag so CI, packaging +Preparation dispatches the caller workflow at the release tag so building, packaging and npm provenance all refer to the same committed source. +Keep linting, tests and security analysis in the caller's separate CI workflow. See [the release guide](../../docs/usage/releases.md) for setup, examples and recovery. ### Permissions @@ -38,7 +39,6 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re - **`issues`**: `write` - **`packages`**: `read` - **`pull-requests`**: `write` -- **`security-events`**: `write` @@ -47,16 +47,13 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re ### Workflow Call Inputs -| **Input** | **Description** | **Required** | **Type** | **Default** | -| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -------------------------------------------- | -| **`versioning`** | Version preparation tool. Use release-please for a single public package or lerna for fixed-version workspaces. Versions are inferred from Conventional Commits. | **false** | **string** | `release-please` | -| **`working-directory`** | Project directory relative to the caller repository root. | **false** | **string** | `.` | -| **`build`** | Build commands and artifact paths, using the continuous-integration workflow's build format. Artifacts must include the package's compiled files. | **false** | **string** | `{"commands":["build"],"artifact":["dist"]}` | -| **`lint`** | Lint options forwarded to continuous-integration. Set to an empty string to disable. | **false** | **string** | `true` | -| **`test`** | Test options forwarded to continuous-integration. Set to an empty string to disable. | **false** | **string** | `true` | -| **`code-ql`** | CodeQL options forwarded to continuous-integration. Set to an empty string to disable. | **false** | **string** | `typescript` | -| **`ai-summary`** | Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. | **false** | **boolean** | `false` | -| **`environment`** | GitHub environment for source preparation, AI and npm publishing. It must permit the default branch and release tags. Configure npm trusted publishing for this environment and the calling workflow. | **false** | **string** | `release` | +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -------------------------------------------- | +| **`versioning`** | Version preparation tool. Use release-please for a single public package or lerna for fixed-version workspaces. Versions are inferred from Conventional Commits. | **false** | **string** | `release-please` | +| **`working-directory`** | Project directory relative to the caller repository root. | **false** | **string** | `.` | +| **`build`** | JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifacts must include the package's compiled files. Builds the tagged source without running CI checks. | **false** | **string** | `{"commands":["build"],"artifact":["dist"]}` | +| **`ai-summary`** | Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. | **false** | **boolean** | `false` | +| **`environment`** | GitHub environment for source preparation, AI and npm publishing. It must permit the default branch and release tags. Configure npm trusted publishing for this environment and the calling workflow. | **false** | **string** | `release` | @@ -78,7 +75,7 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re | **`status`** | no-changes, awaiting-review, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. | | **`tag`** | Prepared release tag. | | **`release-url`** | Published GitHub release URL from the tag run. | -| **`build-artifact-id`** | CI build artifact ID from the tag run, reusable for documentation deployment. | +| **`build-artifact-id`** | Release build artifact ID from the tag run, reusable for documentation deployment. | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1c7313..ce1896b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,8 +3,9 @@ # Keep workflow_dispatch and schedule triggers in the calling workflow. # Release Please prepares a release PR; merge it and run again to publish. # Lerna prepares and atomically pushes versions and changelogs directly. -# Preparation dispatches the caller workflow at the release tag so CI, packaging +# Preparation dispatches the caller workflow at the release tag so building, packaging # and npm provenance all refer to the same committed source. +# Keep linting, tests and security analysis in the caller's separate CI workflow. # See [the release guide](../../docs/usage/releases.md) for setup, examples and recovery. name: Node.js Release @@ -20,21 +21,9 @@ on: type: string default: . build: - description: Build commands and artifact paths, using the continuous-integration workflow's build format. Artifacts must include the package's compiled files. + description: JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifacts must include the package's compiled files. Builds the tagged source without running CI checks. type: string default: '{"commands":["build"],"artifact":["dist"]}' - lint: - description: Lint options forwarded to continuous-integration. Set to an empty string to disable. - type: string - default: "true" - test: - description: Test options forwarded to continuous-integration. Set to an empty string to disable. - type: string - default: "true" - code-ql: - description: CodeQL options forwarded to continuous-integration. Set to an empty string to disable. - type: string - default: typescript ai-summary: description: Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. type: boolean @@ -61,8 +50,8 @@ on: description: Published GitHub release URL from the tag run. value: ${{ jobs.publish.outputs.url }} build-artifact-id: - description: CI build artifact ID from the tag run, reusable for documentation deployment. - value: ${{ jobs.ci.outputs.build-artifact-id }} + description: Release build artifact ID from the tag run, reusable for documentation deployment. + value: ${{ jobs.build.outputs.artifact-id }} permissions: {} @@ -103,26 +92,30 @@ jobs: working-directory: ${{ inputs.working-directory }} github-token: ${{ secrets.github-token || github.token }} - ci: + build: + name: Build release artifacts needs: prepare if: needs.prepare.outputs.status == 'ready' - uses: $/.github/workflows/continuous-integration.yml + runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read packages: read - pull-requests: write - id-token: write - security-events: write - with: - working-directory: ${{ inputs.working-directory }} - build: ${{ inputs.build }} - lint: ${{ inputs.lint }} - test: ${{ inputs.test }} - code-ql: ${{ inputs.code-ql }} + outputs: + artifact-id: ${{ steps.build.outputs.artifact-id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - id: build + uses: $/actions/release/build + with: + working-directory: ${{ inputs.working-directory }} + build: ${{ inputs.build }} draft: name: Draft release notes - needs: [prepare, ci] + needs: [prepare, build] runs-on: ubuntu-latest timeout-minutes: 20 environment: ${{ inputs.environment }} @@ -147,7 +140,7 @@ jobs: packages: name: Publish ${{ matrix.package.name }} - needs: [prepare, ci, draft] + needs: [prepare, build, draft] runs-on: ubuntu-latest timeout-minutes: 30 environment: ${{ inputs.environment }} @@ -160,13 +153,13 @@ jobs: matrix: package: ${{ fromJSON(needs.prepare.outputs.packages) }} steps: - - name: Package the tested release commit + - name: Package the tagged release commit id: package uses: $/actions/package with: working-directory: ${{ inputs.working-directory }} package-directory: ${{ matrix.package.directory }} - build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} + build-artifact-id: ${{ needs.build.outputs.artifact-id }} build-artifact-path: / artifact-name: npm-${{ matrix.package.slug }}-${{ github.run_attempt }} - name: Validate publication diff --git a/actions/release/ReleaseBuild.js b/actions/release/ReleaseBuild.js new file mode 100644 index 0000000..db9ac18 --- /dev/null +++ b/actions/release/ReleaseBuild.js @@ -0,0 +1,57 @@ +const { randomUUID } = require("node:crypto"); +const path = require("node:path"); + +class ReleaseBuild { + constructor(workingDirectory) { + this.workingDirectory = workingDirectory; + } + + configure(input) { + const build = JSON.parse(input); + if (!build || Array.isArray(build) || typeof build !== "object") { + throw new Error("build must be a JSON object."); + } + const commands = ReleaseBuild.strings( + build.commands ?? ["build"], + "commands", + ); + const env = build.env ?? {}; + if (Array.isArray(env) || typeof env !== "object") { + throw new Error("build.env must be an object."); + } + const artifactPaths = build.artifact?.paths ?? build.artifact; + const paths = ReleaseBuild.strings( + typeof artifactPaths === "string" + ? artifactPaths.split("\n") + : artifactPaths, + "artifact paths", + ).map((artifact) => { + // Keep the complete path when upload-artifact strips the glob prefix. + // Package and documentation jobs restore this artifact to /. + return path + .resolve(this.workingDirectory, artifact) + .replace(/^\/([^/]+)/, "/*$1"); + }); + return { + commands: commands.join("\n"), + env: JSON.stringify(env), + artifact: JSON.stringify({ + name: `release-build-${randomUUID()}`, + paths: paths.join("\n"), + }), + }; + } + + static strings(value, name) { + if ( + !Array.isArray(value) || + !value.length || + value.some((item) => typeof item !== "string" || !item.trim()) + ) { + throw new Error(`build.${name} must contain nonempty strings.`); + } + return value.map((item) => item.trim()); + } +} + +module.exports = { ReleaseBuild }; diff --git a/actions/release/build/README.md b/actions/release/build/README.md new file mode 100644 index 0000000..b459ef4 --- /dev/null +++ b/actions/release/build/README.md @@ -0,0 +1,78 @@ + + +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItcGFja2FnZSIgY29sb3I9ImJsdWUiPjxsaW5lIHgxPSIxNi41IiB5MT0iOS40IiB4Mj0iNy41IiB5Mj0iNC4yMSI+PC9saW5lPjxwYXRoIGQ9Ik0yMSAxNlY4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNnoiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIzLjI3IDYuOTYgMTIgMTIuMDEgMjAuNzMgNi45NiI+PC9wb2x5bGluZT48bGluZSB4MT0iMTIiIHkxPSIyMi4wOCIgeDI9IjEyIiB5Mj0iMTIiPjwvbGluZT48L3N2Zz4=) GitHub Action: Build Node.js release artifacts + +
+ Build Node.js release artifacts +
+ +--- + + + + +[![Marketplace](https://img.shields.io/badge/Marketplace-build--node.js--release--artifacts-blue?logo=github-actions)](https://github.com/marketplace/actions/build-node.js-release-artifacts) +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Build the checked-out release source and preserve artifact paths for packaging, independently of CI checks. +See the [release guide](../../../docs/usage/releases.md) for configuration and the CI boundary. + + + + +## Inputs + +| **Input** | **Description** | **Required** | **Default** | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------- | +| **`working-directory`** | Project directory relative to the caller repository root. Check out the release source before calling this action. | **false** | `.` | +| **`build`** | JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifact paths are relative to the project directory or absolute. | **false** | `{"commands":["build"],"artifact":["dist"]}` | + + + + + + +## Outputs + +| **Output** | **Description** | +| ----------------- | --------------------------------------------------------------------------------------------------------- | +| **`artifact-id`** | Build artifact ID. Download to / to restore the original paths for packaging or documentation deployment. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/actions/release/build/action.yml b/actions/release/build/action.yml new file mode 100644 index 0000000..1163dc1 --- /dev/null +++ b/actions/release/build/action.yml @@ -0,0 +1,44 @@ +name: Build Node.js release artifacts +description: | + Build the checked-out release source and preserve artifact paths for packaging, independently of CI checks. + See the [release guide](../../../docs/usage/releases.md) for configuration and the CI boundary. +author: hoverkraft +branding: + icon: package + color: blue +inputs: + working-directory: + description: Project directory relative to the caller repository root. Check out the release source before calling this action. + default: . + build: + description: JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifact paths are relative to the project directory or absolute. + default: '{"commands":["build"],"artifact":["dist"]}' +outputs: + artifact-id: + description: Build artifact ID. Download to / to restore the original paths for packaging or documentation deployment. + value: ${{ steps.build.outputs.artifact-id }} +runs: + using: composite + steps: + - id: working-directory + uses: hoverkraft-tech/ci-github-common/actions/working-directory@44f6108cef0e9884ea7735f3d62932c2310745d4 # 0.39.1 + with: + working-directory: ${{ inputs.working-directory }} + - id: configuration + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + BUILD_INPUT: ${{ inputs.build }} + WORKING_DIRECTORY: ${{ steps.working-directory.outputs.absolute-path }} + with: + script: | + const { ReleaseBuild } = require(`${process.env.ACTION_PATH}/../ReleaseBuild.js`); + const configuration = new ReleaseBuild(process.env.WORKING_DIRECTORY).configure(process.env.BUILD_INPUT); + for (const [name, value] of Object.entries(configuration)) core.setOutput(name, value); + - id: build + uses: $/actions/build + with: + working-directory: ${{ steps.working-directory.outputs.absolute-path }} + build-commands: ${{ steps.configuration.outputs.commands }} + build-env: ${{ steps.configuration.outputs.env }} + build-artifact: ${{ steps.configuration.outputs.artifact }} diff --git a/docs/README.md b/docs/README.md index 7e2ff81..4945628 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,3 +4,4 @@ - [Compose a custom release pipeline](usage/custom-releases.md) - [Validate release automation](development/release-tests.md) - [Release orchestration decision](adr/0001-release-orchestration.md) +- [Separation of CI and release](adr/0002-separate-ci-from-release.md) diff --git a/docs/adr/0001-release-orchestration.md b/docs/adr/0001-release-orchestration.md index 879bcce..5011ffc 100644 --- a/docs/adr/0001-release-orchestration.md +++ b/docs/adr/0001-release-orchestration.md @@ -3,6 +3,9 @@ - Status: accepted - Date: 2026-10-29 +The decision to run CI inside release is superseded by +[ADR 0002](0002-separate-ci-from-release.md). The other decisions remain accepted. + ## Context The Package and Publish actions handle a single tarball. Consumers still need diff --git a/docs/adr/0002-separate-ci-from-release.md b/docs/adr/0002-separate-ci-from-release.md new file mode 100644 index 0000000..68e6d00 --- /dev/null +++ b/docs/adr/0002-separate-ci-from-release.md @@ -0,0 +1,38 @@ +# ADR 0002: Separate CI checks from release publication + +- Status: accepted +- Date: 2026-10-31 +- Partially supersedes: [ADR 0001](0001-release-orchestration.md) + +## Context + +ADR 0001 made the release workflow invoke the full continuous-integration +workflow. That coupled publication to lint, test, coverage and security settings, +repeated checks already owned by consumer CI, and required unrelated permissions. + +Building release artifacts still depends on release preparation: compiled output +can embed the package version, and packaging and provenance must use the tagged +source. Reusing a pre-version CI artifact would break that relationship. + +## Decision + +Keep linting, tests, coverage and security analysis in independent consumer CI. +Remove their inputs and the nested CI invocation from the release workflow. +Keep a dedicated build job that delegates compilation to the existing Build +action. A small release build action translates the release build configuration +and preserves artifact paths for packaging and documentation deployment. + +Release retains version preparation, tagged builds, changelogs, optional AI notes, +publication and recovery. Consumers retain CI policy and release environment +rules. The release workflow does not query or wait for CI status. + +## Consequences + +CI changes no longer change the release workflow's validation settings. The build +job needs only contents and packages read permissions. A release build is not +proof that tests passed on the tag; consumers must enforce their own checks. +This matters for Lerna's generated version commit, which is pushed directly. + +Artifact output and recovery contracts remain unchanged. A failed release build +stops publication, and retries rebuild the original tag. Manual and scheduled +triggers and automatic Conventional Commit version selection remain unchanged. diff --git a/docs/development/release-tests.md b/docs/development/release-tests.md index 2d26328..8ce0ac3 100644 --- a/docs/development/release-tests.md +++ b/docs/development/release-tests.md @@ -7,6 +7,8 @@ npm ci --prefix tests/release npm test --prefix tests/release ``` +Build configuration tests check script/environment forwarding, artifact paths +for single packages and nested workspaces, retry names, and invalid inputs. The suite uses temporary local Git repositories and offline npm commands. It executes the installed Lerna, checks dependent package and lockfile changes, and tests no-change behavior, tag collisions and concurrent branch pushes. A single diff --git a/docs/usage/custom-releases.md b/docs/usage/custom-releases.md index 8abf4db..b9f5452 100644 --- a/docs/usage/custom-releases.md +++ b/docs/usage/custom-releases.md @@ -2,7 +2,7 @@ Publish a package to npm and attach the same tarball to a GitHub release: -**Plan version → Run CI → Package build output → Publish to npm → Create GitHub release** +**Plan version → Build release artifacts → Package → Publish to npm → Create GitHub release** The workflow below releases a single package from the repository root using stable versions such as `1.2.3`. It sets the version during packaging. If your build reads the package version or you commit version changes, @@ -17,10 +17,12 @@ use [Commit the version before building](#commit-the-version-before-building). The example uses GitHub-hosted runners and `id-token: write`; no npm token is needed. For a new npm package, publish its first version before configuring the trusted publisher. 4. Save the workflow below as `.github/workflows/release.yml` in your project. Before running it: - - Replace every `` with the **same `ci-github-nodejs` commit containing `actions/publish`**. + - Replace every `` with the **same `ci-github-nodejs` commit containing `actions/release/build` and `actions/publish`**. The `ci-github-publish` actions are already pinned to `1.0.3`. - - Set the `build`, `lint:ci`, and `test:ci` script names and the `dist/` output path to match your project. - See [CI options](../../.github/workflows/continuous-integration.md) if you need a different setup. + - Set the `build` script name and `dist/` output path to match your project. +5. Run linting, tests and security analysis in a separate + [CI workflow](../../.github/workflows/continuous-integration.md), with required checks + on pull requests and branch rules. This release workflow does not run or wait for CI. ## Workflow @@ -73,23 +75,26 @@ jobs: } core.setOutput('version', version); - ci: + build: needs: plan if: needs.plan.outputs.should-release == 'true' - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@ + runs-on: ubuntu-latest permissions: contents: read packages: read - pull-requests: write - id-token: write - security-events: write - with: - build: '{"commands":["build"],"artifact":{"paths":["dist/"]}}' - lint: '{"command":"lint:ci"}' - test: '{"command":"test:ci"}' + outputs: + artifact-id: ${{ steps.build.outputs.artifact-id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - id: build + uses: hoverkraft-tech/ci-github-nodejs/actions/release/build@ + with: + build: '{"commands":["build"],"artifact":["dist"]}' release: - needs: [plan, ci] + needs: [plan, build] if: needs.plan.outputs.should-release == 'true' runs-on: ubuntu-latest permissions: @@ -103,8 +108,8 @@ jobs: uses: hoverkraft-tech/ci-github-nodejs/actions/package@ with: version: ${{ needs.plan.outputs.version }} - build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} - # The reusable CI workflow preserves absolute paths in build artifacts. + build-artifact-id: ${{ needs.build.outputs.artifact-id }} + # The release build preserves absolute paths in build artifacts. build-artifact-path: / # Add any package smoke tests before publishing. @@ -139,7 +144,7 @@ jobs: publish: "true" ``` -[Package](../../actions/package/README.md) checks out the source, installs dependencies, restores the CI build output, +[Package](../../actions/package/README.md) checks out the source, installs dependencies, restores the release build output, and creates the tarball. [Publish](../../actions/publish/README.md) publishes that tarball. The package version is changed locally; it is not committed. GitHub's source archive therefore keeps the version from the source commit. If `package.json` already contains the planned version, omit the Package action's `version` input. @@ -196,9 +201,9 @@ Releases are serialized so two runs cannot publish the same package concurrently | Situation | Result | | ---------------------------------------------- | ---------------------------------------------------------------------------------- | -| Relevant changes since the last GitHub release | Plan the next version, run CI, and release. | -| No relevant changes | Skip CI and publishing successfully. | -| Manual run with `force` checked | Release the planned version even without changes; CI must still pass. | +| Relevant changes since the last GitHub release | Plan the next version, build, and release. | +| No relevant changes | Skip building and publishing successfully. | +| Manual run with `force` checked | Release the planned version even without changes; the build must still pass. | | No previous matching GitHub release | Plan the first GitHub release. Registry authentication must already be configured. | The dry run checks the tarball with `npm publish --dry-run`. It does not verify registry credentials or trusted publishing access. @@ -211,7 +216,7 @@ Use this when the build embeds the version or the released source must include u 1. Plan the version with `release/plan`, using the same no-change and `force` rules as the workflow above. 2. Update the version on a release branch, for example with `npm version 1.2.3 --no-git-tag-version`. Commit the package manifest, lockfile, changelog, and any generated version files through a pull request. -3. After merging, run CI and packaging **from the merged commit**. Omit the Package action's `version` input; the version is already in `package.json`. +3. After merging, run the release build and packaging **from the merged commit**. Omit the Package action's `version` input; the version is already in `package.json`. 4. Publish the tarball, then create the GitHub release with the planned `tag` and merged commit as `target-sha`. Keep the planned tag throughout this process; do not calculate another version after merging. @@ -230,7 +235,7 @@ for automating the pull request and merge. - **Prereleases:** set `prerelease: "true"` on `release/plan` and `release/create`, and `tag: next` on both Publish steps. Update the version conversion to accept your full prerelease version, such as `1.2.3-rc.1`. The npm distribution tag (`next`) is separate from the Git release tag (`v1.2.3-rc.1`). -- **Monorepos:** scope CI and both release actions with `working-directory`. +- **Monorepos:** scope the build and both release actions with `working-directory`. For Package, set `working-directory` to the dependency installation root and `package-directory` to the package's relative path. Convert prefixed Git tags such as `my-package/v1.2.3` to npm versions, and use a separate concurrency group and artifact name per package. - **Drafts and signing:** create a draft with `publish: "false"`, then use `release/update` to attach assets and publish after npm publication succeeds. diff --git a/docs/usage/examples/reactionable-cli.yml b/docs/usage/examples/reactionable-cli.yml index 074e415..9595b3f 100644 --- a/docs/usage/examples/reactionable-cli.yml +++ b/docs/usage/examples/reactionable-cli.yml @@ -17,14 +17,12 @@ jobs: issues: write pull-requests: write packages: read - security-events: write id-token: write with: versioning: release-please environment: github-pages build: | {"commands":["build","build:doc"],"artifact":["dist","build"]} - test: '{"coverage":"codecov"}' ai-summary: true secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/docs/usage/examples/reactionable.yml b/docs/usage/examples/reactionable.yml index 42f6285..6bdd537 100644 --- a/docs/usage/examples/reactionable.yml +++ b/docs/usage/examples/reactionable.yml @@ -17,14 +17,12 @@ jobs: issues: write pull-requests: write packages: read - security-events: write id-token: write with: versioning: lerna environment: github-pages build: | {"commands":["build:ci","storybook:build","docs:build"],"artifact":["packages/**/lib","build","packages/**/docs"]} - test: '{"coverage":"codecov"}' ai-summary: true secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/docs/usage/releases.md b/docs/usage/releases.md index 0c0d803..8f5a1f2 100644 --- a/docs/usage/releases.md +++ b/docs/usage/releases.md @@ -2,8 +2,8 @@ Use the [Release workflow](../../.github/workflows/release.md) to release one public npm package or fixed-version Lerna workspaces. The calling repository supplies -triggers and build settings. HoverKraft prepares versions, runs CI on the release -commit, packages the build output, publishes to npm and creates the GitHub release. +triggers and build settings. HoverKraft prepares versions, builds the tagged +source, packages the build output, publishes to npm and creates the GitHub release. No custom release scripts are needed in the calling repository. Complete caller examples, including documentation deployment: @@ -31,6 +31,19 @@ GitHub-hosted Linux runners and public packages on npmjs.org are supported. Private packages, custom registries, prereleases and independent Lerna versions require a [custom pipeline](custom-releases.md). +## Keep CI separate + +Run linting, tests, coverage and security analysis in the caller's independent +[CI workflow](../../.github/workflows/continuous-integration.md). Configure required +checks on pull requests and branch rules before allowing releases from that branch. +The Release workflow neither runs those checks nor queries or waits for CI results. + +Release builds run from the version tag because package contents can embed the +version. Their artifacts are used for npm packaging and optional documentation +deployment. They are not evidence that CI passed on that tag. In particular, Lerna +creates a version commit directly; if it needs additional validation, arrange it +in the caller and gate publication through the release environment. + ## Single package This configuration fits a TypeScript CLI such as `reactionable-cli`. Release @@ -57,7 +70,6 @@ jobs: issues: write pull-requests: write packages: read - security-events: write id-token: write with: versioning: release-please @@ -74,7 +86,9 @@ jobs: The defaults use Release Please and `npm run build` with `dist` as the build artifact. Omit `versioning` and `build` when those defaults match your project. -The existing CI workflow's `lint`, `test` and `code-ql` options are also available. +The `build` input is a JSON object with `commands`, `artifact` paths and an optional +`env` object. Commands are package scripts, not shell commands. The Build action +installs dependencies and executes those scripts. GitHub must allow Actions to create PRs. Release PRs created with `GITHUB_TOKEN` do not trigger normal PR workflows. If required checks depend on those events, @@ -96,7 +110,6 @@ with: "commands": ["build:ci", "storybook:build", "docs:build"], "artifact": ["packages/**/lib", "build", "packages/**/docs"] } - test: '{"coverage":"codecov"}' ai-summary: true ``` @@ -119,7 +132,7 @@ Preparation reports one of these outcomes in the job summary: | Status | Meaning | | ----------------- | ------------------------------------------------------------- | -| `no-changes` | Nothing needs releasing; no CI or publishing starts. | +| `no-changes` | Nothing needs releasing; no build or publishing starts. | | `awaiting-review` | Merge the linked Release Please PR, then run again. | | `dispatched` | The version tag exists; follow the new run for that tag. | | `released` | This version already has a published GitHub release. | @@ -127,7 +140,7 @@ Preparation reports one of these outcomes in the job summary: Both preparation and publication share a concurrency group inside the reusable workflow. Do not use that same group in the caller. A successful preparation run -does not imply publication succeeded: its tag run performs CI and publication. +does not imply publication succeeded: its tag run builds artifacts and publishes. AI generation runs before npm publication. A draft becomes public only after every package and release asset succeeds. AI summaries accompany the full release @@ -141,14 +154,15 @@ gh workflow run release.yml --ref v1.2.3 ``` The tag must contain the caller workflow and match the committed version. A tag -run never computes another version. CI rebuilds that source. Already published npm +run never computes another version. The release build uses that source. Already published npm versions are skipped and their original tarballs are downloaded with SHA-512 verification. Existing GitHub assets are reused only when SHA-256 digests match; conflicting or missing digests fail instead of replacing assets. npm workspace publication cannot be atomic, so partial publication is possible. An npm registry error stops recovery; only an absent version permits publishing. -If CI fails because source code needs changing, make a new commit and release. +If the release build fails because source code needs changing, make a new commit +and release. Do not move an existing release tag. The `release-url`, `tag`, `status` and `build-artifact-id` workflow outputs support diff --git a/tests/release/build.test.js b/tests/release/build.test.js new file mode 100644 index 0000000..20ebd4f --- /dev/null +++ b/tests/release/build.test.js @@ -0,0 +1,60 @@ +const assert = require("node:assert/strict"); +const { test } = require("node:test"); +const { ReleaseBuild } = require("../../actions/release/ReleaseBuild.js"); + +const configure = (build, directory = "/home/runner/work/project/project") => + new ReleaseBuild(directory).configure(JSON.stringify(build)); + +test("builds package and documentation scripts with their environment", () => { + const options = configure({ + commands: [" build ", "build:doc"], + env: { NODE_ENV: "production" }, + artifact: ["dist", "build/docs"], + }); + assert.equal(options.commands, "build\nbuild:doc"); + assert.deepEqual(JSON.parse(options.env), { NODE_ENV: "production" }); + assert.equal( + JSON.parse(options.artifact).paths, + "/*home/runner/work/project/project/dist\n/*home/runner/work/project/project/build/docs", + ); +}); + +test("preserves nested workspace output paths when restoring artifacts to root", () => { + const options = configure( + { artifact: { paths: ["packages/**/lib", "build"] } }, + "/home/runner/work/project/project/node", + ); + assert.equal(options.commands, "build"); + assert.equal(options.env, "{}"); + assert.equal( + JSON.parse(options.artifact).paths, + "/*home/runner/work/project/project/node/packages/**/lib\n/*home/runner/work/project/project/node/build", + ); +}); + +test("accepts multiline and absolute artifact paths and gives retries unique names", () => { + const input = { artifact: "dist\n/home/runner/work/project/project/build" }; + const first = JSON.parse(configure(input).artifact); + const second = JSON.parse(configure(input).artifact); + assert.equal( + first.paths, + "/*home/runner/work/project/project/dist\n/*home/runner/work/project/project/build", + ); + assert.notEqual(first.name, second.name); +}); + +test("rejects invalid build configuration before executing scripts", () => { + assert.throws(() => new ReleaseBuild("/tmp").configure("{"), SyntaxError); + for (const input of [null, [], "build", true]) { + assert.throws(() => configure(input), /JSON object/); + } + for (const commands of [[], "build", [1], [" "]]) { + assert.throws(() => configure({ commands, artifact: "dist" }), /commands/); + } + for (const env of [[], "production", 1]) { + assert.throws(() => configure({ env, artifact: "dist" }), /env/); + } + for (const artifact of [undefined, {}, [], "", [false], { paths: 1 }]) { + assert.throws(() => configure({ artifact }), /artifact paths/); + } +}); From af30480705546120527de2c59130e2ebc4182a2a Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Sat, 31 Oct 2026 12:18:46 +0100 Subject: [PATCH 3/4] refactor(release): use HoverKraft planning and publication actions --- .github/workflows/release.md | 29 +- .github/workflows/release.yml | 19 +- actions/release/ReleaseArtifacts.js | 50 +--- actions/release/ReleaseProject.js | 177 ++++++----- actions/release/finalize/README.md | 13 +- actions/release/finalize/action.yml | 29 +- actions/release/prepare/README.md | 24 +- actions/release/prepare/action.yml | 62 ++-- docs/README.md | 1 + docs/adr/0001-release-orchestration.md | 4 +- docs/adr/0003-hoverkraft-release-actions.md | 51 ++++ docs/development/release-tests.md | 9 +- docs/usage/examples/reactionable-cli.yml | 4 +- docs/usage/examples/reactionable.yml | 3 +- docs/usage/releases.md | 74 +++-- tests/release/artifacts.test.js | 49 +++- tests/release/project.test.js | 309 ++++++++++---------- 17 files changed, 500 insertions(+), 407 deletions(-) create mode 100644 docs/adr/0003-hoverkraft-release-actions.md diff --git a/.github/workflows/release.md b/.github/workflows/release.md index 0f68d5c..d1a407a 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -24,8 +24,8 @@ Release a single npm package or fixed-version Lerna workspaces without consumer scripts. Keep workflow_dispatch and schedule triggers in the calling workflow. -Release Please prepares a release PR; merge it and run again to publish. -Lerna prepares and atomically pushes versions and changelogs directly. +HoverKraft plans the release from Conventional Commits. npm or Lerna applies +the plan before the version commit and tag are pushed atomically. Preparation dispatches the caller workflow at the release tag so building, packaging and npm provenance all refer to the same committed source. Keep linting, tests and security analysis in the caller's separate CI workflow. @@ -36,9 +36,8 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re - **`actions`**: `write` - **`contents`**: `write` - **`id-token`**: `write` -- **`issues`**: `write` - **`packages`**: `read` -- **`pull-requests`**: `write` +- **`pull-requests`**: `read` @@ -49,7 +48,7 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re | **Input** | **Description** | **Required** | **Type** | **Default** | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -------------------------------------------- | -| **`versioning`** | Version preparation tool. Use release-please for a single public package or lerna for fixed-version workspaces. Versions are inferred from Conventional Commits. | **false** | **string** | `release-please` | +| **`versioning`** | Tool applying the HoverKraft release plan. Use npm for a single public package or lerna for fixed-version workspaces. HoverKraft infers versions from Conventional Commits in both modes. | **false** | **string** | `npm` | | **`working-directory`** | Project directory relative to the caller repository root. | **false** | **string** | `.` | | **`build`** | JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifacts must include the package's compiled files. Builds the tagged source without running CI checks. | **false** | **string** | `{"commands":["build"],"artifact":["dist"]}` | | **`ai-summary`** | Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. | **false** | **boolean** | `false` | @@ -60,22 +59,22 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re ## Secrets -| **Secret** | **Description** | **Required** | -| -------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------ | -| **`OPENAI_API_KEY`** | OpenAI API key, required when ai-summary is enabled. | **false** | -| **`github-token`** | Optional GitHub App token for release PR CI. Defaults to GITHUB_TOKEN; see the guide for its PR event restrictions. | **false** | +| **Secret** | **Description** | **Required** | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| **`OPENAI_API_KEY`** | OpenAI API key, required when ai-summary is enabled. | **false** | +| **`github-token`** | Optional token for pushing release commits and dispatching publication. Defaults to GITHUB_TOKEN. Its identity must be permitted by branch and tag rules. | **false** | ## Outputs -| **Output** | **Description** | -| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| **`status`** | no-changes, awaiting-review, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. | -| **`tag`** | Prepared release tag. | -| **`release-url`** | Published GitHub release URL from the tag run. | -| **`build-artifact-id`** | Release build artifact ID from the tag run, reusable for documentation deployment. | +| **Output** | **Description** | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| **`status`** | no-changes, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. | +| **`tag`** | Prepared release tag. | +| **`release-url`** | Published GitHub release URL from the tag run. | +| **`build-artifact-id`** | Release build artifact ID from the tag run, reusable for documentation deployment. | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce1896b..556f6c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,8 @@ # Release a single npm package or fixed-version Lerna workspaces without consumer scripts. # # Keep workflow_dispatch and schedule triggers in the calling workflow. -# Release Please prepares a release PR; merge it and run again to publish. -# Lerna prepares and atomically pushes versions and changelogs directly. +# HoverKraft plans the release from Conventional Commits. npm or Lerna applies +# the plan before the version commit and tag are pushed atomically. # Preparation dispatches the caller workflow at the release tag so building, packaging # and npm provenance all refer to the same committed source. # Keep linting, tests and security analysis in the caller's separate CI workflow. @@ -13,9 +13,9 @@ on: workflow_call: inputs: versioning: - description: Version preparation tool. Use release-please for a single public package or lerna for fixed-version workspaces. Versions are inferred from Conventional Commits. + description: Tool applying the HoverKraft release plan. Use npm for a single public package or lerna for fixed-version workspaces. HoverKraft infers versions from Conventional Commits in both modes. type: string - default: release-please + default: npm working-directory: description: Project directory relative to the caller repository root. type: string @@ -37,11 +37,11 @@ on: description: OpenAI API key, required when ai-summary is enabled. required: false github-token: - description: Optional GitHub App token for release PR CI. Defaults to GITHUB_TOKEN; see the guide for its PR event restrictions. + description: Optional token for pushing release commits and dispatching publication. Defaults to GITHUB_TOKEN. Its identity must be permitted by branch and tag rules. required: false outputs: status: - description: no-changes, awaiting-review, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. + description: no-changes, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. value: ${{ jobs.publish.outputs.status || jobs.prepare.outputs.status }} tag: description: Prepared release tag. @@ -68,8 +68,7 @@ jobs: permissions: actions: write contents: write - issues: write - pull-requests: write + pull-requests: read outputs: status: ${{ steps.prepare.outputs.status }} tag: ${{ steps.prepare.outputs.tag }} @@ -202,8 +201,7 @@ jobs: environment: ${{ inputs.environment }} permissions: contents: write - issues: write - pull-requests: read + actions: read outputs: url: ${{ steps.finalize.outputs.url }} status: ${{ steps.finalize.outcome == 'success' && 'published' || '' }} @@ -218,4 +216,3 @@ jobs: with: tag: ${{ needs.prepare.outputs.tag }} asset-directory: ${{ runner.temp }}/release-assets - versioning: ${{ inputs.versioning }} diff --git a/actions/release/ReleaseArtifacts.js b/actions/release/ReleaseArtifacts.js index e7853a1..bfc259e 100644 --- a/actions/release/ReleaseArtifacts.js +++ b/actions/release/ReleaseArtifacts.js @@ -54,7 +54,7 @@ class ReleaseArtifacts { return true; } - async attachAssets({ github, repo, tag, directory }) { + async prepareAssets({ github, repo, tag, directory }) { const release = await ReleaseArtifacts.findRelease(github, repo, tag); if (!release) throw new Error(`No release found for ${tag}.`); const assets = await github.paginate(github.rest.repos.listReleaseAssets, { @@ -67,6 +67,7 @@ class ReleaseArtifacts { .filter((name) => name.endsWith(".tgz")); if (!files.length) throw new Error("No package tarballs available to attach."); + const missing = []; for (const name of files) { const data = fs.readFileSync(path.join(directory, name)); const digest = `sha256:${createHash("sha256").update(data).digest("hex")}`; @@ -80,49 +81,12 @@ class ReleaseArtifacts { } if (!release.draft) throw new Error(`Published release is missing expected asset ${name}.`); - await github.rest.repos.uploadReleaseAsset({ - ...repo, - release_id: release.id, - name, - data, - headers: { - "content-type": "application/gzip", - "content-length": data.length, - }, - }); - } - return release; - } - - async markReleaseRequests({ github, context }) { - const issues = await github.paginate(github.rest.issues.listForRepo, { - ...context.repo, - labels: "autorelease: pending", - state: "closed", - per_page: 100, - }); - for (const issue of issues.filter((item) => item.pull_request)) { - const { data } = await github.rest.pulls.get({ - ...context.repo, - pull_number: issue.number, - }); - if ( - data.merged_at && - data.merge_commit_sha === context.sha && - data.base.ref === context.payload.repository.default_branch - ) { - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: issue.number, - labels: ["autorelease: tagged"], - }); - await github.rest.issues.removeLabel({ - ...context.repo, - issue_number: issue.number, - name: "autorelease: pending", - }); - } + missing.push({ name, data }); } + const pendingDirectory = fs.mkdtempSync(path.join(directory, "pending-")); + for (const { name, data } of missing) + fs.writeFileSync(path.join(pendingDirectory, name), data); + return { release, pendingDirectory, hasAssets: missing.length > 0 }; } } diff --git a/actions/release/ReleaseProject.js b/actions/release/ReleaseProject.js index 9407e8b..36340b6 100644 --- a/actions/release/ReleaseProject.js +++ b/actions/release/ReleaseProject.js @@ -1,13 +1,12 @@ const fs = require("node:fs"); const path = require("node:path"); const { execFileSync } = require("node:child_process"); -const { ReleaseArtifacts } = require("./ReleaseArtifacts.js"); class ReleaseProject { constructor({ workspace, workingDirectory = ".", - versioning, + versioning = "npm", github, context, }) { @@ -22,19 +21,16 @@ class ReleaseProject { "Release working directory must be inside the repository.", ); } - if (!["lerna", "release-please"].includes(versioning)) { - throw new Error("versioning must be lerna or release-please."); + if (!["lerna", "npm"].includes(versioning)) { + throw new Error("versioning must be npm or lerna."); } this.versioning = versioning; this.github = github; this.context = context; const manifest = this.readJson("package.json"); - if ( - versioning === "release-please" && - (manifest.workspaces || manifest.private) - ) { + if (versioning === "npm" && (manifest.workspaces || manifest.private)) { throw new Error( - "release-please mode requires one public package; use lerna for fixed-version workspaces.", + "npm mode requires one public package; use lerna for fixed-version workspaces.", ); } this.version(); @@ -74,7 +70,7 @@ class ReleaseProject { packages(tag) { const version = this.version(); - if (tag !== `v${version}`) + if (this.versionFromTag(tag) !== version) throw new Error("Release tag must match the committed package version."); const candidates = this.versioning === "lerna" @@ -133,85 +129,119 @@ class ReleaseProject { } } - async releaseRequests(state = "all") { - const issues = await this.github.paginate( - this.github.rest.issues.listForRepo, - { - ...this.context.repo, - labels: "autorelease: pending", - state, - per_page: 100, - }, - ); - const requests = []; - for (const issue of issues.filter((item) => item.pull_request)) { - const { data } = await this.github.rest.pulls.get({ - ...this.context.repo, - pull_number: issue.number, - }); - if (data.base.ref === this.context.payload.repository.default_branch) - requests.push(data); - } - return requests.sort((a, b) => - (a.merged_at || "").localeCompare(b.merged_at || ""), - ); + versionFromTag(tag) { + const slug = path + .basename(this.directory) + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + const prefixes = ["", "v", ...(this.relativeDirectory ? [`${slug}-`] : [])]; + const version = prefixes + .map((prefix) => (tag.startsWith(prefix) ? tag.slice(prefix.length) : "")) + .find((candidate) => + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(candidate), + ); + if (!version) + throw new Error( + "Expected a stable version tag, optionally prefixed with v or the project directory slug.", + ); + return version; } - async prepareReleasePlease() { - const requests = await this.releaseRequests(); - const merged = requests.find((request) => request.merged_at); - if (!merged) { - const open = requests.find((request) => request.state === "open"); - return { - status: open ? "awaiting-review" : "no-changes", - url: open?.html_url || "", - }; + currentTag() { + if (this.context.ref.startsWith("refs/tags/")) + return this.context.ref.slice("refs/tags/".length); + if ( + this.context.ref !== + `refs/heads/${this.context.payload.repository.default_branch}` + ) { + throw new Error( + "Run releases from the default branch or an existing release tag.", + ); } - this.git( - "merge-base", - "--is-ancestor", - merged.merge_commit_sha, - this.context.sha, - ); - this.git("checkout", "--detach", merged.merge_commit_sha); - const tag = `v${this.version()}`; - const existing = this.tagCommit(tag); - if (existing && existing !== merged.merge_commit_sha) - throw new Error(`Existing tag ${tag} points to another commit.`); - if (!existing) this.git("tag", tag, merged.merge_commit_sha); - this.git("push", "origin", `refs/tags/${tag}`); - return { tag }; + const tags = this.git("tag", "--points-at", this.context.sha) + .split("\n") + .filter((tag) => { + try { + return this.versionFromTag(tag) === this.version(); + } catch { + return false; + } + }); + if (tags.length > 1) + throw new Error( + "Several release tags match this commit; resume the intended tag explicitly.", + ); + return tags[0]; } - prepareLerna() { + prepareVersion({ tag, body }) { + const version = this.versionFromTag(tag); + const current = this.version(); + const previousParts = current.split(".").map(BigInt); + const parts = version.split(".").map(BigInt); + const difference = parts + .map((part, index) => part - previousParts[index]) + .find((part) => part !== 0n); + if (!difference || difference < 0n) + throw new Error( + "Planned version must be newer than the committed version; reconcile the release history and configuration.", + ); + if (this.tagCommit(tag)) + throw new Error(`Existing tag ${tag} must not be overwritten.`); + if (typeof body !== "string" || !body.trim()) + throw new Error( + "Upstream release notes are required before preparing source files.", + ); const branch = this.context.payload.repository.default_branch; this.git("switch", "--force-create", branch, this.context.sha); + if (this.git("status", "--porcelain")) + throw new Error( + "Release source must be clean before applying the planned version.", + ); this.git("config", "user.name", "github-actions[bot]"); this.git( "config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com", ); - let tag = `v${this.version()}`; - if (this.tagCommit(tag) !== this.git("rev-parse", "HEAD")) { + if (this.versioning === "lerna") { this.run("npm", [ "exec", "--no", "--", "lerna", "version", + version, "--conventional-commits", + "--tag-version-prefix", + tag.slice(0, -version.length), + "--no-git-tag-version", "--no-push", "--yes", - "--message", - "chore(release): %s", ]); - if (this.git("rev-parse", "HEAD") === this.context.sha) - return { status: "no-changes" }; - tag = `v${this.version()}`; + if (this.version() === current) return { status: "no-changes" }; + } else { + this.run("npm", [ + "version", + version, + "--no-git-tag-version", + "--ignore-scripts", + ]); } - if (this.tagCommit(tag) !== this.git("rev-parse", "HEAD")) - throw new Error("Release tag does not match the prepared commit."); + if (this.version() !== version) + throw new Error("Version preparation did not apply the planned version."); + const changelog = path.join(this.directory, "CHANGELOG.md"); + const previous = fs.existsSync(changelog) + ? fs.readFileSync(changelog, "utf8").replace(/^# Changelog\s*/i, "") + : ""; + fs.writeFileSync( + changelog, + `# Changelog\n\n## ${tag}\n\n${body.trim()}\n\n${previous}`, + ); + this.git("add", "--all", "--", "."); + this.git("commit", "-m", `chore(release): ${tag}`); + this.git("tag", tag); this.git( "push", "--atomic", @@ -222,7 +252,7 @@ class ReleaseProject { return { tag }; } - async prepare({ workflowRef, findRelease }) { + async prepare({ workflowRef, findRelease, plan }) { const { ref, payload, repo } = this.context; if (ref.startsWith("refs/tags/")) { const tag = ref.slice("refs/tags/".length); @@ -240,19 +270,14 @@ class ReleaseProject { "Run releases from the default branch or an existing release tag.", ); } - const result = - this.versioning === "lerna" - ? this.prepareLerna() - : await this.prepareReleasePlease(); + const currentTag = this.currentTag(); + if (!currentTag && !plan) + throw new Error("An upstream release plan is required."); + if (!currentTag && !plan.hasChanges) return { status: "no-changes" }; + const result = currentTag ? { tag: currentTag } : this.prepareVersion(plan); if (!result.tag) return result; const release = await findRelease(this.github, repo, result.tag); if (release && !release.draft) { - if (this.versioning === "release-please") { - await new ReleaseArtifacts().markReleaseRequests({ - github: this.github, - context: { ...this.context, sha: this.git("rev-parse", "HEAD") }, - }); - } return { ...result, status: "released", url: release.html_url }; } const prefix = `${repo.owner}/${repo.repo}/`; diff --git a/actions/release/finalize/README.md b/actions/release/finalize/README.md index dfe95a7..1d3dee4 100644 --- a/actions/release/finalize/README.md +++ b/actions/release/finalize/README.md @@ -22,19 +22,18 @@ ## Overview -Attach published npm tarballs without overwriting existing assets, publish the draft and mark its release PR as released. +Verify published npm tarballs against existing assets, then upload missing assets and publish the draft with HoverKraft release/update. ## Inputs -| **Input** | **Description** | **Required** | **Default** | -| --------------------- | -------------------------------------------------- | ------------ | --------------------- | -| **`tag`** | Committed release tag. | **true** | - | -| **`asset-directory`** | Directory containing the published npm tarballs. | **true** | - | -| **`versioning`** | Version preparation tool, release-please or lerna. | **false** | `release-please` | -| **`github-token`** | Token with contents and issues write permissions. | **false** | `${{ github.token }}` | +| **Input** | **Description** | **Required** | **Default** | +| --------------------- | ------------------------------------------------ | ------------ | --------------------- | +| **`tag`** | Committed release tag. | **true** | - | +| **`asset-directory`** | Directory containing the published npm tarballs. | **true** | - | +| **`github-token`** | Token with contents write permissions. | **false** | `${{ github.token }}` | diff --git a/actions/release/finalize/action.yml b/actions/release/finalize/action.yml index 53e10f6..a446056 100644 --- a/actions/release/finalize/action.yml +++ b/actions/release/finalize/action.yml @@ -1,5 +1,5 @@ name: Finalize Node.js release -description: Attach published npm tarballs without overwriting existing assets, publish the draft and mark its release PR as released. +description: Verify published npm tarballs against existing assets, then upload missing assets and publish the draft with HoverKraft release/update. author: hoverkraft branding: icon: tag @@ -11,11 +11,8 @@ inputs: asset-directory: description: Directory containing the published npm tarballs. required: true - versioning: - description: Version preparation tool, release-please or lerna. - default: release-please github-token: - description: Token with contents and issues write permissions. + description: Token with contents write permissions. default: ${{ github.token }} outputs: url: @@ -34,22 +31,22 @@ runs: github-token: ${{ inputs.github-token }} script: | const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); - const release = await new ReleaseArtifacts().attachAssets({ github, repo: context.repo, tag: process.env.RELEASE_TAG, directory: process.env.ASSET_DIRECTORY }); + const { release, pendingDirectory, hasAssets } = await new ReleaseArtifacts().prepareAssets({ github, repo: context.repo, tag: process.env.RELEASE_TAG, directory: process.env.ASSET_DIRECTORY }); core.setOutput('url', release.html_url); core.setOutput('published', String(!release.draft)); + core.setOutput('pending-directory', pendingDirectory); + core.setOutput('has-assets', String(hasAssets)); + - id: pending-assets + if: steps.assets.outputs.has-assets == 'true' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-pending-assets-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ steps.assets.outputs.pending-directory }}/*.tgz + if-no-files-found: error - uses: hoverkraft-tech/ci-github-publish/actions/release/update@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 if: steps.assets.outputs.published != 'true' with: tag: ${{ inputs.tag }} publish: "true" + release-artifact-id: ${{ steps.pending-assets.outputs.artifact-id }} github-token: ${{ inputs.github-token }} - - name: Complete release PR - if: inputs.versioning == 'release-please' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - ACTION_PATH: ${{ github.action_path }} - with: - github-token: ${{ inputs.github-token }} - script: | - const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); - await new ReleaseArtifacts().markReleaseRequests({ github, context }); diff --git a/actions/release/prepare/README.md b/actions/release/prepare/README.md index 98c7891..10020da 100644 --- a/actions/release/prepare/README.md +++ b/actions/release/prepare/README.md @@ -22,18 +22,18 @@ ## Overview -Prepare a single-package Release Please PR or a fixed-version Lerna release, and dispatch publication from the committed tag. +Plan with HoverKraft, apply the version and changelog with npm or Lerna, and dispatch publication from the committed tag. ## Inputs -| **Input** | **Description** | **Required** | **Default** | -| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | -| **`versioning`** | Version preparation tool, release-please for one package or lerna for fixed-version workspaces. | **false** | `release-please` | -| **`working-directory`** | Project directory relative to the caller repository root. | **false** | `.` | -| **`github-token`** | Token with contents, issues, pull-requests and actions write permissions. An app token allows CI to run on release PRs. | **false** | `${{ github.token }}` | +| **Input** | **Description** | **Required** | **Default** | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------- | +| **`versioning`** | Tool applying the HoverKraft release plan, npm for one package or lerna for fixed-version workspaces. | **false** | `npm` | +| **`working-directory`** | Project directory relative to the caller repository root. | **false** | `.` | +| **`github-token`** | Token with contents and actions write and pull-requests read permissions. Its identity must be allowed to push release commits and tags. | **false** | `${{ github.token }}` | @@ -42,12 +42,12 @@ Prepare a single-package Release Please PR or a fixed-version Lerna release, and ## Outputs -| **Output** | **Description** | -| --------------- | ------------------------------------------------------------------------------------------------------------ | -| **`status`** | One of no-changes, awaiting-review, dispatched, ready or released. Only ready should start publication jobs. | -| **`tag`** | Stable release tag, empty when preparation has no release to publish. | -| **`packages`** | JSON array of public packages at the committed release version, with name, version, directory and slug. | -| **`published`** | Whether the tag already has a published GitHub release. | +| **Output** | **Description** | +| --------------- | ------------------------------------------------------------------------------------------------------- | +| **`status`** | One of no-changes, dispatched, ready or released. Only ready should start publication jobs. | +| **`tag`** | Stable release tag, empty when preparation has no release to publish. | +| **`packages`** | JSON array of public packages at the committed release version, with name, version, directory and slug. | +| **`published`** | Whether the tag already has a published GitHub release. | diff --git a/actions/release/prepare/action.yml b/actions/release/prepare/action.yml index f3d9871..f8d7032 100644 --- a/actions/release/prepare/action.yml +++ b/actions/release/prepare/action.yml @@ -1,22 +1,22 @@ name: Prepare Node.js release -description: Prepare a single-package Release Please PR or a fixed-version Lerna release, and dispatch publication from the committed tag. +description: Plan with HoverKraft, apply the version and changelog with npm or Lerna, and dispatch publication from the committed tag. author: hoverkraft branding: icon: git-commit color: blue inputs: versioning: - description: Version preparation tool, release-please for one package or lerna for fixed-version workspaces. - default: release-please + description: Tool applying the HoverKraft release plan, npm for one package or lerna for fixed-version workspaces. + default: npm working-directory: description: Project directory relative to the caller repository root. default: . github-token: - description: Token with contents, issues, pull-requests and actions write permissions. An app token allows CI to run on release PRs. + description: Token with contents and actions write and pull-requests read permissions. Its identity must be allowed to push release commits and tags. default: ${{ github.token }} outputs: status: - description: One of no-changes, awaiting-review, dispatched, ready or released. Only ready should start publication jobs. + description: One of no-changes, dispatched, ready or released. Only ready should start publication jobs. value: ${{ steps.prepare.outputs.status }} tag: description: Stable release tag, empty when preparation has no release to publish. @@ -37,6 +37,7 @@ runs: # The preparation step pushes version commits and tags atomically. persist-credentials: true - name: Validate release project + id: inspect uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ACTION_PATH: ${{ github.action_path }} @@ -45,24 +46,42 @@ runs: with: script: | const { ReleaseProject } = require(`${process.env.ACTION_PATH}/../ReleaseProject.js`); - if (context.ref !== `refs/heads/${context.payload.repository.default_branch}` && !context.ref.startsWith('refs/tags/v')) { - throw new Error('Run releases from the default branch or an existing release tag.'); - } - new ReleaseProject({ workspace: process.env.GITHUB_WORKSPACE, workingDirectory: process.env.WORKING_DIRECTORY, versioning: process.env.VERSIONING, github, context }); + const project = new ReleaseProject({ workspace: process.env.GITHUB_WORKSPACE, workingDirectory: process.env.WORKING_DIRECTORY, versioning: process.env.VERSIONING, github, context }); + core.setOutput('needs-plan', String(!project.currentTag())); + - name: Plan release with HoverKraft + id: plan + if: steps.inspect.outputs.needs-plan == 'true' + uses: hoverkraft-tech/ci-github-publish/actions/release/plan@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + with: + github-token: ${{ inputs.github-token }} + target-sha: ${{ github.sha }} + working-directory: ${{ inputs.working-directory != '.' && inputs.working-directory || '' }} + - name: Restore source checkout for version preparation + if: steps.plan.outputs.has-changes == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + token: ${{ inputs.github-token }} + persist-credentials: true - uses: $/actions/setup-node - if: inputs.versioning == 'lerna' + if: steps.plan.outputs.has-changes == 'true' || inputs.versioning == 'lerna' && steps.inspect.outputs.needs-plan != 'true' with: working-directory: ${{ inputs.working-directory }} - - name: Prepare single-package release PR - if: inputs.versioning == 'release-please' && github.ref_type == 'branch' - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + - id: configuration + if: steps.plan.outputs.has-changes == 'true' + uses: hoverkraft-tech/ci-github-publish/actions/release/get-configuration@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 + with: + working-directory: ${{ inputs.working-directory != '.' && inputs.working-directory || '' }} + - name: Render the planned changelog without creating release state + id: notes + if: steps.plan.outputs.has-changes == 'true' + uses: hoverkraft-tech/ci-github-publish/actions/release/prepare@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 with: + config-name: file:${{ steps.configuration.outputs.config-path }} token: ${{ inputs.github-token }} - release-type: node - path: ${{ inputs.working-directory }} - target-branch: ${{ github.event.repository.default_branch }} - include-component-in-tag: false - skip-github-release: true + dry-run: "true" + tag: ${{ steps.plan.outputs.tag }} + commitish: ${{ github.sha }} # codespell:ignore commitish - name: Prepare release source id: prepare uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -71,19 +90,22 @@ runs: WORKING_DIRECTORY: ${{ inputs.working-directory }} VERSIONING: ${{ inputs.versioning }} CALLER_WORKFLOW_REF: ${{ github.workflow_ref }} + PLANNED_TAG: ${{ steps.plan.outputs.tag }} + HAS_CHANGES: ${{ steps.plan.outputs.has-changes }} + CHANGELOG: ${{ steps.notes.outputs.body }} with: github-token: ${{ inputs.github-token }} script: | const { ReleaseProject } = require(`${process.env.ACTION_PATH}/../ReleaseProject.js`); const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); const project = new ReleaseProject({ workspace: process.env.GITHUB_WORKSPACE, workingDirectory: process.env.WORKING_DIRECTORY, versioning: process.env.VERSIONING, github, context }); - const result = await project.prepare({ workflowRef: process.env.CALLER_WORKFLOW_REF, findRelease: ReleaseArtifacts.findRelease }); + const plan = { tag: process.env.PLANNED_TAG, hasChanges: process.env.HAS_CHANGES === 'true', body: process.env.CHANGELOG }; + const result = await project.prepare({ workflowRef: process.env.CALLER_WORKFLOW_REF, findRelease: ReleaseArtifacts.findRelease, plan }); for (const [name, value] of Object.entries(result)) core.setOutput(name, value); core.setOutput('packages', result.packages || []); core.setOutput('published', String(Boolean(result.published))); const messages = { 'no-changes': 'No changes to release.', - 'awaiting-review': `Merge the release PR, then run this workflow again: ${result.url}`, dispatched: `Publication dispatched for ${result.tag}. Follow the new workflow run for that tag.`, ready: `Publishing committed release ${result.tag}.`, released: `${result.tag} is already released.`, diff --git a/docs/README.md b/docs/README.md index 4945628..75f3bd1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,3 +5,4 @@ - [Validate release automation](development/release-tests.md) - [Release orchestration decision](adr/0001-release-orchestration.md) - [Separation of CI and release](adr/0002-separate-ci-from-release.md) +- [HoverKraft release planning and publication](adr/0003-hoverkraft-release-actions.md) diff --git a/docs/adr/0001-release-orchestration.md b/docs/adr/0001-release-orchestration.md index 5011ffc..081955f 100644 --- a/docs/adr/0001-release-orchestration.md +++ b/docs/adr/0001-release-orchestration.md @@ -4,7 +4,9 @@ - Date: 2026-10-29 The decision to run CI inside release is superseded by -[ADR 0002](0002-separate-ci-from-release.md). The other decisions remain accepted. +[ADR 0002](0002-separate-ci-from-release.md). The version planning and release PR +decisions are superseded by [ADR 0003](0003-hoverkraft-release-actions.md). +The other decisions remain accepted. ## Context diff --git a/docs/adr/0003-hoverkraft-release-actions.md b/docs/adr/0003-hoverkraft-release-actions.md new file mode 100644 index 0000000..fa39a20 --- /dev/null +++ b/docs/adr/0003-hoverkraft-release-actions.md @@ -0,0 +1,51 @@ +# ADR 0003: Use HoverKraft release actions for planning and publication + +- Status: accepted +- Date: 2026-10-31 +- Partially supersedes: [ADR 0001](0001-release-orchestration.md) + +## Context + +Release Please added a separate release policy and PR lifecycle for single +packages. Lerna independently selected monorepo versions. HoverKraft already +provides shared release planning, changelog rendering, AI notes and publication. + +The `ci-github-publish` stable release 1.0.3, commit +`ea6682de88070c254dac619a6623a242a6d57cd3`, was checked on this date. +Its `release/plan` returns change detection and the planned tag. Its internal +`release/prepare` helper can render notes for an explicit tag without writing a +release. `release/update` uploads provided assets without overwriting existing +names and publishes only after those uploads succeed. + +## Decision + +Use `release/plan` as the version authority for both single packages and Lerna +workspaces. Keep npm and Lerna as tools that apply the exact planned version to +manifests and lockfiles. Lerna also maintains workspace dependencies and package +changelogs. Remove Release Please, release PR detection and its pending labels. + +Use `release/get-configuration` and the pinned `release/prepare` helper in dry-run +mode to render the planned changelog before committing source files. This is the +one direct use of the internal helper: the public planner's outputs do not expose +the rendered notes. Pass the planned tag explicitly; do not calculate a second +release identity. Preserve older root changelog entries. + +Push the version commit and tag atomically in both modes. Retain explicit tag +dispatch, build and npm publication. Use `release/create` for the draft and its +optional AI summary. Verify existing asset digests and stage only missing files; +let `release/update` upload those files and publish the draft. + +## Consequences + +Manual and scheduled releases share the same preparation policy. Both require +branch rules permitting the release commit. Neither mode creates or auto-merges +a release PR. CI remains independent as decided in ADR 0002. + +Root changelogs and release notes use HoverKraft's configuration. The initial +interface supports stable bare, v-prefixed and directory-scoped version tags. +Existing release history must be consistent with committed versions. Older or +equal plans, existing tags and concurrent branch updates stop preparation. + +Retries use the original tag without planning again. Already published npm bytes +and GitHub assets remain protected. No additional release planner or npm runtime +dependency is introduced by the Node.js workflow. diff --git a/docs/development/release-tests.md b/docs/development/release-tests.md index 8ce0ac3..d16304e 100644 --- a/docs/development/release-tests.md +++ b/docs/development/release-tests.md @@ -10,13 +10,16 @@ npm test --prefix tests/release Build configuration tests check script/environment forwarding, artifact paths for single packages and nested workspaces, retry names, and invalid inputs. The suite uses temporary local Git repositories and offline npm commands. It -executes the installed Lerna, checks dependent package and lockfile changes, and +executes npm and the installed Lerna with planned versions, checks dependent +package and lockfile changes, and tests no-change behavior, tag collisions and concurrent branch pushes. A single -package fixture models a merged Release Please PR, builds a CLI, packs it, installs +package fixture applies a release plan, builds a CLI, packs it, installs the tarball and executes its entry point. Registry and GitHub responses are mocked for partial publication, integrity -failures, draft lookup, asset recovery and release PR labels. Tests do not call +failures, draft lookup and staging missing assets for HoverKraft upload. Release +plans and rendered notes are supplied as upstream action outputs; this suite does +not duplicate HoverKraft's version calculation tests. Tests do not call OpenAI, publish packages or create remote releases. The suite runs in the shared CI workflow. diff --git a/docs/usage/examples/reactionable-cli.yml b/docs/usage/examples/reactionable-cli.yml index 9595b3f..3d2bb91 100644 --- a/docs/usage/examples/reactionable-cli.yml +++ b/docs/usage/examples/reactionable-cli.yml @@ -14,12 +14,10 @@ jobs: permissions: actions: write contents: write - issues: write - pull-requests: write + pull-requests: read packages: read id-token: write with: - versioning: release-please environment: github-pages build: | {"commands":["build","build:doc"],"artifact":["dist","build"]} diff --git a/docs/usage/examples/reactionable.yml b/docs/usage/examples/reactionable.yml index 6bdd537..a1b164a 100644 --- a/docs/usage/examples/reactionable.yml +++ b/docs/usage/examples/reactionable.yml @@ -14,8 +14,7 @@ jobs: permissions: actions: write contents: write - issues: write - pull-requests: write + pull-requests: read packages: read id-token: write with: diff --git a/docs/usage/releases.md b/docs/usage/releases.md index 8f5a1f2..0c274ee 100644 --- a/docs/usage/releases.md +++ b/docs/usage/releases.md @@ -19,7 +19,8 @@ Complete caller examples, including documentation deployment: for each package. Use the **calling repository and workflow filename**, such as `release.yml`, and the environment passed below. Allow direct `npm publish`. Publish the first version of a new npm package before configuring its publisher. -3. Allow the default branch and `v*` tags in the GitHub environment. Its name +3. Allow the default branch and the tags produced by your release configuration + in the GitHub environment (for example, `*.*.*` for stable version tags). Its name defaults to `release`; an existing environment can be passed explicitly. 4. Store `OPENAI_API_KEY` as a repository or environment secret when enabling AI summaries, and pass it explicitly as shown below. @@ -41,15 +42,16 @@ The Release workflow neither runs those checks nor queries or waits for CI resul Release builds run from the version tag because package contents can embed the version. Their artifacts are used for npm packaging and optional documentation deployment. They are not evidence that CI passed on that tag. In particular, Lerna -creates a version commit directly; if it needs additional validation, arrange it -in the caller and gate publication through the release environment. +and npm both create version commits directly; if they need additional validation, +arrange it in the caller and gate publication through the release environment. ## Single package -This configuration fits a TypeScript CLI such as `reactionable-cli`. Release -Please computes versions from Conventional Commits and prepares a release PR -containing `package.json`, the lockfile and `CHANGELOG.md` updates. It does not -publish until that PR is merged and the workflow runs again, manually or on schedule. +This configuration fits a TypeScript CLI such as `reactionable-cli`. HoverKraft +plans the version from Conventional Commits and renders the changelog. npm applies +that exact version to `package.json` and npm lockfiles. The workflow commits those +files with `CHANGELOG.md`, pushes the commit and tag atomically, and dispatches +publication at the tag. ```yaml name: Release @@ -67,12 +69,10 @@ jobs: permissions: actions: write contents: write - issues: write - pull-requests: write + pull-requests: read packages: read id-token: write with: - versioning: release-please environment: github-pages build: | { @@ -84,17 +84,33 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ``` -The defaults use Release Please and `npm run build` with `dist` as the build +The defaults use `versioning: npm` and `npm run build` with `dist` as the build artifact. Omit `versioning` and `build` when those defaults match your project. The `build` input is a JSON object with `commands`, `artifact` paths and an optional `env` object. Commands are package scripts, not shell commands. The Build action installs dependencies and executes those scripts. -GitHub must allow Actions to create PRs. Release PRs created with `GITHUB_TOKEN` -do not trigger normal PR workflows. If required checks depend on those events, -pass a GitHub App token through the optional `github-token` secret. Grant the app -contents, issues, pull requests and actions write access. The workflow never -merges a release PR or bypasses branch protection. +## Release planning and branch rules + +Both package modes use `ci-github-publish/actions/release/plan` as their version +planner. Its generated rules use Conventional Commits: breaking changes select +major, features select minor, and other changes select patch. Repository release +configuration takes precedence. See the +[HoverKraft release actions](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md) +for configuration, history filtering and change detection. + +The workflow preserves the planned tag. It accepts stable `1.2.3` and `v1.2.3` +tags, plus `-1.2.3` for a project in `working-directory`. +Keep the tag convention and release history aligned with existing packages. +A plan older than or equal to the committed version is rejected. If the first +planned release is behind an existing package version, reconcile the GitHub +release baseline and release configuration before running it. + +Branch and tag rules must allow the preparation identity to push the release +commit and tag. The default is `GITHUB_TOKEN`; pass an appropriate token through +`github-token` when required by your rules or when release commits must trigger +normal push CI. It needs contents and actions write and pull requests read access. +The workflow does not create or auto-merge a release PR, and does not bypass rules. ## Fixed-version Lerna workspaces @@ -114,10 +130,12 @@ with: ``` Install Lerna in the project and use a stable fixed version in `lerna.json`. -The workflow runs the installed Lerna with `--conventional-commits`; it does not -implement another version calculator. Lerna updates package versions, internal -dependency ranges, lockfiles and changelogs. Its existing versioning settings, -including pre-1.0 behavior, apply. +The workflow passes the exact HoverKraft-planned version to the installed Lerna. +Lerna updates package versions, internal dependency ranges and lockfiles; +`--conventional-commits` generates package changelogs. It does not choose a second +version. The root `CHANGELOG.md` receives HoverKraft's rendered notes. +Lerna can report no changed packages even when the release plan contains changes; +in that case no release commit, tag or publication run is created. This mode commits directly to the default branch and pushes the commit and tag atomically. Repository rules must permit that push. If the branch advances during @@ -130,13 +148,12 @@ unchanged packages can retain their previous versions. Run the calling workflow from the default branch or wait for its schedule. Preparation reports one of these outcomes in the job summary: -| Status | Meaning | -| ----------------- | ------------------------------------------------------------- | -| `no-changes` | Nothing needs releasing; no build or publishing starts. | -| `awaiting-review` | Merge the linked Release Please PR, then run again. | -| `dispatched` | The version tag exists; follow the new run for that tag. | -| `released` | This version already has a published GitHub release. | -| `published` | The tag run published all packages and finalized the release. | +| Status | Meaning | +| ------------ | ------------------------------------------------------------- | +| `no-changes` | Nothing needs releasing; no build or publishing starts. | +| `dispatched` | The version tag exists; follow the new run for that tag. | +| `released` | This version already has a published GitHub release. | +| `published` | The tag run published all packages and finalized the release. | Both preparation and publication share a concurrency group inside the reusable workflow. Do not use that same group in the caller. A successful preparation run @@ -159,6 +176,9 @@ versions are skipped and their original tarballs are downloaded with SHA-512 verification. Existing GitHub assets are reused only when SHA-256 digests match; conflicting or missing digests fail instead of replacing assets. +Asset uploads and final publication use HoverKraft `release/update`. On retry, +only missing assets are passed to it after the existing digests are verified. + npm workspace publication cannot be atomic, so partial publication is possible. An npm registry error stops recovery; only an absent version permits publishing. If the release build fails because source code needs changing, make a new commit diff --git a/tests/release/artifacts.test.js b/tests/release/artifacts.test.js index 20b7cca..59dd627 100644 --- a/tests/release/artifacts.test.js +++ b/tests/release/artifacts.test.js @@ -142,12 +142,10 @@ function assetsFixture(t) { fs.writeFileSync(path.join(root, "example-core-1.2.0.tgz"), data); const release = { id: 1, draft: true }; const assets = []; - const uploaded = []; const github = { rest: { repos: { getReleaseByTag: async () => ({ data: release }), - uploadReleaseAsset: async (asset) => uploaded.push(asset), }, }, paginate: async () => assets, @@ -155,7 +153,6 @@ function assetsFixture(t) { return { release, assets, - uploaded, data, args: { github, @@ -166,34 +163,58 @@ function assetsFixture(t) { }; } -test("uploads the exact package bytes and skips an identical existing release asset", async (t) => { +test("stages only missing package bytes for HoverKraft upload and skips identical assets", async (t) => { const f = assetsFixture(t); - await new ReleaseArtifacts().attachAssets(f.args); - assert.equal(f.uploaded.length, 1); - assert.deepEqual(f.uploaded[0].data, f.data); + const result = await new ReleaseArtifacts().prepareAssets(f.args); + assert.equal(result.hasAssets, true); + assert.deepEqual( + fs.readFileSync( + path.join(result.pendingDirectory, "example-core-1.2.0.tgz"), + ), + f.data, + ); f.assets.push({ name: "example-core-1.2.0.tgz", digest: `sha256:${createHash("sha256").update(f.data).digest("hex")}`, }); - await new ReleaseArtifacts().attachAssets(f.args); - assert.equal(f.uploaded.length, 1); + const retry = await new ReleaseArtifacts().prepareAssets(f.args); + assert.equal(retry.hasAssets, false); + assert.deepEqual(fs.readdirSync(retry.pendingDirectory), []); f.release.draft = false; - await new ReleaseArtifacts().attachAssets(f.args); - assert.equal(f.uploaded.length, 1); + assert.equal( + (await new ReleaseArtifacts().prepareAssets(f.args)).hasAssets, + false, + ); }); test("does not overwrite mismatched assets or modify an incomplete published release", async (t) => { const f = assetsFixture(t); f.assets.push({ name: "example-core-1.2.0.tgz", digest: "sha256:different" }); await assert.rejects( - new ReleaseArtifacts().attachAssets(f.args), + new ReleaseArtifacts().prepareAssets(f.args), /different or unavailable digest/, ); f.assets.length = 0; f.release.draft = false; await assert.rejects( - new ReleaseArtifacts().attachAssets(f.args), + new ReleaseArtifacts().prepareAssets(f.args), /missing expected asset/, ); - assert.equal(f.uploaded.length, 0); +}); + +test("partial asset recovery passes only missing tarballs to the uploader", async (t) => { + const f = assetsFixture(t); + const name = "example-cli-1.2.0.tgz"; + fs.writeFileSync(path.join(f.args.directory, name), "second package"); + f.assets.push({ + name: "example-core-1.2.0.tgz", + digest: `sha256:${createHash("sha256").update(f.data).digest("hex")}`, + }); + const result = await new ReleaseArtifacts().prepareAssets(f.args); + assert.equal(result.hasAssets, true); + assert.deepEqual(fs.readdirSync(result.pendingDirectory), [name]); + assert.equal( + fs.readFileSync(path.join(result.pendingDirectory, name), "utf8"), + "second package", + ); }); diff --git a/tests/release/project.test.js b/tests/release/project.test.js index 60ff578..6f82f55 100644 --- a/tests/release/project.test.js +++ b/tests/release/project.test.js @@ -5,9 +5,6 @@ const os = require("node:os"); const path = require("node:path"); const { test } = require("node:test"); const { ReleaseProject } = require("../../actions/release/ReleaseProject.js"); -const { - ReleaseArtifacts, -} = require("../../actions/release/ReleaseArtifacts.js"); Object.assign(process.env, { CI: "true", @@ -20,7 +17,7 @@ Object.assign(process.env, { npm_config_fund: "false", }); -function fixture(t, versioning = "release-please") { +function fixture(t, versioning = "npm") { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nodejs-release-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const directory = path.join(root, "project"); @@ -102,27 +99,18 @@ function fixture(t, versioning = "release-please") { commit("chore: initial"); git("tag", "-a", "v1.0.0", "-m", "v1.0.0"); git("push", "origin", "main", "--tags"); - const requests = []; const dispatches = []; - const labels = []; const github = { rest: { - issues: { - listForRepo: () => {}, - addLabels: async (args) => labels.push(args), - removeLabel: async (args) => labels.push(args), - }, - pulls: { - get: async ({ pull_number }) => ({ - data: requests.find((request) => request.number === pull_number), - }), - }, actions: { createWorkflowDispatch: async (args) => dispatches.push(args), }, }, - paginate: async () => - requests.map((request) => ({ number: request.number, pull_request: {} })), + }; + const plan = { + tag: "v1.1.0", + hasChanges: true, + body: "### Features\n\n* Add a command.\n", }; const context = { repo: { owner: "example", repo: "project" }, @@ -141,6 +129,7 @@ function fixture(t, versioning = "release-please") { workflowRef: "example/project/.github/workflows/release.yml@refs/heads/main", findRelease: async () => undefined, + plan, }); return { root, @@ -151,9 +140,8 @@ function fixture(t, versioning = "release-please") { write, json, commit, - requests, + plan, dispatches, - labels, github, context, project, @@ -161,49 +149,38 @@ function fixture(t, versioning = "release-please") { }; } -function releaseRequest(f, properties = {}) { - const request = { - number: 1, - state: "open", - html_url: "https://example.invalid/pull/1", - base: { ref: "main" }, - ...properties, - }; - f.requests.push(request); - return request; -} - -test("single package waits for its release PR without dispatching publication", async (t) => { +test("an unchanged upstream plan skips source preparation and publication", async (t) => { const f = fixture(t); + f.write("README.md", "Documentation only."); + f.context.sha = f.commit("docs: describe setup"); + f.plan.hasChanges = false; assert.equal((await f.prepare()).status, "no-changes"); - const request = releaseRequest(f); - assert.deepEqual(await f.prepare(), { - status: "awaiting-review", - url: request.html_url, - }); + assert.equal(f.project.version(), "1.0.0"); assert.equal(f.dispatches.length, 0); }); -test("single package publishes the merged release commit and packs an executable CLI", async (t) => { +test("single package applies the HoverKraft plan and packs an executable CLI", async (t) => { const f = fixture(t); - f.run("npm", [ - "version", - "1.1.0", - "--no-git-tag-version", - "--ignore-scripts", - ]); - f.write("CHANGELOG.md", "# 1.1.0\n\n* Add a command.\n"); - const releaseSha = f.commit("chore(main): release 1.1.0"); - releaseRequest(f, { - state: "closed", - merged_at: "2026-01-01T00:00:00Z", - merge_commit_sha: releaseSha, - }); - f.write("later.txt", "Later changes must not enter the release."); - f.context.sha = f.commit("feat: later change"); + f.write("CHANGELOG.md", "# Changelog\n\n## v1.0.0\n\nExisting history.\n"); + f.context.sha = f.commit("feat: add output"); f.git("push", "origin", "main"); assert.deepEqual(await f.prepare(), { tag: "v1.1.0", status: "dispatched" }); - assert.equal(f.project.tagCommit("v1.1.0"), releaseSha); + const releaseSha = f.project.tagCommit("v1.1.0"); + assert.notEqual(releaseSha, f.context.sha); + assert.equal( + f.run("git", ["rev-parse", "refs/heads/main"], f.remote), + releaseSha, + ); + assert.equal( + f.project.readJson("package-lock.json").packages[""].version, + "1.1.0", + ); + const changelog = fs.readFileSync( + path.join(f.directory, "CHANGELOG.md"), + "utf8", + ); + assert.ok(changelog.includes(f.plan.body.trim())); + assert.ok(changelog.includes("Existing history.")); assert.deepEqual(f.dispatches, [ { owner: "example", @@ -212,7 +189,6 @@ test("single package publishes the merged release commit and packs an executable ref: "v1.1.0", }, ]); - assert.equal(fs.existsSync(path.join(f.directory, "later.txt")), false); f.context.ref = "refs/tags/v1.1.0"; f.context.sha = releaseSha; const ready = await f.prepare(); @@ -250,88 +226,70 @@ test("single package publishes the merged release commit and packs an executable ]), "1.1.0", ); - await new ReleaseArtifacts().markReleaseRequests({ - github: f.github, - context: f.context, - }); - assert.deepEqual( - f.labels.map((item) => item.labels || item.name), - [["autorelease: tagged"], "autorelease: pending"], - ); }); -test("a single-package tag collision preserves the remote tag", async (t) => { - const f = fixture(t); - f.write("CHANGELOG.md", "a merged PR with an already-used version"); - f.context.sha = f.commit("chore(main): release 1.0.0"); - releaseRequest(f, { - state: "closed", - merged_at: "2026-01-01T00:00:00Z", - merge_commit_sha: f.context.sha, +for (const mode of ["npm", "lerna"]) { + test(`${mode} preserves an existing tag when the planned version collides`, async (t) => { + const f = fixture(t, mode); + f.git("tag", "v1.1.0"); + f.git("push", "origin", "refs/tags/v1.1.0"); + const original = f.project.tagCommit("v1.1.0"); + f.write( + mode === "lerna" ? "packages/core/index.js" : "feature.txt", + "new feature", + ); + f.context.sha = f.commit("feat: add output"); + f.git("push", "origin", "main"); + await assert.rejects(f.prepare(), /Existing tag/); + assert.equal(f.project.tagCommit("v1.1.0"), original); + assert.equal( + f.run("git", ["rev-parse", "refs/heads/main"], f.remote), + f.context.sha, + ); + assert.equal(f.dispatches.length, 0); }); - const original = f.project.tagCommit("v1.0.0"); - await assert.rejects(f.prepare(), /another commit/); - assert.equal(f.project.tagCommit("v1.0.0"), original); - assert.equal(f.dispatches.length, 0); -}); -test("unmerged or unrelated release PRs cannot mark a release as complete", async (t) => { - const f = fixture(t); - releaseRequest(f, { merge_commit_sha: f.context.sha }); - releaseRequest(f, { - number: 2, - state: "closed", - merged_at: "2026-01-01", - merge_commit_sha: "other", - }); - releaseRequest(f, { - number: 3, - state: "closed", - merged_at: "2026-01-01", - merge_commit_sha: f.context.sha, - base: { ref: "other" }, - }); - await new ReleaseArtifacts().markReleaseRequests({ - github: f.github, - context: f.context, + test(`${mode} applies a planned version without recomputing its increment`, async (t) => { + const f = fixture(t, mode); + f.write( + mode === "lerna" ? "packages/core/index.js" : "feature.txt", + "fixed output", + ); + f.context.sha = f.commit("fix: correct output"); + f.git("push", "origin", "main"); + f.plan.tag = "3.2.1"; + assert.equal((await f.prepare()).tag, "3.2.1"); + assert.equal(f.project.version(), "3.2.1"); + assert.equal(f.project.tagCommit("3.2.1"), f.git("rev-parse", "HEAD")); }); - assert.equal(f.labels.length, 0); -}); -test("branch recovery completes PR labels after publication succeeded", async (t) => { - const f = fixture(t); - f.run("npm", [ - "version", - "1.0.1", - "--no-git-tag-version", - "--ignore-scripts", - ]); - f.context.sha = f.commit("chore(main): release 1.0.1"); - releaseRequest(f, { - state: "closed", - merged_at: "2026-01-01", - merge_commit_sha: f.context.sha, + test(`${mode} rejects invalid, older or missing plans before changing source`, async (t) => { + const f = fixture(t, mode); + f.write("feature.txt", "new feature"); + f.context.sha = f.commit("feat: add output"); + for (const tag of ["v0.9.0", "v1.0.0", "v1.0.1-rc.1", "latest"]) { + f.plan.tag = tag; + await assert.rejects(f.prepare(), /newer|stable version tag/); + assert.equal(f.git("rev-parse", "HEAD"), f.context.sha); + } + f.plan.tag = "v1.1.0"; + f.plan.body = ""; + await assert.rejects(f.prepare(), /release notes/); + await assert.rejects(f.project.prepare({}), /release plan/); + assert.equal(f.dispatches.length, 0); }); - const result = await f.project.prepare({ - findRelease: async () => ({ - draft: false, - html_url: "https://example.invalid/release", - }), - }); - assert.equal(result.status, "released"); - assert.equal(f.labels.length, 2); - assert.equal(f.dispatches.length, 0); -}); +} for (const [message, version] of [ ["fix: correct output", "1.0.1"], ["feat: add output", "1.1.0"], ["feat: change output\n\nBREAKING CHANGE: output is now structured", "2.0.0"], ]) { - test(`Lerna infers ${version}, updates dependent packages and resumes the tag`, async (t) => { + test(`Lerna applies planned ${version}, updates dependent packages and resumes the tag`, async (t) => { const f = fixture(t, "lerna"); f.write("packages/core/index.js", "module.exports = 2;\n"); f.context.sha = f.commit(message); + f.plan.tag = `v${version}`; f.git("push", "origin", "main"); assert.deepEqual(await f.prepare(), { tag: `v${version}`, @@ -376,39 +334,25 @@ test("Lerna skips a commit that changes no packages", async (t) => { assert.equal(f.dispatches.length, 0); }); -test("Lerna does not overwrite an existing version tag", async (t) => { - const f = fixture(t, "lerna"); - f.git("tag", "v1.1.0"); - f.git("push", "origin", "refs/tags/v1.1.0"); - const original = f.project.tagCommit("v1.1.0"); - f.write("packages/core/index.js", "module.exports = 2;\n"); - f.context.sha = f.commit("feat: add output"); - f.git("push", "origin", "main"); - await assert.rejects(f.prepare()); - assert.equal(f.project.tagCommit("v1.1.0"), original); - assert.equal( - f.run("git", ["rev-parse", "refs/heads/main"], f.remote), - f.context.sha, - ); -}); - -test("an advancing default branch rejects both the Lerna commit and tag push", async (t) => { - const f = fixture(t, "lerna"); - f.write("packages/core/index.js", "module.exports = 2;\n"); - f.context.sha = f.commit("feat: add output"); - f.git("push", "origin", "main"); - f.write("later.txt", "concurrent main commit"); - const advanced = f.commit("docs: update main"); - f.git("push", "origin", "main"); - await assert.rejects(f.prepare()); - assert.equal( - f.run("git", ["rev-parse", "refs/heads/main"], f.remote), - advanced, - ); - assert.throws(() => - f.run("git", ["rev-parse", "refs/tags/v1.1.0"], f.remote), - ); -}); +for (const mode of ["npm", "lerna"]) { + test(`${mode} rejects both commit and tag when the default branch advances`, async (t) => { + const f = fixture(t, mode); + f.write("packages/core/index.js", "module.exports = 2;\n"); + f.context.sha = f.commit("feat: add output"); + f.git("push", "origin", "main"); + f.write("later.txt", "concurrent main commit"); + const advanced = f.commit("docs: update main"); + f.git("push", "origin", "main"); + await assert.rejects(f.prepare()); + assert.equal( + f.run("git", ["rev-parse", "refs/heads/main"], f.remote), + advanced, + ); + assert.throws(() => + f.run("git", ["rev-parse", "refs/tags/v1.1.0"], f.remote), + ); + }); +} test("published releases skip dispatch and tag recovery preserves their notes", async (t) => { const f = fixture(t, "lerna"); @@ -426,6 +370,57 @@ test("published releases skip dispatch and tag recovery preserves their notes", ); }); +test("branch recovery dispatches an unfinished tagged version without planning again", async (t) => { + const f = fixture(t); + f.write("feature.txt", "feature"); + f.context.sha = f.commit("feat: add output"); + f.git("push", "origin", "main"); + await f.prepare(); + f.context.sha = f.git("rev-parse", "HEAD"); + const result = await f.project.prepare({ + workflowRef: "example/project/.github/workflows/release.yml@main", + findRelease: async () => ({ draft: true }), + }); + assert.equal(result.tag, "v1.1.0"); + assert.equal(result.status, "dispatched"); + assert.equal(f.dispatches.length, 2); +}); + +test("a nested npm project preserves the directory-scoped planned tag", async (t) => { + const f = fixture(t); + const nested = path.join(f.directory, "tools", "cli"); + fs.mkdirSync(nested, { recursive: true }); + for (const name of ["package.json", "package-lock.json", "build.js", "bin"]) { + fs.renameSync(path.join(f.directory, name), path.join(nested, name)); + } + f.context.sha = f.commit("feat: relocate CLI"); + f.git("push", "origin", "main"); + const project = new ReleaseProject({ + workspace: f.directory, + workingDirectory: "tools/cli", + github: f.github, + context: f.context, + }); + const result = await project.prepare({ + workflowRef: "example/project/.github/workflows/release.yml@main", + findRelease: async () => undefined, + plan: { ...f.plan, tag: "cli-1.1.0" }, + }); + assert.equal(result.tag, "cli-1.1.0"); + assert.equal(project.version(), "1.1.0"); + assert.equal(project.packages(result.tag)[0].directory, "."); + f.context.sha = f.git("rev-parse", "HEAD"); + assert.equal(project.currentTag(), "cli-1.1.0"); +}); + +test("ambiguous version tags require explicit tag recovery", (t) => { + const f = fixture(t); + f.git("tag", "1.0.0"); + assert.throws(() => f.project.currentTag(), /Several release tags/); + f.context.ref = "refs/tags/1.0.0"; + assert.equal(f.project.currentTag(), "1.0.0"); +}); + test("rejects unsupported layouts, versions, refs and mismatched tags", async (t) => { const f = fixture(t); assert.throws( @@ -437,7 +432,7 @@ test("rejects unsupported layouts, versions, refs and mismatched tags", async (t new ReleaseProject({ workspace: f.directory, workingDirectory: "..", - versioning: "release-please", + versioning: "npm", }), /inside/, ); @@ -450,7 +445,7 @@ test("rejects unsupported layouts, versions, refs and mismatched tags", async (t () => new ReleaseProject({ workspace: f.directory, - versioning: "release-please", + versioning: "npm", }), /one public package/, ); From 6339d16052db9cb311b8741fafaad9515363e49c Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Sat, 31 Oct 2026 15:07:12 +0100 Subject: [PATCH 4/4] refactor(release): publish artifacts produced by separate CI --- .github/workflows/__shared-ci.yml | 1 + ...__test-workflow-continuous-integration.yml | 22 ++ .github/workflows/continuous-integration.md | 16 +- .github/workflows/continuous-integration.yml | 67 ++++ .github/workflows/prepare-release.md | 96 ++++++ .github/workflows/prepare-release.yml | 63 ++++ .github/workflows/release.md | 46 +-- .github/workflows/release.yml | 137 +++----- README.md | 3 +- actions/package-artifacts/PackageArtifacts.js | 104 ++++++ .../build => package-artifacts}/README.md | 22 +- actions/package-artifacts/action.yml | 26 ++ actions/release/ReleaseArtifacts.js | 17 + actions/release/ReleaseBuild.js | 57 ---- actions/release/ReleaseProject.js | 12 +- actions/release/build/action.yml | 44 --- actions/release/prepare/README.md | 2 +- actions/release/prepare/action.yml | 4 +- actions/release/restore-package/README.md | 2 +- actions/release/restore-package/action.yml | 6 +- actions/release/verify/README.md | 79 +++++ actions/release/verify/action.yml | 49 +++ docs/README.md | 1 + docs/adr/0002-separate-ci-from-release.md | 3 +- docs/adr/0003-hoverkraft-release-actions.md | 3 +- .../adr/0004-ci-produces-release-artifacts.md | 49 +++ docs/development/release-tests.md | 8 +- docs/usage/custom-releases.md | 314 ++++-------------- docs/usage/examples/reactionable-cli.yml | 41 ++- docs/usage/examples/reactionable.yml | 43 ++- docs/usage/releases.md | 293 +++++++--------- tests/release/build.test.js | 60 ---- tests/release/package-artifacts.test.js | 160 +++++++++ 33 files changed, 1105 insertions(+), 745 deletions(-) create mode 100644 .github/workflows/prepare-release.md create mode 100644 .github/workflows/prepare-release.yml create mode 100644 actions/package-artifacts/PackageArtifacts.js rename actions/{release/build => package-artifacts}/README.md (55%) create mode 100644 actions/package-artifacts/action.yml delete mode 100644 actions/release/ReleaseBuild.js delete mode 100644 actions/release/build/action.yml create mode 100644 actions/release/verify/README.md create mode 100644 actions/release/verify/action.yml create mode 100644 docs/adr/0004-ci-produces-release-artifacts.md delete mode 100644 tests/release/build.test.js create mode 100644 tests/release/package-artifacts.test.js diff --git a/.github/workflows/__shared-ci.yml b/.github/workflows/__shared-ci.yml index f3be608..873a59a 100644 --- a/.github/workflows/__shared-ci.yml +++ b/.github/workflows/__shared-ci.yml @@ -80,6 +80,7 @@ jobs: needs: linter uses: $/.github/workflows/__test-workflow-continuous-integration.yml permissions: + actions: read attestations: write contents: read packages: write diff --git a/.github/workflows/__test-workflow-continuous-integration.yml b/.github/workflows/__test-workflow-continuous-integration.yml index eff95a0..60d909a 100644 --- a/.github/workflows/__test-workflow-continuous-integration.yml +++ b/.github/workflows/__test-workflow-continuous-integration.yml @@ -10,6 +10,7 @@ jobs: name: Act - Run the continuous integration workflow (without container) uses: $/.github/workflows/continuous-integration.yml permissions: + actions: read contents: read id-token: write packages: read @@ -17,6 +18,7 @@ jobs: security-events: write with: working-directory: tests/npm + packages: '[{"name":"test-for-ci-with-npm","version":"0.1.0","directory":".","slug":"test-for-ci-with-npm"}]' code-ql: | { "languages": "javascript-typescript", @@ -31,6 +33,9 @@ jobs: name: Assert - Ensure build artifact has been uploaded (without container) runs-on: ubuntu-latest needs: act-without-container + permissions: + actions: read + contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -43,11 +48,26 @@ jobs: - name: Check the build artifacts run: test -f tests/npm/dist/test.txt + - name: Download the packaged CI output + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ fromJSON(needs.act-without-container.outputs.package-artifacts)[0].artifact-id }} + path: ${{ runner.temp }}/ci-package + skip-decompress: true + + - name: Verify the tarball contains the tested build output + env: + ARTIFACT_NAME: ${{ fromJSON(needs.act-without-container.outputs.package-artifacts)[0].artifact-name }} + run: | + tar -xOf "$RUNNER_TEMP/ci-package/$ARTIFACT_NAME" package/dist/test.txt > "$RUNNER_TEMP/packaged-build.txt" + cmp tests/npm/dist/test.txt "$RUNNER_TEMP/packaged-build.txt" + act-without-container-and-github-reports: # jscpd:ignore-start name: Act - Run the continuous integration workflow (without container and GitHub reports) uses: $/.github/workflows/continuous-integration.yml permissions: + actions: read contents: read id-token: write packages: read @@ -94,6 +114,7 @@ jobs: uses: $/.github/workflows/continuous-integration.yml needs: arrange-with-container permissions: + actions: read contents: read id-token: write packages: read @@ -130,6 +151,7 @@ jobs: uses: $/.github/workflows/continuous-integration.yml needs: arrange-with-container permissions: + actions: read contents: read id-token: write packages: read diff --git a/.github/workflows/continuous-integration.md b/.github/workflows/continuous-integration.md index 89a49e8..71cd33d 100644 --- a/.github/workflows/continuous-integration.md +++ b/.github/workflows/continuous-integration.md @@ -3,7 +3,7 @@ # GitHub Reusable Workflow: Node.js Continuous Integration
- Node.js Continuous Integration + Node.js Continuous Integration
--- @@ -16,7 +16,6 @@ [![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) [![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) -![GitHub Verified Creator](https://img.shields.io/badge/GitHub-Verified%20Creator-4493F8?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJyZ2IoNjgsIDE0NywgMjQ4KSI+CiAgPHBhdGggZD0ibTkuNTg1LjUyLjkyOS42OGMuMTUzLjExMi4zMzEuMTg2LjUxOC4yMTVsMS4xMzguMTc1YTIuNjc4IDIuNjc4IDAgMCAxIDIuMjQgMi4yNGwuMTc0IDEuMTM5Yy4wMjkuMTg3LjEwMy4zNjUuMjE1LjUxOGwuNjguOTI4YTIuNjc3IDIuNjc3IDAgMCAxIDAgMy4xN2wtLjY4LjkyOGExLjE3NCAxLjE3NCAwIDAgMC0uMjE1LjUxOGwtLjE3NSAxLjEzOGEyLjY3OCAyLjY3OCAwIDAgMS0yLjI0MSAyLjI0MWwtMS4xMzguMTc1YTEuMTcgMS4xNyAwIDAgMC0uNTE4LjIxNWwtLjkyOC42OGEyLjY3NyAyLjY3NyAwIDAgMS0zLjE3IDBsLS45MjgtLjY4YTEuMTc0IDEuMTc0IDAgMCAwLS41MTgtLjIxNUwzLjgzIDE0LjQxYTIuNjc4IDIuNjc4IDAgMCAxLTIuMjQtMi4yNGwtLjE3NS0xLjEzOGExLjE3IDEuMTcgMCAwIDAtLjIxNS0uNTE4bC0uNjgtLjkyOGEyLjY3NyAyLjY3NyAwIDAgMSAwLTMuMTdsLjY4LS45MjhjLjExMi0uMTUzLjE4Ni0uMzMxLjIxNS0uNTE4bC4xNzUtMS4xNGEyLjY3OCAyLjY3OCAwIDAgMSAyLjI0LTIuMjRsMS4xMzktLjE3NWMuMTg3LS4wMjkuMzY1LS4xMDMuNTE4LS4yMTVsLjkyOC0uNjhhMi42NzcgMi42NzcgMCAwIDEgMy4xNyAwWk03LjMwMyAxLjcyOGwtLjkyNy42OGEyLjY3IDIuNjcgMCAwIDEtMS4xOC40ODlsLTEuMTM3LjE3NGExLjE3OSAxLjE3OSAwIDAgMC0uOTg3Ljk4N2wtLjE3NCAxLjEzNmEyLjY3NyAyLjY3NyAwIDAgMS0uNDg5IDEuMThsLS42OC45MjhhMS4xOCAxLjE4IDAgMCAwIDAgMS4zOTRsLjY4LjkyN2MuMjU2LjM0OC40MjQuNzUzLjQ4OSAxLjE4bC4xNzQgMS4xMzdjLjA3OC41MDkuNDc4LjkwOS45ODcuOTg3bDEuMTM2LjE3NGEyLjY3IDIuNjcgMCAwIDEgMS4xOC40ODlsLjkyOC42OGMuNDE0LjMwNS45NzkuMzA1IDEuMzk0IDBsLjkyNy0uNjhhMi42NyAyLjY3IDAgMCAxIDEuMTgtLjQ4OWwxLjEzNy0uMTc0YTEuMTggMS4xOCAwIDAgMCAuOTg3LS45ODdsLjE3NC0xLjEzNmEyLjY3IDIuNjcgMCAwIDEgLjQ4OS0xLjE4bC42OC0uOTI4YTEuMTc2IDEuMTc2IDAgMCAwIDAtMS4zOTRsLS42OC0uOTI3YTIuNjg2IDIuNjg2IDAgMCAxLS40ODktMS4xOGwtLjE3NC0xLjEzN2ExLjE3OSAxLjE3OSAwIDAgMC0uOTg3LS45ODdsLTEuMTM2LS4xNzRhMi42NzcgMi42NzcgMCAwIDEtMS4xOC0uNDg5bC0uOTI4LS42OGExLjE3NiAxLjE3NiAwIDAgMC0xLjM5NCAwWk0xMS4yOCA2Ljc4bC0zLjc1IDMuNzVhLjc1Ljc1IDAgMCAxLTEuMDYgMEw0LjcyIDguNzhhLjc1MS43NTEgMCAwIDEgLjAxOC0xLjA0Mi43NTEuNzUxIDAgMCAxIDEuMDQyLS4wMThMNyA4Ljk0bDMuMjItMy4yMmEuNzUxLjc1MSAwIDAgMSAxLjA0Mi4wMTguNzUxLjc1MSAwIDAgMSAuMDE4IDEuMDQyWiI+PC9wYXRoPgo8L3N2Zz4K) @@ -34,9 +33,11 @@ Workflow to performs continuous integration steps against a Node.js project: - Linting - Build - Test +- Optional npm packaging for publication from the same commit ### Permissions +- **`actions`**: `read` - **`contents`**: `read` - **`id-token`**: `write` - **`packages`**: `read` @@ -253,6 +254,10 @@ jobs: | | | | | | | | Example: | | | | | |
{
 "commands": [
 "build",
 "generate-artifacts"
 ],
 "env": {
 "CUSTOM_ENV_VAR": "value"
 },
 "artifact": [
 "dist/",
 "packages/package-a/build/"
 ]
}
| | | | +| **`packages`** | Optional JSON package list from prepare-release, with name, version, directory and slug. | **false** | **string** | - | +| | Packages the build output after checks pass and returns immutable tarball artifact IDs. | | | | +| | Requires checks, a build artifact, GitHub-hosted Ubuntu runners and no container. | | | | +| | Leave empty for ordinary CI without release packaging. | | | | | **`checks`** | Optional flag to enable check steps. | **false** | **boolean** | `true` | | **`lint`** | Whether to enable linting. | **false** | **string** | `true` | | | Set to `null` or empty to disable. | | | | @@ -395,9 +400,10 @@ When specified, steps will execute inside this container instead of checking out ## Outputs -| **Output** | **Description** | -| ----------------------- | --------------------------------------------------------- | -| **`build-artifact-id`** | ID of the build artifact) uploaded during the build step. | +| **Output** | **Description** | +| ----------------------- | ------------------------------------------------------------------------------------------------------- | +| **`package-artifacts`** | JSON package artifact list for the release workflow in this same run. Empty when packaging is disabled. | +| **`build-artifact-id`** | ID of the build artifact) uploaded during the build step. | diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index a46535d..1b07286 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -4,6 +4,7 @@ # - Linting # - Build # - Test +# - Optional npm packaging for publication from the same commit name: Node.js Continuous Integration @@ -46,6 +47,14 @@ on: type: string required: false default: "build" + packages: + description: | + Optional JSON package list from prepare-release, with name, version, directory and slug. + Packages the build output after checks pass and returns immutable tarball artifact IDs. + Requires checks, a build artifact, GitHub-hosted Ubuntu runners and no container. + Leave empty for ordinary CI without release packaging. + type: string + default: "" checks: description: "Optional flag to enable check steps." type: boolean @@ -188,6 +197,9 @@ on: Defaults to `GITHUB_TOKEN` if not provided. required: false outputs: + package-artifacts: + description: JSON package artifact list for the release workflow in this same run. Empty when packaging is disabled. + value: ${{ jobs.package-artifacts.outputs.packages }} build-artifact-id: description: "ID of the build artifact) uploaded during the build step." value: ${{ jobs.build.outputs.artifact-id }} @@ -208,6 +220,18 @@ jobs: container-username: ${{ steps.parse.outputs.container-username }} path-mapping: ${{ steps.parse.outputs.path-mapping }} steps: + - name: Validate packaging configuration + if: inputs.packages != '' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + CHECKS: ${{ inputs.checks }} + CONTAINER: ${{ inputs.container }} + RUNS_ON: ${{ inputs.runs-on }} + with: + script: | + if (process.env.CHECKS !== 'true' || process.env.CONTAINER || JSON.stringify(JSON.parse(process.env.RUNS_ON)) !== '["ubuntu-latest"]') { + core.setFailed('Release packaging requires checks, runs-on: ["ubuntu-latest"] and no container.'); + } - id: parse uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: @@ -606,3 +630,46 @@ jobs: report-file: ${{ steps.prepare-test-options.outputs.report-file }} path-mapping: ${{ needs.prepare.outputs.path-mapping || '' }} github-token: ${{ secrets.github-token || github.token }} + + package: + name: 📦 Package ${{ matrix.package.name }} + needs: [prepare, setup, build, lint, test, code-ql, dependency-review] + if: >- + ${{ !cancelled() && inputs.packages != '' && needs.build.result == 'success' + && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + actions: read + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(inputs.packages) }} + steps: + - name: Require build output + env: + BUILD_ARTIFACT_ID: ${{ needs.build.outputs.artifact-id }} + run: test -n "$BUILD_ARTIFACT_ID" + - uses: $/actions/package + with: + working-directory: ${{ inputs.working-directory }} + package-directory: ${{ matrix.package.directory }} + build-artifact-id: ${{ needs.build.outputs.artifact-id }} + build-artifact-path: / + artifact-name: npm-${{ matrix.package.slug }}-${{ github.run_id }}-${{ github.run_attempt }} + + package-artifacts: + name: 📦 Collect package artifact IDs + needs: package + if: ${{ !cancelled() && needs.package.result == 'success' }} + runs-on: ubuntu-latest + permissions: + actions: read + outputs: + packages: ${{ steps.artifacts.outputs.packages }} + steps: + - id: artifacts + uses: $/actions/package-artifacts + with: + packages: ${{ inputs.packages }} diff --git a/.github/workflows/prepare-release.md b/.github/workflows/prepare-release.md new file mode 100644 index 0000000..2a5bd43 --- /dev/null +++ b/.github/workflows/prepare-release.md @@ -0,0 +1,96 @@ + + +# GitHub Reusable Workflow: Prepare Node.js Release + +
+ Prepare Node.js Release +
+ +--- + + + + +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Prepare a Node.js release version and changelog without building or publishing. +Dispatches the calling workflow at the prepared tag; that run can invoke CI and publication. +See [the release guide](../../docs/usage/releases.md). + +### Permissions + +- **`actions`**: `write` +- **`contents`**: `write` +- **`pull-requests`**: `read` + + + + +## Inputs + +### Workflow Call Inputs + +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | ---------------------------------------------------------------------------------------------------------- | ------------ | ---------- | ----------- | +| **`versioning`** | Tool applying the HoverKraft release plan, npm for a single package or lerna for fixed-version workspaces. | **false** | **string** | `npm` | +| **`working-directory`** | Project directory relative to the caller repository root. | **false** | **string** | `.` | +| **`environment`** | GitHub environment permitting release preparation on the default branch and release tags. | **false** | **string** | `release` | + + + + +## Secrets + +| **Secret** | **Description** | **Required** | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| **`github-token`** | Optional token for pushing release commits and dispatching the caller at the tag. Its identity must be permitted by branch and tag rules. | **false** | + + + + +## Outputs + +| **Output** | **Description** | +| -------------- | -------------------------------------------------------------------------------------------------- | +| **`status`** | no-changes, dispatched, released or ready. Only ready should start CI and publication in this run. | +| **`tag`** | Prepared release tag. | +| **`packages`** | JSON package list for the CI workflow's packages input. Available when status is ready. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft-tech + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..d1428ea --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,63 @@ +# Prepare a Node.js release version and changelog without building or publishing. +# Dispatches the calling workflow at the prepared tag; that run can invoke CI and publication. +# See [the release guide](../../docs/usage/releases.md). +name: Prepare Node.js Release + +on: + workflow_call: + inputs: + versioning: + description: Tool applying the HoverKraft release plan, npm for a single package or lerna for fixed-version workspaces. + type: string + default: npm + working-directory: + description: Project directory relative to the caller repository root. + type: string + default: . + environment: + description: GitHub environment permitting release preparation on the default branch and release tags. + type: string + default: release + secrets: + github-token: + description: Optional token for pushing release commits and dispatching the caller at the tag. Its identity must be permitted by branch and tag rules. + required: false + outputs: + status: + description: no-changes, dispatched, released or ready. Only ready should start CI and publication in this run. + value: ${{ jobs.prepare.outputs.status }} + tag: + description: Prepared release tag. + value: ${{ jobs.prepare.outputs.tag }} + packages: + description: JSON package list for the CI workflow's packages input. Available when status is ready. + value: ${{ jobs.prepare.outputs.packages }} + +permissions: {} + +concurrency: + group: node-release-prepare-${{ github.repository }}-${{ inputs.working-directory }} + cancel-in-progress: false + +jobs: + prepare: + name: Prepare release + runs-on: ubuntu-latest + timeout-minutes: 20 + environment: ${{ inputs.environment }} + permissions: + actions: write + contents: write + pull-requests: read + outputs: + status: ${{ steps.prepare.outputs.status }} + tag: ${{ steps.prepare.outputs.tag }} + packages: ${{ steps.prepare.outputs.packages }} + published: ${{ steps.prepare.outputs.published }} + steps: + - id: prepare + uses: $/actions/release/prepare + with: + versioning: ${{ inputs.versioning }} + working-directory: ${{ inputs.working-directory }} + github-token: ${{ secrets.github-token || github.token }} diff --git a/.github/workflows/release.md b/.github/workflows/release.md index d1a407a..cb7c1f6 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -21,22 +21,15 @@ ## Overview -Release a single npm package or fixed-version Lerna workspaces without consumer scripts. - -Keep workflow_dispatch and schedule triggers in the calling workflow. -HoverKraft plans the release from Conventional Commits. npm or Lerna applies -the plan before the version commit and tag are pushed atomically. -Preparation dispatches the caller workflow at the release tag so building, packaging -and npm provenance all refer to the same committed source. -Keep linting, tests and security analysis in the caller's separate CI workflow. -See [the release guide](../../docs/usage/releases.md) for setup, examples and recovery. +Publish CI-produced npm tarballs and finalize a GitHub release without rebuilding. +Call after successful CI on the prepared tag, passing its package-artifacts output. +See [the release guide](../../docs/usage/releases.md) for preparation, CI and recovery. ### Permissions -- **`actions`**: `write` +- **`actions`**: `read` - **`contents`**: `write` - **`id-token`**: `write` -- **`packages`**: `read` - **`pull-requests`**: `read` @@ -46,35 +39,32 @@ See [the release guide](../../docs/usage/releases.md) for setup, examples and re ### Workflow Call Inputs -| **Input** | **Description** | **Required** | **Type** | **Default** | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -------------------------------------------- | -| **`versioning`** | Tool applying the HoverKraft release plan. Use npm for a single public package or lerna for fixed-version workspaces. HoverKraft infers versions from Conventional Commits in both modes. | **false** | **string** | `npm` | -| **`working-directory`** | Project directory relative to the caller repository root. | **false** | **string** | `.` | -| **`build`** | JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifacts must include the package's compiled files. Builds the tagged source without running CI checks. | **false** | **string** | `{"commands":["build"],"artifact":["dist"]}` | -| **`ai-summary`** | Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. | **false** | **boolean** | `false` | -| **`environment`** | GitHub environment for source preparation, AI and npm publishing. It must permit the default branch and release tags. Configure npm trusted publishing for this environment and the calling workflow. | **false** | **string** | `release` | +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | ----------- | +| **`package-artifacts`** | JSON package artifact list from successful CI in this same workflow run and at this release tag's commit. | **true** | **string** | - | +| **`working-directory`** | Project directory relative to the caller repository root, matching release preparation and CI. | **false** | **string** | `.` | +| **`ai-summary`** | Generate an OpenAI summary before npm publication. Requires OPENAI_API_KEY; failures stop publication. | **false** | **boolean** | `false` | +| **`environment`** | GitHub environment for AI and npm publishing. Configure npm trusted publishing for this environment and the calling workflow. | **false** | **string** | `release` | ## Secrets -| **Secret** | **Description** | **Required** | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| **`OPENAI_API_KEY`** | OpenAI API key, required when ai-summary is enabled. | **false** | -| **`github-token`** | Optional token for pushing release commits and dispatching publication. Defaults to GITHUB_TOKEN. Its identity must be permitted by branch and tag rules. | **false** | +| **Secret** | **Description** | **Required** | +| -------------------- | ---------------------------------------------------- | ------------ | +| **`OPENAI_API_KEY`** | OpenAI API key, required when ai-summary is enabled. | **false** | ## Outputs -| **Output** | **Description** | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| **`status`** | no-changes, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. | -| **`tag`** | Prepared release tag. | -| **`release-url`** | Published GitHub release URL from the tag run. | -| **`build-artifact-id`** | Release build artifact ID from the tag run, reusable for documentation deployment. | +| **Output** | **Description** | +| ----------------- | ------------------------------------------------------------------- | +| **`status`** | published after all npm packages and GitHub release assets succeed. | +| **`tag`** | Published release tag. | +| **`release-url`** | Published GitHub release URL. | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 556f6c0..f56a257 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,79 +1,61 @@ -# Release a single npm package or fixed-version Lerna workspaces without consumer scripts. -# -# Keep workflow_dispatch and schedule triggers in the calling workflow. -# HoverKraft plans the release from Conventional Commits. npm or Lerna applies -# the plan before the version commit and tag are pushed atomically. -# Preparation dispatches the caller workflow at the release tag so building, packaging -# and npm provenance all refer to the same committed source. -# Keep linting, tests and security analysis in the caller's separate CI workflow. -# See [the release guide](../../docs/usage/releases.md) for setup, examples and recovery. +# Publish CI-produced npm tarballs and finalize a GitHub release without rebuilding. +# Call after successful CI on the prepared tag, passing its package-artifacts output. +# See [the release guide](../../docs/usage/releases.md) for preparation, CI and recovery. name: Node.js Release on: workflow_call: inputs: - versioning: - description: Tool applying the HoverKraft release plan. Use npm for a single public package or lerna for fixed-version workspaces. HoverKraft infers versions from Conventional Commits in both modes. + package-artifacts: + description: JSON package artifact list from successful CI in this same workflow run and at this release tag's commit. type: string - default: npm + required: true working-directory: - description: Project directory relative to the caller repository root. + description: Project directory relative to the caller repository root, matching release preparation and CI. type: string default: . - build: - description: JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifacts must include the package's compiled files. Builds the tagged source without running CI checks. - type: string - default: '{"commands":["build"],"artifact":["dist"]}' ai-summary: - description: Generate an OpenAI summary before npm publication. Requires the OPENAI_API_KEY secret; failures stop publication. + description: Generate an OpenAI summary before npm publication. Requires OPENAI_API_KEY; failures stop publication. type: boolean default: false environment: - description: GitHub environment for source preparation, AI and npm publishing. It must permit the default branch and release tags. Configure npm trusted publishing for this environment and the calling workflow. + description: GitHub environment for AI and npm publishing. Configure npm trusted publishing for this environment and the calling workflow. type: string default: release secrets: OPENAI_API_KEY: description: OpenAI API key, required when ai-summary is enabled. required: false - github-token: - description: Optional token for pushing release commits and dispatching publication. Defaults to GITHUB_TOKEN. Its identity must be permitted by branch and tag rules. - required: false outputs: status: - description: no-changes, dispatched, released or published. A dispatched run only prepares the release; follow its tag run for publication. - value: ${{ jobs.publish.outputs.status || jobs.prepare.outputs.status }} + description: published after all npm packages and GitHub release assets succeed. + value: ${{ jobs.publish.outputs.status }} tag: - description: Prepared release tag. - value: ${{ jobs.prepare.outputs.tag }} + description: Published release tag. + value: ${{ jobs.verify.outputs.tag }} release-url: - description: Published GitHub release URL from the tag run. + description: Published GitHub release URL. value: ${{ jobs.publish.outputs.url }} - build-artifact-id: - description: Release build artifact ID from the tag run, reusable for documentation deployment. - value: ${{ jobs.build.outputs.artifact-id }} permissions: {} concurrency: - group: node-release-${{ github.repository }}-${{ inputs.working-directory }} + group: node-release-publish-${{ github.repository }}-${{ inputs.working-directory }} cancel-in-progress: false jobs: - prepare: - name: Prepare release + verify: + name: Verify CI artifacts runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 10 environment: ${{ inputs.environment }} permissions: - actions: write - contents: write - pull-requests: read + actions: read + contents: read outputs: - status: ${{ steps.prepare.outputs.status }} - tag: ${{ steps.prepare.outputs.tag }} - packages: ${{ steps.prepare.outputs.packages }} - published: ${{ steps.prepare.outputs.published }} + tag: ${{ steps.verify.outputs.tag }} + packages: ${{ steps.verify.outputs.packages }} + published: ${{ steps.verify.outputs.published }} steps: - name: Validate AI configuration if: inputs.ai-summary @@ -84,37 +66,15 @@ jobs: echo "OPENAI_API_KEY is required when ai-summary is enabled." >&2 exit 1 fi - - id: prepare - uses: $/actions/release/prepare - with: - versioning: ${{ inputs.versioning }} - working-directory: ${{ inputs.working-directory }} - github-token: ${{ secrets.github-token || github.token }} - - build: - name: Build release artifacts - needs: prepare - if: needs.prepare.outputs.status == 'ready' - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: read - packages: read - outputs: - artifact-id: ${{ steps.build.outputs.artifact-id }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - id: build - uses: $/actions/release/build + - id: verify + uses: $/actions/release/verify with: + package-artifacts: ${{ inputs.package-artifacts }} working-directory: ${{ inputs.working-directory }} - build: ${{ inputs.build }} draft: name: Draft release notes - needs: [prepare, build] + needs: verify runs-on: ubuntu-latest timeout-minutes: 20 environment: ${{ inputs.environment }} @@ -126,12 +86,12 @@ jobs: with: persist-credentials: false - name: Create draft and optional AI summary - if: needs.prepare.outputs.published != 'true' + if: needs.verify.outputs.published != 'true' uses: hoverkraft-tech/ci-github-publish/actions/release/create@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} with: - tag: ${{ needs.prepare.outputs.tag }} + tag: ${{ needs.verify.outputs.tag }} target-sha: ${{ github.sha }} working-directory: ${{ inputs.working-directory != '.' && inputs.working-directory || '' }} publish: "false" @@ -139,7 +99,7 @@ jobs: packages: name: Publish ${{ matrix.package.name }} - needs: [prepare, build, draft] + needs: [verify, draft] runs-on: ubuntu-latest timeout-minutes: 30 environment: ${{ inputs.environment }} @@ -150,38 +110,35 @@ jobs: strategy: fail-fast: false matrix: - package: ${{ fromJSON(needs.prepare.outputs.packages) }} + package: ${{ fromJSON(needs.verify.outputs.packages) }} steps: - - name: Package the tagged release commit - id: package - uses: $/actions/package - with: - working-directory: ${{ inputs.working-directory }} - package-directory: ${{ matrix.package.directory }} - build-artifact-id: ${{ needs.build.outputs.artifact-id }} - build-artifact-path: / - artifact-name: npm-${{ matrix.package.slug }}-${{ github.run_attempt }} - - name: Validate publication - uses: $/actions/publish + - name: Download the CI package tarball + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} - dry-run: "true" - provenance: "false" + artifact-ids: ${{ matrix.package.artifact-id }} + path: ${{ runner.temp }}/ci-package + skip-decompress: true - id: registry uses: $/actions/release/restore-package with: name: ${{ matrix.package.name }} version: ${{ matrix.package.version }} - tarball-path: ${{ steps.package.outputs.package-tarball-path }} + tarball-path: ${{ runner.temp }}/ci-package/${{ matrix.package.artifact-name }} + - name: Validate publication + uses: $/actions/publish + with: + package-tarball-artifact-id: ${{ matrix.package.artifact-id }} + dry-run: "true" + provenance: "false" - name: Publish missing npm version if: steps.registry.outputs.published != 'true' uses: $/actions/publish with: - package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} + package-tarball-artifact-id: ${{ matrix.package.artifact-id }} tag: latest - name: Prepare release asset env: - PACKAGE_TARBALL: ${{ steps.package.outputs.package-tarball-path }} + PACKAGE_TARBALL: ${{ runner.temp }}/ci-package/${{ matrix.package.artifact-name }} ASSET_NAME: ${{ matrix.package.slug }}-${{ matrix.package.version }}.tgz run: | mkdir -p "$RUNNER_TEMP/release-assets" @@ -195,7 +152,7 @@ jobs: publish: name: Publish GitHub release - needs: [prepare, packages] + needs: [verify, packages] runs-on: ubuntu-latest timeout-minutes: 15 environment: ${{ inputs.environment }} @@ -214,5 +171,5 @@ jobs: - id: finalize uses: $/actions/release/finalize with: - tag: ${{ needs.prepare.outputs.tag }} + tag: ${{ needs.verify.outputs.tag }} asset-directory: ${{ runner.temp }}/release-assets diff --git a/README.md b/README.md index f0cfbda..01ce3ff 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ _Actions focused on discovering and preparing the Node.js environment._ ### Release -- [Release packages](docs/usage/releases.md) — single-package and Lerna releases through one reusable workflow. +- [Release packages](docs/usage/releases.md) — single-package and Lerna releases with preparation, CI and publication. +- [Preparation workflow reference](.github/workflows/prepare-release.md) — version planning and tagged source. - [Release workflow reference](.github/workflows/release.md) — inputs, secrets and outputs. - [Custom release pipelines](docs/usage/custom-releases.md) — compose the package, publish and GitHub release actions. diff --git a/actions/package-artifacts/PackageArtifacts.js b/actions/package-artifacts/PackageArtifacts.js new file mode 100644 index 0000000..7fa3c96 --- /dev/null +++ b/actions/package-artifacts/PackageArtifacts.js @@ -0,0 +1,104 @@ +const path = require("node:path"); + +class PackageArtifacts { + constructor({ github, context }) { + this.github = github; + this.context = context; + } + + parse(input) { + const packages = JSON.parse(input); + if (!Array.isArray(packages) || !packages.length || packages.length > 256) + throw new Error( + "Expected a nonempty package array with at most 256 entries.", + ); + for (const item of packages) { + if ( + !item || + typeof item.name !== "string" || + !/^(?:@[a-z0-9-]+\/)?[a-z0-9][a-z0-9._-]*$/.test(item.name) || + item.slug !== item.name.replace(/^@/, "").replace("/", "-") || + typeof item.version !== "string" || + !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(item.version) || + typeof item.directory !== "string" || + !item.directory || + path.isAbsolute(item.directory) || + item.directory.split(/[\\/]/).includes("..") + ) + throw new Error( + "Expected valid package names, slugs, stable versions and relative directories.", + ); + } + if (new Set(packages.map((item) => item.slug)).size !== packages.length) + throw new Error("Package slugs must be unique."); + return packages; + } + + matches(artifact, item) { + const prefix = `npm-${item.slug}-${this.context.runId}-`; + return ( + artifact.name.startsWith(prefix) && + /^[1-9]\d*\.tgz$/.test(artifact.name.slice(prefix.length)) + ); + } + + validateArtifact(artifact, item) { + if ( + !this.matches(artifact, item) || + artifact.expired || + artifact.workflow_run?.id !== this.context.runId + ) + throw new Error( + `Package ${item.name} must use an unexpired CI artifact from this run.`, + ); + } + + async collect(input) { + const packages = this.parse(input); + const artifacts = await this.github.paginate( + this.github.rest.actions.listWorkflowRunArtifacts, + { + ...this.context.repo, + run_id: this.context.runId, + per_page: 100, + }, + ); + return packages.map((item) => { + // Successful matrix jobs from earlier attempts remain valid on a failed-job rerun. + const artifact = artifacts + .filter((candidate) => this.matches(candidate, item)) + .sort((a, b) => b.id - a.id)[0]; + if (!artifact) throw new Error(`No CI tarball found for ${item.name}.`); + this.validateArtifact(artifact, item); + return { + ...item, + "artifact-id": artifact.id, + "artifact-name": artifact.name, + }; + }); + } + + async verify(input, version) { + const packages = this.parse(input); + for (const item of packages) { + if (item.version !== version) + throw new Error( + "CI package version must match the prepared release tag.", + ); + if (!Number.isSafeInteger(item["artifact-id"]) || item["artifact-id"] < 1) + throw new Error("Expected a positive CI artifact ID."); + const { data: artifact } = await this.github.rest.actions.getArtifact({ + ...this.context.repo, + artifact_id: item["artifact-id"], + }); + this.validateArtifact(artifact, item); + if (artifact.workflow_run.head_sha !== this.context.sha) + throw new Error("CI artifact must come from this prepared commit."); + if (item["artifact-name"] !== artifact.name) + throw new Error("CI artifact filename does not match its ID."); + } + return packages; + } +} + +module.exports = { PackageArtifacts }; diff --git a/actions/release/build/README.md b/actions/package-artifacts/README.md similarity index 55% rename from actions/release/build/README.md rename to actions/package-artifacts/README.md index b459ef4..d44d220 100644 --- a/actions/release/build/README.md +++ b/actions/package-artifacts/README.md @@ -1,9 +1,9 @@ -# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItcGFja2FnZSIgY29sb3I9ImJsdWUiPjxsaW5lIHgxPSIxNi41IiB5MT0iOS40IiB4Mj0iNy41IiB5Mj0iNC4yMSI+PC9saW5lPjxwYXRoIGQ9Ik0yMSAxNlY4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNnoiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIzLjI3IDYuOTYgMTIgMTIuMDEgMjAuNzMgNi45NiI+PC9wb2x5bGluZT48bGluZSB4MT0iMTIiIHkxPSIyMi4wOCIgeDI9IjEyIiB5Mj0iMTIiPjwvbGluZT48L3N2Zz4=) GitHub Action: Build Node.js release artifacts +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItcGFja2FnZSIgY29sb3I9ImJsdWUiPjxsaW5lIHgxPSIxNi41IiB5MT0iOS40IiB4Mj0iNy41IiB5Mj0iNC4yMSI+PC9saW5lPjxwYXRoIGQ9Ik0yMSAxNlY4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNnoiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIzLjI3IDYuOTYgMTIgMTIuMDEgMjAuNzMgNi45NiI+PC9wb2x5bGluZT48bGluZSB4MT0iMTIiIHkxPSIyMi4wOCIgeDI9IjEyIiB5Mj0iMTIiPjwvbGluZT48L3N2Zz4=) GitHub Action: Collect npm package artifacts
- Build Node.js release artifacts + Collect npm package artifacts
--- @@ -11,7 +11,7 @@ -[![Marketplace](https://img.shields.io/badge/Marketplace-build--node.js--release--artifacts-blue?logo=github-actions)](https://github.com/marketplace/actions/build-node.js-release-artifacts) +[![Marketplace](https://img.shields.io/badge/Marketplace-collect--npm--package--artifacts-blue?logo=github-actions)](https://github.com/marketplace/actions/collect-npm-package-artifacts) [![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) [![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) [![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) @@ -22,18 +22,16 @@ ## Overview -Build the checked-out release source and preserve artifact paths for packaging, independently of CI checks. -See the [release guide](../../../docs/usage/releases.md) for configuration and the CI boundary. +Collect immutable tarball artifact IDs after CI packaging, including successful matrix jobs from earlier attempts of the same run. ## Inputs -| **Input** | **Description** | **Required** | **Default** | -| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------- | -| **`working-directory`** | Project directory relative to the caller repository root. Check out the release source before calling this action. | **false** | `.` | -| **`build`** | JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifact paths are relative to the project directory or absolute. | **false** | `{"commands":["build"],"artifact":["dist"]}` | +| **Input** | **Description** | **Required** | **Default** | +| -------------- | -------------------------------------------------------------------------------------- | ------------ | ----------- | +| **`packages`** | JSON list of packages with name, version, directory and slug from release preparation. | **true** | - | @@ -42,9 +40,9 @@ See the [release guide](../../../docs/usage/releases.md) for configuration and t ## Outputs -| **Output** | **Description** | -| ----------------- | --------------------------------------------------------------------------------------------------------- | -| **`artifact-id`** | Build artifact ID. Download to / to restore the original paths for packaging or documentation deployment. | +| **Output** | **Description** | +| -------------- | ----------------------------------------------------------------------------------------- | +| **`packages`** | JSON package list with artifact-id and artifact-name, verified against this workflow run. | diff --git a/actions/package-artifacts/action.yml b/actions/package-artifacts/action.yml new file mode 100644 index 0000000..9e98dcc --- /dev/null +++ b/actions/package-artifacts/action.yml @@ -0,0 +1,26 @@ +name: Collect npm package artifacts +description: Collect immutable tarball artifact IDs after CI packaging, including successful matrix jobs from earlier attempts of the same run. +author: hoverkraft +branding: + icon: package + color: blue +inputs: + packages: + description: JSON list of packages with name, version, directory and slug from release preparation. + required: true +outputs: + packages: + description: JSON package list with artifact-id and artifact-name, verified against this workflow run. + value: ${{ steps.collect.outputs.packages }} +runs: + using: composite + steps: + - id: collect + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + PACKAGES: ${{ inputs.packages }} + with: + script: | + const { PackageArtifacts } = require(`${process.env.ACTION_PATH}/PackageArtifacts.js`); + core.setOutput('packages', await new PackageArtifacts({ github, context }).collect(process.env.PACKAGES)); diff --git a/actions/release/ReleaseArtifacts.js b/actions/release/ReleaseArtifacts.js index bfc259e..a102ef1 100644 --- a/actions/release/ReleaseArtifacts.js +++ b/actions/release/ReleaseArtifacts.js @@ -1,5 +1,6 @@ const { createHash } = require("node:crypto"); const fs = require("node:fs"); +const { execFileSync } = require("node:child_process"); const path = require("node:path"); class ReleaseArtifacts { @@ -24,6 +25,22 @@ class ReleaseArtifacts { } } + static verifyTarball({ name, version, tarballPath }) { + const manifest = JSON.parse( + execFileSync("tar", ["-xOf", tarballPath, "package/package.json"], { + encoding: "utf8", + }), + ); + if ( + manifest.name !== name || + manifest.version !== version || + manifest.private + ) + throw new Error( + "CI tarball must contain the expected public package and release version.", + ); + } + async restorePackage({ name, version, tarballPath }) { const response = await this.fetchPackage( `https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, diff --git a/actions/release/ReleaseBuild.js b/actions/release/ReleaseBuild.js deleted file mode 100644 index db9ac18..0000000 --- a/actions/release/ReleaseBuild.js +++ /dev/null @@ -1,57 +0,0 @@ -const { randomUUID } = require("node:crypto"); -const path = require("node:path"); - -class ReleaseBuild { - constructor(workingDirectory) { - this.workingDirectory = workingDirectory; - } - - configure(input) { - const build = JSON.parse(input); - if (!build || Array.isArray(build) || typeof build !== "object") { - throw new Error("build must be a JSON object."); - } - const commands = ReleaseBuild.strings( - build.commands ?? ["build"], - "commands", - ); - const env = build.env ?? {}; - if (Array.isArray(env) || typeof env !== "object") { - throw new Error("build.env must be an object."); - } - const artifactPaths = build.artifact?.paths ?? build.artifact; - const paths = ReleaseBuild.strings( - typeof artifactPaths === "string" - ? artifactPaths.split("\n") - : artifactPaths, - "artifact paths", - ).map((artifact) => { - // Keep the complete path when upload-artifact strips the glob prefix. - // Package and documentation jobs restore this artifact to /. - return path - .resolve(this.workingDirectory, artifact) - .replace(/^\/([^/]+)/, "/*$1"); - }); - return { - commands: commands.join("\n"), - env: JSON.stringify(env), - artifact: JSON.stringify({ - name: `release-build-${randomUUID()}`, - paths: paths.join("\n"), - }), - }; - } - - static strings(value, name) { - if ( - !Array.isArray(value) || - !value.length || - value.some((item) => typeof item !== "string" || !item.trim()) - ) { - throw new Error(`build.${name} must contain nonempty strings.`); - } - return value.map((item) => item.trim()); - } -} - -module.exports = { ReleaseBuild }; diff --git a/actions/release/ReleaseProject.js b/actions/release/ReleaseProject.js index 36340b6..af3d868 100644 --- a/actions/release/ReleaseProject.js +++ b/actions/release/ReleaseProject.js @@ -130,12 +130,20 @@ class ReleaseProject { } versionFromTag(tag) { + return ReleaseProject.versionFromTag( + tag, + this.directory, + this.relativeDirectory, + ); + } + + static versionFromTag(tag, directory, relativeDirectory) { const slug = path - .basename(this.directory) + .basename(directory) .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); - const prefixes = ["", "v", ...(this.relativeDirectory ? [`${slug}-`] : [])]; + const prefixes = ["", "v", ...(relativeDirectory ? [`${slug}-`] : [])]; const version = prefixes .map((prefix) => (tag.startsWith(prefix) ? tag.slice(prefix.length) : "")) .find((candidate) => diff --git a/actions/release/build/action.yml b/actions/release/build/action.yml deleted file mode 100644 index 1163dc1..0000000 --- a/actions/release/build/action.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Build Node.js release artifacts -description: | - Build the checked-out release source and preserve artifact paths for packaging, independently of CI checks. - See the [release guide](../../../docs/usage/releases.md) for configuration and the CI boundary. -author: hoverkraft -branding: - icon: package - color: blue -inputs: - working-directory: - description: Project directory relative to the caller repository root. Check out the release source before calling this action. - default: . - build: - description: JSON object with commands (script names), artifact (paths or an object with paths), and optional env. Artifact paths are relative to the project directory or absolute. - default: '{"commands":["build"],"artifact":["dist"]}' -outputs: - artifact-id: - description: Build artifact ID. Download to / to restore the original paths for packaging or documentation deployment. - value: ${{ steps.build.outputs.artifact-id }} -runs: - using: composite - steps: - - id: working-directory - uses: hoverkraft-tech/ci-github-common/actions/working-directory@44f6108cef0e9884ea7735f3d62932c2310745d4 # 0.39.1 - with: - working-directory: ${{ inputs.working-directory }} - - id: configuration - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - ACTION_PATH: ${{ github.action_path }} - BUILD_INPUT: ${{ inputs.build }} - WORKING_DIRECTORY: ${{ steps.working-directory.outputs.absolute-path }} - with: - script: | - const { ReleaseBuild } = require(`${process.env.ACTION_PATH}/../ReleaseBuild.js`); - const configuration = new ReleaseBuild(process.env.WORKING_DIRECTORY).configure(process.env.BUILD_INPUT); - for (const [name, value] of Object.entries(configuration)) core.setOutput(name, value); - - id: build - uses: $/actions/build - with: - working-directory: ${{ steps.working-directory.outputs.absolute-path }} - build-commands: ${{ steps.configuration.outputs.commands }} - build-env: ${{ steps.configuration.outputs.env }} - build-artifact: ${{ steps.configuration.outputs.artifact }} diff --git a/actions/release/prepare/README.md b/actions/release/prepare/README.md index 10020da..fc784aa 100644 --- a/actions/release/prepare/README.md +++ b/actions/release/prepare/README.md @@ -44,7 +44,7 @@ Plan with HoverKraft, apply the version and changelog with npm or Lerna, and dis | **Output** | **Description** | | --------------- | ------------------------------------------------------------------------------------------------------- | -| **`status`** | One of no-changes, dispatched, ready or released. Only ready should start publication jobs. | +| **`status`** | One of no-changes, dispatched, ready or released. Only ready should start CI, followed by publication. | | **`tag`** | Stable release tag, empty when preparation has no release to publish. | | **`packages`** | JSON array of public packages at the committed release version, with name, version, directory and slug. | | **`published`** | Whether the tag already has a published GitHub release. | diff --git a/actions/release/prepare/action.yml b/actions/release/prepare/action.yml index f8d7032..b31fdf3 100644 --- a/actions/release/prepare/action.yml +++ b/actions/release/prepare/action.yml @@ -16,7 +16,7 @@ inputs: default: ${{ github.token }} outputs: status: - description: One of no-changes, dispatched, ready or released. Only ready should start publication jobs. + description: One of no-changes, dispatched, ready or released. Only ready should start CI, followed by publication. value: ${{ steps.prepare.outputs.status }} tag: description: Stable release tag, empty when preparation has no release to publish. @@ -107,7 +107,7 @@ runs: const messages = { 'no-changes': 'No changes to release.', dispatched: `Publication dispatched for ${result.tag}. Follow the new workflow run for that tag.`, - ready: `Publishing committed release ${result.tag}.`, + ready: `Ready for CI on committed release ${result.tag}.`, released: `${result.tag} is already released.`, }; await core.summary.addRaw(messages[result.status]).write(); diff --git a/actions/release/restore-package/README.md b/actions/release/restore-package/README.md index 7178e8b..b7c6f55 100644 --- a/actions/release/restore-package/README.md +++ b/actions/release/restore-package/README.md @@ -22,7 +22,7 @@ ## Overview -Check an exact npm version and restore its original tarball with integrity verification when resuming publication. +Validate a CI tarball, check an exact npm version and restore its original tarball with integrity verification when resuming publication. diff --git a/actions/release/restore-package/action.yml b/actions/release/restore-package/action.yml index 4b2c891..a191917 100644 --- a/actions/release/restore-package/action.yml +++ b/actions/release/restore-package/action.yml @@ -1,5 +1,5 @@ name: Restore published Node.js package -description: Check an exact npm version and restore its original tarball with integrity verification when resuming publication. +description: Validate a CI tarball, check an exact npm version and restore its original tarball with integrity verification when resuming publication. author: hoverkraft branding: icon: package @@ -31,5 +31,7 @@ runs: with: script: | const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); - const published = await new ReleaseArtifacts().restorePackage({ name: process.env.PACKAGE_NAME, version: process.env.PACKAGE_VERSION, tarballPath: process.env.TARBALL_PATH }); + const options = { name: process.env.PACKAGE_NAME, version: process.env.PACKAGE_VERSION, tarballPath: process.env.TARBALL_PATH }; + ReleaseArtifacts.verifyTarball(options); + const published = await new ReleaseArtifacts().restorePackage(options); core.setOutput('published', String(published)); diff --git a/actions/release/verify/README.md b/actions/release/verify/README.md new file mode 100644 index 0000000..1b6cf72 --- /dev/null +++ b/actions/release/verify/README.md @@ -0,0 +1,79 @@ + + +# ![Icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hlY2stY2lyY2xlIiBjb2xvcj0iYmx1ZSI+PHBhdGggZD0iTTIyIDExLjA4VjEyYTEwIDEwIDAgMSAxLTUuOTMtOS4xNCI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjIyIDQgMTIgMTQuMDEgOSAxMS4wMSI+PC9wb2x5bGluZT48L3N2Zz4=) GitHub Action: Verify release package artifacts + +
+ Verify release package artifacts +
+ +--- + + + + +[![Marketplace](https://img.shields.io/badge/Marketplace-verify--release--package--artifacts-blue?logo=github-actions)](https://github.com/marketplace/actions/verify-release-package-artifacts) +[![Release](https://img.shields.io/github/v/release/hoverkraft-tech/ci-github-nodejs)](https://github.com/hoverkraft-tech/ci-github-nodejs/releases) +[![License](https://img.shields.io/github/license/hoverkraft-tech/ci-github-nodejs)](http://choosealicense.com/licenses/mit/) +[![Stars](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social)](https://img.shields.io/github/stars/hoverkraft-tech/ci-github-nodejs?style=social) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) + + + + +## Overview + +Require CI tarballs from this tag, workflow run and source commit before creating release state. + + + + +## Inputs + +| **Input** | **Description** | **Required** | **Default** | +| ----------------------- | -------------------------------------------------------------------------------- | ------------ | ----------- | +| **`package-artifacts`** | JSON package artifact list from the CI workflow. | **true** | - | +| **`working-directory`** | Project directory relative to the repository root, matching release preparation. | **false** | `.` | + + + + + + +## Outputs + +| **Output** | **Description** | +| --------------- | ----------------------------------------------------------------------------- | +| **`packages`** | Verified package artifact list for publication. | +| **`tag`** | Release tag from this workflow's immutable source ref. | +| **`published`** | Whether the GitHub release is already public and its notes must be preserved. | + + + + +## Contributing + +Contributions are welcome! Please see the [contributing guidelines](https://github.com/hoverkraft-tech/ci-github-nodejs/blob/main/CONTRIBUTING.md) for more details. + + + + + + +## License + +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2026 hoverkraft + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + + + +--- + +This documentation was automatically generated by [CI Dokumentor](https://github.com/hoverkraft-tech/ci-dokumentor). + + diff --git a/actions/release/verify/action.yml b/actions/release/verify/action.yml new file mode 100644 index 0000000..66f0be7 --- /dev/null +++ b/actions/release/verify/action.yml @@ -0,0 +1,49 @@ +name: Verify release package artifacts +description: Require CI tarballs from this tag, workflow run and source commit before creating release state. +author: hoverkraft +branding: + icon: check-circle + color: blue +inputs: + package-artifacts: + description: JSON package artifact list from the CI workflow. + required: true + working-directory: + description: Project directory relative to the repository root, matching release preparation. + default: . +outputs: + packages: + description: Verified package artifact list for publication. + value: ${{ steps.verify.outputs.packages }} + tag: + description: Release tag from this workflow's immutable source ref. + value: ${{ steps.verify.outputs.tag }} + published: + description: Whether the GitHub release is already public and its notes must be preserved. + value: ${{ steps.verify.outputs.published }} +runs: + using: composite + steps: + - id: verify + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ACTION_PATH: ${{ github.action_path }} + PACKAGES: ${{ inputs.package-artifacts }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + script: | + const path = require('node:path'); + const { PackageArtifacts } = require(`${process.env.ACTION_PATH}/../../package-artifacts/PackageArtifacts.js`); + const { ReleaseProject } = require(`${process.env.ACTION_PATH}/../ReleaseProject.js`); + const { ReleaseArtifacts } = require(`${process.env.ACTION_PATH}/../ReleaseArtifacts.js`); + if (!context.ref.startsWith('refs/tags/')) throw new Error('Publish from the prepared release tag.'); + const tag = context.ref.slice('refs/tags/'.length); + const directory = path.resolve(process.env.GITHUB_WORKSPACE, process.env.WORKING_DIRECTORY); + const relativeDirectory = path.relative(process.env.GITHUB_WORKSPACE, directory); + if (relativeDirectory.startsWith('..') || path.isAbsolute(relativeDirectory)) throw new Error('Release directory must be inside the repository.'); + const version = ReleaseProject.versionFromTag(tag, directory, relativeDirectory); + const packages = await new PackageArtifacts({ github, context }).verify(process.env.PACKAGES, version); + const release = await ReleaseArtifacts.findRelease(github, context.repo, tag); + core.setOutput('packages', packages); + core.setOutput('tag', tag); + core.setOutput('published', String(Boolean(release && !release.draft))); diff --git a/docs/README.md b/docs/README.md index 75f3bd1..a5bcdae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,3 +6,4 @@ - [Release orchestration decision](adr/0001-release-orchestration.md) - [Separation of CI and release](adr/0002-separate-ci-from-release.md) - [HoverKraft release planning and publication](adr/0003-hoverkraft-release-actions.md) +- [CI produces release artifacts](adr/0004-ci-produces-release-artifacts.md) diff --git a/docs/adr/0002-separate-ci-from-release.md b/docs/adr/0002-separate-ci-from-release.md index 68e6d00..d43ed6f 100644 --- a/docs/adr/0002-separate-ci-from-release.md +++ b/docs/adr/0002-separate-ci-from-release.md @@ -1,6 +1,7 @@ # ADR 0002: Separate CI checks from release publication -- Status: accepted +- Status: accepted; build ownership and orchestration partially superseded by + [ADR 0004](0004-ci-produces-release-artifacts.md) - Date: 2026-10-31 - Partially supersedes: [ADR 0001](0001-release-orchestration.md) diff --git a/docs/adr/0003-hoverkraft-release-actions.md b/docs/adr/0003-hoverkraft-release-actions.md index fa39a20..528a605 100644 --- a/docs/adr/0003-hoverkraft-release-actions.md +++ b/docs/adr/0003-hoverkraft-release-actions.md @@ -1,6 +1,7 @@ # ADR 0003: Use HoverKraft release actions for planning and publication -- Status: accepted +- Status: accepted; build ownership and orchestration partially superseded by + [ADR 0004](0004-ci-produces-release-artifacts.md) - Date: 2026-10-31 - Partially supersedes: [ADR 0001](0001-release-orchestration.md) diff --git a/docs/adr/0004-ci-produces-release-artifacts.md b/docs/adr/0004-ci-produces-release-artifacts.md new file mode 100644 index 0000000..46c0dec --- /dev/null +++ b/docs/adr/0004-ci-produces-release-artifacts.md @@ -0,0 +1,49 @@ +# ADR 0004: CI produces the artifacts consumed by release + +- Status: accepted +- Date: 2026-10-31 +- Partially supersedes: [ADR 0002](0002-separate-ci-from-release.md) and + [ADR 0003](0003-hoverkraft-release-actions.md) + +## Context + +ADR 0002 separated validation from publication but left compilation and packaging +inside release. This duplicated build ownership and did not prove that CI checked +the generated version commit. Artifacts from before version preparation can embed +an old version, so simply reusing the preceding branch build is insufficient. + +## Decision + +Expose preparation and publication as separate reusable workflows. The caller +connects preparation, its existing CI configuration, and publication through `needs`. +Preparation retains HoverKraft's planner, npm/Lerna version application, atomic push +and explicit dispatch at the resulting tag. It never builds the project. + +CI owns compilation, tests and optional packaging on that exact tagged commit. Add +an optional package list input and an artifact list output to the existing CI +workflow. Packaging runs only after all enabled checks succeed. Ordinary CI leaves +packaging disabled. Consumers with a shared CI wrapper forward these two values +instead of duplicating build and test settings. + +Publication consumes immutable CI tarball IDs from the same run and source SHA. +Verify artifact metadata, tag versions and tarball manifests before npm publication. +Keep HoverKraft draft creation, AI summaries, asset upload and final publication. +Remove the release build helper and its configuration surface. + +## Consequences + +One caller serializes the complete pipeline; reusable workflow concurrency groups +remain distinct. The publication job depends on successful CI. Artifact metadata +checks alone cannot prove that tests passed, so that dependency is part of the caller +contract. Cross-run lookup and polling are deliberately excluded. + +A failed-job rerun reuses successful CI artifacts, including successful matrix jobs +from earlier attempts of the same run. A new dispatch at the original tag reruns CI. +Expired artifacts fail explicitly. Existing npm bytes and GitHub assets retain the +integrity and retry protections from ADR 0003. Documentation deploys from CI's build +artifact after publication succeeds. + +The initial packaging path supports the existing default Ubuntu runner and restored +build artifacts, not container-backed CI. This avoids promising path restoration +across incompatible runner layouts. Manual and scheduled triggers and automatic +Conventional Commit versions are unchanged. diff --git a/docs/development/release-tests.md b/docs/development/release-tests.md index d16304e..c36278b 100644 --- a/docs/development/release-tests.md +++ b/docs/development/release-tests.md @@ -7,8 +7,8 @@ npm ci --prefix tests/release npm test --prefix tests/release ``` -Build configuration tests check script/environment forwarding, artifact paths -for single packages and nested workspaces, retry names, and invalid inputs. +Artifact handoff tests check exact run and commit identity, package versions, +missing and expired artifacts, partial matrix reruns, and tarball manifests. The suite uses temporary local Git repositories and offline npm commands. It executes npm and the installed Lerna with planned versions, checks dependent package and lockfile changes, and @@ -21,7 +21,9 @@ failures, draft lookup and staging missing assets for HoverKraft upload. Release plans and rendered notes are supplied as upstream action outputs; this suite does not duplicate HoverKraft's version calculation tests. Tests do not call OpenAI, publish packages or create remote releases. The suite runs in the shared -CI workflow. +CI workflow. The continuous-integration workflow test also enables packaging, +downloads the returned raw artifact ID and compares its compiled file with the +original CI build output. It does not publish a package. Run `make lint` for repository linting. Generate changed action and workflow readmes with the existing CI Dokumentor tool used by `release-actions.yml`; edit diff --git a/docs/usage/custom-releases.md b/docs/usage/custom-releases.md index b9f5452..94d39ad 100644 --- a/docs/usage/custom-releases.md +++ b/docs/usage/custom-releases.md @@ -1,256 +1,58 @@ -# Release a Node.js package - -Publish a package to npm and attach the same tarball to a GitHub release: - -**Plan version → Build release artifacts → Package → Publish to npm → Create GitHub release** - -The workflow below releases a single package from the repository root using stable versions such as `1.2.3`. -It sets the version during packaging. If your build reads the package version or you commit version changes, -use [Commit the version before building](#commit-the-version-before-building). - -## Set up - -1. Configure [release labels and version rules](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/prepare-release.md). - These determine the version and release notes. -2. Check the package's `package.json`: set `name`, `files`, and `repository.url`, and ensure `private` is not `true`. -3. Configure [npm trusted publishing](../../actions/publish/README.md#npm-trusted-publishing) for your repository and workflow filename, `release.yml`. - The example uses GitHub-hosted runners and `id-token: write`; no npm token is needed. - For a new npm package, publish its first version before configuring the trusted publisher. -4. Save the workflow below as `.github/workflows/release.yml` in your project. Before running it: - - Replace every `` with the **same `ci-github-nodejs` commit containing `actions/release/build` and `actions/publish`**. - The `ci-github-publish` actions are already pinned to `1.0.3`. - - Set the `build` script name and `dist/` output path to match your project. -5. Run linting, tests and security analysis in a separate - [CI workflow](../../.github/workflows/continuous-integration.md), with required checks - on pull requests and branch rules. This release workflow does not run or wait for CI. - -## Workflow - -```yaml -name: Release - -on: - schedule: - - cron: "25 8 * * 1" - workflow_dispatch: - inputs: - force: - description: Release even when no relevant changes are detected - type: boolean - required: false - default: false - -permissions: {} - -concurrency: - group: release-${{ github.repository }} - cancel-in-progress: false - -jobs: - plan: - if: github.ref_name == github.event.repository.default_branch - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - outputs: - should-release: ${{ steps.plan.outputs.has-changes == 'true' || (github.event_name == 'workflow_dispatch' && inputs.force) }} - tag: ${{ steps.plan.outputs.tag }} - version: ${{ steps.version.outputs.version }} - steps: - - name: Plan release - id: plan - uses: hoverkraft-tech/ci-github-publish/actions/release/plan@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 - - - id: version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - RELEASE_TAG: ${{ steps.plan.outputs.tag }} - with: - script: | - // This example uses tags such as v1.2.3 or 1.2.3. - const version = process.env.RELEASE_TAG.replace(/^v/, ''); - if (!/^\d+\.\d+\.\d+$/.test(version)) { - return core.setFailed('Expected a stable SemVer tag; adapt the mapping for prereleases or monorepos'); - } - core.setOutput('version', version); - - build: - needs: plan - if: needs.plan.outputs.should-release == 'true' - runs-on: ubuntu-latest - permissions: - contents: read - packages: read - outputs: - artifact-id: ${{ steps.build.outputs.artifact-id }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - id: build - uses: hoverkraft-tech/ci-github-nodejs/actions/release/build@ - with: - build: '{"commands":["build"],"artifact":["dist"]}' - - release: - needs: [plan, build] - if: needs.plan.outputs.should-release == 'true' - runs-on: ubuntu-latest - permissions: - actions: read - contents: write - pull-requests: read - id-token: write - steps: - - name: Package build output - id: package - uses: hoverkraft-tech/ci-github-nodejs/actions/package@ - with: - version: ${{ needs.plan.outputs.version }} - build-artifact-id: ${{ needs.build.outputs.artifact-id }} - # The release build preserves absolute paths in build artifacts. - build-artifact-path: / - - # Add any package smoke tests before publishing. - - name: Check package publishing - uses: hoverkraft-tech/ci-github-nodejs/actions/publish@ - with: - package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} - dry-run: "true" - provenance: "false" - - # Wrap the raw .tgz in a ZIP artifact for release/create. - - id: release-assets - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: release-assets - path: ${{ steps.package.outputs.package-tarball-path }} - if-no-files-found: error - - - id: publish-package - name: Publish package - uses: hoverkraft-tech/ci-github-nodejs/actions/publish@ - with: - package-tarball-artifact-id: ${{ steps.package.outputs.package-tarball-artifact-id }} - tag: latest - - - name: Create GitHub release - uses: hoverkraft-tech/ci-github-publish/actions/release/create@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 - with: - tag: ${{ needs.plan.outputs.tag }} - target-sha: ${{ github.sha }} - release-artifact-id: ${{ steps.release-assets.outputs.artifact-id }} - publish: "true" -``` - -[Package](../../actions/package/README.md) checks out the source, installs dependencies, restores the release build output, -and creates the tarball. [Publish](../../actions/publish/README.md) publishes that tarball. -The package version is changed locally; it is not committed. GitHub's source archive therefore keeps the version from the source commit. -If `package.json` already contains the planned version, omit the Package action's `version` input. - -## Generate the changelog - -`release/create` generates the GitHub release notes with Release Drafter from the merged pull requests since the previous matching release. -No separate changelog command is needed for the workflow above. `release/plan` calculates the version without creating a release. - -Use the [prepare-release workflow](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/prepare-release.md) -to label pull requests from their Conventional Commit titles. Its default rules select a major version for breaking changes, -a minor version for features, and a patch version for other changes. Check the labels before merging. -To customize categories, version rules, or the release-note template, add `.github/release-config.yml`. -For monorepos, use the same `working-directory` and `include-paths` values in `release/plan` and `release/create`; -an existing repository configuration takes precedence over those generated defaults. - -### Add an AI summary - -Store the provider credential as the `OPENAI_API_KEY` Actions secret, then add these entries to the **Create GitHub release** step. -Merge `changelog-summary` into its existing `with` mapping: - -```yaml -env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} -with: - changelog-summary: | - { - "llmAuth": ${{ toJSON(env.OPENAI_API_KEY) }}, - "llmProvider": "openai" - } -``` - -The action drafts the changelog, generates a release summary and breaking-change section, and prepends them to the full changelog before publishing. -The summary also uses GitHub evidence, so keep `contents: write` and `pull-requests: read` on the release job. -Use `toJSON` to encode the credential safely. Omit `changelog-summary` to publish the generated notes without an AI summary. -See [summarizer inputs](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/actions/release/summarize-changelog/README.md) -for `llmModel`, `llmConfig`, and custom summary templates. - -To review the notes before publication, set `publish: "false"`, review the draft, then publish it with `release/update`. -If summary generation fails, the action does not publish the GitHub release. The npm package may already be published; -follow [Recover a failed release](#recover-a-failed-release) instead of rerunning npm publication. - -### Keep a changelog in the repository - -Release notes are stored in the GitHub release body; these actions do not write or commit `CHANGELOG.md`. -If the source archive or npm tarball must contain it, generate and review the changelog on the release branch, -commit it with the version update, and build from that merged commit as described in [Commit the version before building](#commit-the-version-before-building). -Keep the planned tag fixed through preparation and publication. - -## Run a release - -In **Actions → Release → Run workflow**, select the default branch. The workflow also runs every Monday at 08:25 UTC. -Releases are serialized so two runs cannot publish the same package concurrently. - -| Situation | Result | -| ---------------------------------------------- | ---------------------------------------------------------------------------------- | -| Relevant changes since the last GitHub release | Plan the next version, build, and release. | -| No relevant changes | Skip building and publishing successfully. | -| Manual run with `force` checked | Release the planned version even without changes; the build must still pass. | -| No previous matching GitHub release | Plan the first GitHub release. Registry authentication must already be configured. | - -The dry run checks the tarball with `npm publish --dry-run`. It does not verify registry credentials or trusted publishing access. -For a first trial without publishing, keep the dry-run step and remove the **Publish package** and **Create GitHub release** steps. - -## Commit the version before building - -Use this when the build embeds the version or the released source must include updated version files or a changelog: - -1. Plan the version with `release/plan`, using the same no-change and `force` rules as the workflow above. -2. Update the version on a release branch, for example with `npm version 1.2.3 --no-git-tag-version`. - Commit the package manifest, lockfile, changelog, and any generated version files through a pull request. -3. After merging, run the release build and packaging **from the merged commit**. Omit the Package action's `version` input; the version is already in `package.json`. -4. Publish the tarball, then create the GitHub release with the planned `tag` and merged commit as `target-sha`. - -Keep the planned tag throughout this process; do not calculate another version after merging. -If you use a second workflow for publication, read the committed version instead of calling `release/plan` again, -and skip it when that version is already released. - -The Package action performs its own checkout of the workflow's source ref. -Checking out the merged commit in an earlier step does not change that ref: start the packaging workflow at the merged commit. -See the [release preparation example](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md#example-3-validate-source-update-files-create-release-artifacts-and-release) -for automating the pull request and merge. - -## Other setups - -- **GitHub Packages or registry tokens:** use the [authentication examples](../../actions/publish/README.md#registry-tokens-and-github-packages). - The `github-token` input only authenticates artifact downloads; registry credentials use `NODE_AUTH_TOKEN`. -- **Prereleases:** set `prerelease: "true"` on `release/plan` and `release/create`, and `tag: next` on both Publish steps. - Update the version conversion to accept your full prerelease version, such as `1.2.3-rc.1`. - The npm distribution tag (`next`) is separate from the Git release tag (`v1.2.3-rc.1`). -- **Monorepos:** scope the build and both release actions with `working-directory`. - For Package, set `working-directory` to the dependency installation root and `package-directory` to the package's relative path. - Convert prefixed Git tags such as `my-package/v1.2.3` to npm versions, and use a separate concurrency group and artifact name per package. -- **Drafts and signing:** create a draft with `publish: "false"`, then use `release/update` to attach assets and publish after npm publication succeeds. - See the [draft release example](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md#example-2-validate-source-create-release-artifacts-and-release). - Use `release/delete` with `draft-only: "true"` to clean up a temporary draft on failure **only if the npm package has not been published**. - -## Recover a failed release - -Check the registry before retrying: an npm package version cannot be overwritten, and a failed run may still have published it. - -| Where it failed | What to do | -| ------------------------------------------------- | ------------------------------------------------------------------------ | -| Before npm publication | Fix the failure and rerun the workflow. | -| npm published, but GitHub release creation failed | Run only release creation with the original tag, source SHA, and assets. | -| npm published, but a draft was not finalized | Keep the draft and retry `release/update`. | - -After npm publication, do not rerun the whole release job or plan a new version to finish the same release. -Deleting a draft does not undo npm publication. +# Compose a custom release pipeline + +Use [the shared release workflows](releases.md) for single npm packages and +fixed-version Lerna workspaces. For custom registries or packaging rules, compose +the existing actions while keeping preparation, CI and publication separate. + +## Preparation + +Use HoverKraft [`release/plan`](https://github.com/hoverkraft-tech/ci-github-publish/tree/1.0.3/actions/release/plan) +for version selection. Commit version, lockfile and changelog updates before CI +builds the source, especially when compiled files embed the version. Keep the planned +tag fixed throughout the pipeline. The [Node.js preparation action](../../actions/release/prepare/README.md) +handles this for npm and fixed-version Lerna projects and dispatches the caller at the tag. + +## CI artifacts + +Run the [CI workflow](../../.github/workflows/continuous-integration.md) against the +prepared commit. Its optional `packages` input delegates packaging to the +[Package action](../../actions/package/README.md) after checks pass. It returns +`package-artifacts` for publication and `build-artifact-id` for documentation deployment. + +If you own the packaging jobs, the Package action accepts a build artifact ID and +package directory. It checks out the workflow's source ref, installs dependencies, +restores build output, and runs `npm pack`. Start that workflow at the prepared tag; +an earlier step checking out a different ref does not change Package's own checkout. +Leave its `version` input empty when versions are already committed. Run any tarball +smoke tests in CI before making artifacts available to publication. + +## Publication + +Keep publication dependent on successful CI. The [Publish action](../../actions/publish/README.md) +consumes one CI tarball artifact ID and never packages the project. A dry run validates +`npm publish` inputs, but does not prove registry authentication or trusted publishing. +See its [authentication guide](../../actions/publish/README.md#registry-tokens-and-github-packages) +for registries beyond npmjs.org. + +Use HoverKraft [`release/create`](https://github.com/hoverkraft-tech/ci-github-publish/tree/1.0.3/actions/release/create) +to draft notes and optional AI summaries before npm publication. Pass the prepared +tag and source SHA. Set `publish: "false"` until all packages and assets succeed. +Use [`release/update`](https://github.com/hoverkraft-tech/ci-github-publish/tree/1.0.3/actions/release/update) +to attach assets and publish the draft. Those actions expect ZIP artifacts for release +assets; the Package action's raw `.tgz` must be wrapped with `upload-artifact` using +its default archive mode. + +The [shared publication workflow](../../.github/workflows/release.md) already performs +these steps for npmjs.org, including exact artifact identity checks, AI summaries and +safe retries. It accepts only artifacts from CI in the same run and at the same tag. + +## Recovery + +npm publication across several packages cannot be atomic. Check exact package +versions before retrying; published versions cannot be overwritten. Restore original +published bytes for release assets, verify integrity and existing asset digests, and +upload only missing assets. Do not delete a draft to undo an npm publication. + +The [release recovery guide](releases.md#run-and-resume-releases) describes this behavior +for the shared workflow. Custom pipelines own equivalent recovery rules. Keep the +original tag and source identity when finishing a partial release. diff --git a/docs/usage/examples/reactionable-cli.yml b/docs/usage/examples/reactionable-cli.yml index 3d2bb91..8b53f52 100644 --- a/docs/usage/examples/reactionable-cli.yml +++ b/docs/usage/examples/reactionable-cli.yml @@ -1,4 +1,4 @@ -# Replace with a published ci-github-nodejs commit containing the Release workflow. +# Replace with a published ci-github-nodejs commit containing these workflows. name: Release on: @@ -8,26 +8,55 @@ on: permissions: {} +concurrency: + group: release-${{ github.repository }} + cancel-in-progress: false + jobs: - release: - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + prepare: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/prepare-release.yml@ permissions: actions: write contents: write pull-requests: read + with: + environment: github-pages + + ci: + needs: prepare + if: needs.prepare.outputs.status == 'ready' + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@ + permissions: + actions: read + contents: read packages: read + pull-requests: write + security-events: write id-token: write with: - environment: github-pages + packages: ${{ needs.prepare.outputs.packages }} build: | {"commands":["build","build:doc"],"artifact":["dist","build"]} + test: '{"coverage":"codecov"}' + + release: + needs: ci + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + permissions: + actions: read + contents: write + pull-requests: read + id-token: write + with: + package-artifacts: ${{ needs.ci.outputs.package-artifacts }} + environment: github-pages ai-summary: true secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} pages: name: Deploy documentation - needs: release + needs: [ci, release] if: needs.release.outputs.status == 'published' runs-on: ubuntu-latest permissions: @@ -46,7 +75,7 @@ jobs: - id: deployment uses: hoverkraft-tech/ci-github-publish/actions/deploy/github-pages@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 with: - build-artifact-id: ${{ needs.release.outputs.build-artifact-id }} + build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} build-path: build/docs deploy-checks: diff --git a/docs/usage/examples/reactionable.yml b/docs/usage/examples/reactionable.yml index a1b164a..6740598 100644 --- a/docs/usage/examples/reactionable.yml +++ b/docs/usage/examples/reactionable.yml @@ -1,4 +1,4 @@ -# Replace with a published ci-github-nodejs commit containing the Release workflow. +# Replace with a published ci-github-nodejs commit containing these workflows. name: Release on: @@ -8,27 +8,56 @@ on: permissions: {} +concurrency: + group: release-${{ github.repository }} + cancel-in-progress: false + jobs: - release: - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + prepare: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/prepare-release.yml@ permissions: actions: write contents: write pull-requests: read - packages: read - id-token: write with: versioning: lerna environment: github-pages + + ci: + needs: prepare + if: needs.prepare.outputs.status == 'ready' + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@ + permissions: + actions: read + contents: read + packages: read + pull-requests: write + security-events: write + id-token: write + with: + packages: ${{ needs.prepare.outputs.packages }} build: | {"commands":["build:ci","storybook:build","docs:build"],"artifact":["packages/**/lib","build","packages/**/docs"]} + test: '{"coverage":"codecov"}' + + release: + needs: ci + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + permissions: + actions: read + contents: write + pull-requests: read + id-token: write + with: + package-artifacts: ${{ needs.ci.outputs.package-artifacts }} + environment: github-pages ai-summary: true secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} pages: name: Deploy documentation - needs: release + needs: [ci, release] if: needs.release.outputs.status == 'published' runs-on: ubuntu-latest permissions: @@ -47,5 +76,5 @@ jobs: - id: deployment uses: hoverkraft-tech/ci-github-publish/actions/deploy/github-pages@ea6682de88070c254dac619a6623a242a6d57cd3 # 1.0.3 with: - build-artifact-id: ${{ needs.release.outputs.build-artifact-id }} + build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} build-path: build/storybook diff --git a/docs/usage/releases.md b/docs/usage/releases.md index 0c274ee..a88503d 100644 --- a/docs/usage/releases.md +++ b/docs/usage/releases.md @@ -1,191 +1,152 @@ -# Release Node.js packages - -Use the [Release workflow](../../.github/workflows/release.md) to release one public -npm package or fixed-version Lerna workspaces. The calling repository supplies -triggers and build settings. HoverKraft prepares versions, builds the tagged -source, packages the build output, publishes to npm and creates the GitHub release. -No custom release scripts are needed in the calling repository. - -Complete caller examples, including documentation deployment: - -- [Single-package CLI](examples/reactionable-cli.yml) -- [Lerna monorepo](examples/reactionable.yml) - -## Configure the repository - -1. Set each public package's `name`, `repository.url`, and package contents (`files` - or `.npmignore`). Include compiled output and any CLI entry points. -2. Configure [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) - for each package. Use the **calling repository and workflow filename**, such - as `release.yml`, and the environment passed below. Allow direct `npm publish`. - Publish the first version of a new npm package before configuring its publisher. -3. Allow the default branch and the tags produced by your release configuration - in the GitHub environment (for example, `*.*.*` for stable version tags). Its name - defaults to `release`; an existing environment can be passed explicitly. -4. Store `OPENAI_API_KEY` as a repository or environment secret when enabling AI - summaries, and pass it explicitly as shown below. -5. Replace `` with an immutable `ci-github-nodejs` commit containing this - reusable workflow. Keep `workflow_dispatch` with no required inputs: preparation - uses it to start publication at the generated tag. - -GitHub-hosted Linux runners and public packages on npmjs.org are supported. -Private packages, custom registries, prereleases and independent Lerna versions -require a [custom pipeline](custom-releases.md). - -## Keep CI separate - -Run linting, tests, coverage and security analysis in the caller's independent -[CI workflow](../../.github/workflows/continuous-integration.md). Configure required -checks on pull requests and branch rules before allowing releases from that branch. -The Release workflow neither runs those checks nor queries or waits for CI results. - -Release builds run from the version tag because package contents can embed the -version. Their artifacts are used for npm packaging and optional documentation -deployment. They are not evidence that CI passed on that tag. In particular, Lerna -and npm both create version commits directly; if they need additional validation, -arrange it in the caller and gate publication through the release environment. - -## Single package - -This configuration fits a TypeScript CLI such as `reactionable-cli`. HoverKraft -plans the version from Conventional Commits and renders the changelog. npm applies -that exact version to `package.json` and npm lockfiles. The workflow commits those -files with `CHANGELOG.md`, pushes the commit and tag atomically, and dispatches -publication at the tag. +# Release a single package or Lerna workspaces + +Connect three reusable workflows: **prepare → CI → publish**. The caller owns the +manual and scheduled triggers. Consumers need no versioning or publication scripts. + +- [Prepare release](../../.github/workflows/prepare-release.md) uses HoverKraft's + Conventional Commit release plan, commits versions and changelogs, and atomically + pushes the version commit and tag. It dispatches the caller at that tag. +- [Continuous integration](../../.github/workflows/continuous-integration.md) builds, + tests and packages the prepared commit. Use the same CI configuration as pull requests. +- [Release](../../.github/workflows/release.md) consumes those CI tarballs, generates + release notes and an optional AI summary, publishes npm packages, and finalizes the + GitHub release. It does not build or pack the project. + +The tag run is essential: artifacts built before version preparation can contain the +old version. All three workflows run against the prepared commit during publication. + +## Set up + +1. Keep the package's stable version and existing GitHub release history aligned. + HoverKraft selects the next version from that history and Conventional Commits. + A plan older than or equal to the committed version fails instead of guessing a + replacement. Configure `.github/release-config.yml` when needed; repository + configuration takes precedence over generated defaults. +2. Configure [npm trusted publishing](../../actions/publish/README.md#npm-trusted-publishing) + for your repository, caller workflow filename and environment. Publish a new + package's first version before configuring its trusted publisher. Publication + uses GitHub-hosted runners with `id-token: write`. +3. Permit the preparation identity to push release commits and tags under your branch + and tag rules. Preparation defaults to `GITHUB_TOKEN`; optionally pass + `github-token` to `prepare-release.yml`. The token does not bypass repository rules. +4. Allow the chosen environment on the default branch and release tags. Set + `OPENAI_API_KEY` and pass it to publication when enabling `ai-summary`. +5. Copy a caller example and replace `` with the same supported + `ci-github-nodejs` commit throughout: + - [Single npm package with documentation deployment](examples/reactionable-cli.yml) + - [Fixed-version Lerna workspaces with documentation deployment](examples/reactionable.yml) + +The minimal connection between the jobs is: ```yaml -name: Release - -on: - workflow_dispatch: - schedule: - - cron: "25 8 * * 1" - -permissions: {} - jobs: - release: - uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + prepare: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/prepare-release.yml@ permissions: actions: write contents: write pull-requests: read + + ci: + needs: prepare + if: needs.prepare.outputs.status == 'ready' + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/continuous-integration.yml@ + permissions: + actions: read + contents: read packages: read + pull-requests: write + security-events: write id-token: write with: - environment: github-pages - build: | - { - "commands": ["build", "build:doc"], - "artifact": ["dist", "build"] - } - ai-summary: true - secrets: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} -``` - -The defaults use `versioning: npm` and `npm run build` with `dist` as the build -artifact. Omit `versioning` and `build` when those defaults match your project. -The `build` input is a JSON object with `commands`, `artifact` paths and an optional -`env` object. Commands are package scripts, not shell commands. The Build action -installs dependencies and executes those scripts. - -## Release planning and branch rules - -Both package modes use `ci-github-publish/actions/release/plan` as their version -planner. Its generated rules use Conventional Commits: breaking changes select -major, features select minor, and other changes select patch. Repository release -configuration takes precedence. See the -[HoverKraft release actions](https://github.com/hoverkraft-tech/ci-github-publish/blob/1.0.3/.github/workflows/release.md) -for configuration, history filtering and change detection. - -The workflow preserves the planned tag. It accepts stable `1.2.3` and `v1.2.3` -tags, plus `-1.2.3` for a project in `working-directory`. -Keep the tag convention and release history aligned with existing packages. -A plan older than or equal to the committed version is rejected. If the first -planned release is behind an existing package version, reconcile the GitHub -release baseline and release configuration before running it. - -Branch and tag rules must allow the preparation identity to push the release -commit and tag. The default is `GITHUB_TOKEN`; pass an appropriate token through -`github-token` when required by your rules or when release commits must trigger -normal push CI. It needs contents and actions write and pull requests read access. -The workflow does not create or auto-merge a release PR, and does not bypass rules. + packages: ${{ needs.prepare.outputs.packages }} + build: '{"commands":["build"],"artifact":["dist"]}' -## Fixed-version Lerna workspaces - -Keep the same triggers, permissions and secrets. Change the inputs to match a -repository such as `reactionable`: - -```yaml -with: - versioning: lerna - environment: github-pages - build: | - { - "commands": ["build:ci", "storybook:build", "docs:build"], - "artifact": ["packages/**/lib", "build", "packages/**/docs"] - } - ai-summary: true + release: + needs: ci + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@ + permissions: + actions: read + contents: write + pull-requests: read + id-token: write + with: + package-artifacts: ${{ needs.ci.outputs.package-artifacts }} ``` -Install Lerna in the project and use a stable fixed version in `lerna.json`. -The workflow passes the exact HoverKraft-planned version to the installed Lerna. -Lerna updates package versions, internal dependency ranges and lockfiles; -`--conventional-commits` generates package changelogs. It does not choose a second -version. The root `CHANGELOG.md` receives HoverKraft's rendered notes. -Lerna can report no changed packages even when the release plan contains changes; -in that case no release commit, tag or publication run is created. - -This mode commits directly to the default branch and pushes the commit and tag -atomically. Repository rules must permit that push. If the branch advances during -preparation, the push fails without pushing the new tag. Existing tags are never -overwritten. Only public workspaces at the new release version are published; -unchanged packages can retain their previous versions. +Add `workflow_dispatch`, your schedule, `permissions: {}` and a caller concurrency +group such as `release-${{ github.repository }}` with `cancel-in-progress: false`. +Use a different group from either reusable workflow. This serializes the whole +pipeline, including CI. If your repository already has a shared CI wrapper, forward +its optional `packages` input and `package-artifacts` output instead of copying build +and test settings into the release caller. Ordinary CI leaves `packages` empty. + +## Versioning and packaging + +Preparation defaults to `versioning: npm` for a single public package. npm applies +the exact planned version to the manifest and lockfile without running version scripts. +For fixed-version workspaces, set `versioning: lerna` on **prepare** and install +Lerna in the project. Lerna applies the exact planned version, updates internal +dependency ranges and lockfiles, and uses `--conventional-commits` for package +changelogs. It does not select another version. Independent versions and prereleases +are unsupported. The root `CHANGELOG.md` receives HoverKraft's rendered notes. + +Only public workspaces at the new version are packaged; unchanged packages can keep +their previous versions. If Lerna reports no changed packages, preparation creates no +commit, tag or publication run. If the default branch advances during preparation, +the atomic push fails without publishing the new tag. Existing tags are never overwritten. + +Set matching `working-directory` values on all three workflows for nested projects. +The planner supports bare stable tags, `v`-prefixed tags and directory-slug prefixes +for nested projects. The committed version must match the selected tag. + +CI packaging requires `checks: true`, a build artifact, the default +`runs-on: '["ubuntu-latest"]'`, and no container. Configure the build artifact to +include the compiled package files and any documentation you deploy. Packaging runs +after all enabled checks succeed, restores the build output and runs `npm pack`. +Package lifecycle scripts therefore run in CI. Publication never installs project +dependencies or invokes its build or packaging scripts. + +CI returns immutable tarball IDs. Publication verifies that each artifact belongs to +this run and commit, then checks the tarball's package name and version before npm +publication. Cross-run artifact reuse is unsupported. A caller must keep `release` +dependent on successful CI; artifact metadata alone is not proof that checks passed. ## Run and resume releases -Run the calling workflow from the default branch or wait for its schedule. -Preparation reports one of these outcomes in the job summary: +Run the caller from the default branch or wait for its schedule. Preparation reports: -| Status | Meaning | -| ------------ | ------------------------------------------------------------- | -| `no-changes` | Nothing needs releasing; no build or publishing starts. | -| `dispatched` | The version tag exists; follow the new run for that tag. | -| `released` | This version already has a published GitHub release. | -| `published` | The tag run published all packages and finalized the release. | +| Status | Meaning | +| ------------ | --------------------------------------------------- | +| `no-changes` | No release needed; CI and publication are skipped. | +| `dispatched` | Follow the new caller run at the prepared tag. | +| `released` | This version already has a public GitHub release. | +| `ready` | This tag run can start CI, followed by publication. | -Both preparation and publication share a concurrency group inside the reusable -workflow. Do not use that same group in the caller. A successful preparation run -does not imply publication succeeded: its tag run builds artifacts and publishes. +Publication reports `published` only after all npm packages and release assets +succeed. Preparation success alone does not mean publication succeeded. -AI generation runs before npm publication. A draft becomes public only after -every package and release asset succeeds. AI summaries accompany the full release -notes and should be checked for accuracy. A published release keeps its notes on retry. +AI generation runs before npm publication. The draft becomes public only after every +package and asset succeeds. Review AI summaries for accuracy; published releases +keep their notes on retries. Asset upload and final publication use HoverKraft +`release/update`. -To resume a release after its tag was pushed, dispatch the **calling** workflow -at the original tag: +For a failed publication job, rerun failed jobs: successful CI tarballs from earlier +attempts of the same run are reused without rebuilding. Expired or deleted artifacts +fail rather than falling back to another run. To run CI again and resume the same +release, dispatch the **caller** at its original tag: ```sh gh workflow run release.yml --ref v1.2.3 ``` -The tag must contain the caller workflow and match the committed version. A tag -run never computes another version. The release build uses that source. Already published npm -versions are skipped and their original tarballs are downloaded with SHA-512 -verification. Existing GitHub assets are reused only when SHA-256 digests match; -conflicting or missing digests fail instead of replacing assets. - -Asset uploads and final publication use HoverKraft `release/update`. On retry, -only missing assets are passed to it after the existing digests are verified. - -npm workspace publication cannot be atomic, so partial publication is possible. -An npm registry error stops recovery; only an absent version permits publishing. -If the release build fails because source code needs changing, make a new commit -and release. -Do not move an existing release tag. - -The `release-url`, `tag`, `status` and `build-artifact-id` workflow outputs support -follow-up jobs. For documentation deployment, require `status == 'published'` -and consume `build-artifact-id` from that tag run. Documentation deployment remains -an independent consumer job. +A tag run never plans another version. Already published npm versions are skipped, +and their original tarballs are restored with SHA-512 verification for GitHub assets. +Existing assets are reused only when SHA-256 digests match. Conflicts or unavailable +digests fail instead of replacing assets. Partial npm publication remains possible; +registry errors stop recovery, and only an absent version permits publication. +If source needs changing, make a new commit and release. Do not move a release tag. + +For documentation deployment, require `release.outputs.status == 'published'` and +consume **CI's** `build-artifact-id`. The publication workflow returns `status`, +`tag` and `release-url`; documentation remains an independent caller job. diff --git a/tests/release/build.test.js b/tests/release/build.test.js deleted file mode 100644 index 20ebd4f..0000000 --- a/tests/release/build.test.js +++ /dev/null @@ -1,60 +0,0 @@ -const assert = require("node:assert/strict"); -const { test } = require("node:test"); -const { ReleaseBuild } = require("../../actions/release/ReleaseBuild.js"); - -const configure = (build, directory = "/home/runner/work/project/project") => - new ReleaseBuild(directory).configure(JSON.stringify(build)); - -test("builds package and documentation scripts with their environment", () => { - const options = configure({ - commands: [" build ", "build:doc"], - env: { NODE_ENV: "production" }, - artifact: ["dist", "build/docs"], - }); - assert.equal(options.commands, "build\nbuild:doc"); - assert.deepEqual(JSON.parse(options.env), { NODE_ENV: "production" }); - assert.equal( - JSON.parse(options.artifact).paths, - "/*home/runner/work/project/project/dist\n/*home/runner/work/project/project/build/docs", - ); -}); - -test("preserves nested workspace output paths when restoring artifacts to root", () => { - const options = configure( - { artifact: { paths: ["packages/**/lib", "build"] } }, - "/home/runner/work/project/project/node", - ); - assert.equal(options.commands, "build"); - assert.equal(options.env, "{}"); - assert.equal( - JSON.parse(options.artifact).paths, - "/*home/runner/work/project/project/node/packages/**/lib\n/*home/runner/work/project/project/node/build", - ); -}); - -test("accepts multiline and absolute artifact paths and gives retries unique names", () => { - const input = { artifact: "dist\n/home/runner/work/project/project/build" }; - const first = JSON.parse(configure(input).artifact); - const second = JSON.parse(configure(input).artifact); - assert.equal( - first.paths, - "/*home/runner/work/project/project/dist\n/*home/runner/work/project/project/build", - ); - assert.notEqual(first.name, second.name); -}); - -test("rejects invalid build configuration before executing scripts", () => { - assert.throws(() => new ReleaseBuild("/tmp").configure("{"), SyntaxError); - for (const input of [null, [], "build", true]) { - assert.throws(() => configure(input), /JSON object/); - } - for (const commands of [[], "build", [1], [" "]]) { - assert.throws(() => configure({ commands, artifact: "dist" }), /commands/); - } - for (const env of [[], "production", 1]) { - assert.throws(() => configure({ env, artifact: "dist" }), /env/); - } - for (const artifact of [undefined, {}, [], "", [false], { paths: 1 }]) { - assert.throws(() => configure({ artifact }), /artifact paths/); - } -}); diff --git a/tests/release/package-artifacts.test.js b/tests/release/package-artifacts.test.js new file mode 100644 index 0000000..67f8511 --- /dev/null +++ b/tests/release/package-artifacts.test.js @@ -0,0 +1,160 @@ +const assert = require("node:assert/strict"); +const { test } = require("node:test"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); +const { + PackageArtifacts, +} = require("../../actions/package-artifacts/PackageArtifacts.js"); +const { + ReleaseArtifacts, +} = require("../../actions/release/ReleaseArtifacts.js"); + +const context = { + repo: { owner: "example", repo: "project" }, + runId: 42, + sha: "prepared-commit", +}; +const item = { + name: "@example/cli", + slug: "example-cli", + version: "1.2.3", + directory: ".", +}; +const artifact = (id, attempt = 1, overrides = {}) => ({ + id, + name: `npm-example-cli-42-${attempt}.tgz`, + expired: false, + workflow_run: { id: 42, head_sha: context.sha }, + ...overrides, +}); +const collector = (artifacts) => + new PackageArtifacts({ + context, + github: { + paginate: async () => artifacts, + rest: { + actions: { + listWorkflowRunArtifacts() {}, + getArtifact: async ({ artifact_id }) => ({ + data: artifacts.find((entry) => entry.id === artifact_id), + }), + }, + }, + }, + }); + +test("collects exact CI artifact IDs and filenames for publication", async () => { + const result = await collector([artifact(10)]).collect( + JSON.stringify([item]), + ); + assert.deepEqual(result, [ + { ...item, "artifact-id": 10, "artifact-name": "npm-example-cli-42-1.tgz" }, + ]); + assert.deepEqual( + await collector([artifact(10)]).verify(JSON.stringify(result), "1.2.3"), + result, + ); +}); + +test("failed-job reruns combine successful packages from different attempts", async () => { + const other = { + ...item, + name: "other", + slug: "other", + directory: "packages/other", + }; + const result = await collector([ + artifact(10), + artifact(20, 2), + artifact(30, 1, { name: "npm-other-42-1.tgz" }), + ]).collect(JSON.stringify([item, other])); + assert.deepEqual( + result.map((entry) => entry["artifact-id"]), + [20, 30], + ); +}); + +test("fails when any package is missing, expired or from another run", async () => { + for (const artifacts of [ + [], + [artifact(10, 1, { expired: true })], + [artifact(10, 1, { workflow_run: { id: 41, head_sha: context.sha } })], + [artifact(10, 1, { workflow_run: undefined })], + ]) { + await assert.rejects( + collector(artifacts).collect(JSON.stringify([item])), + /CI tarball|CI artifact/, + ); + } +}); + +test("rejects ambiguous names, unsafe paths, invalid versions and invalid package lists", () => { + const service = collector([]); + for (const packages of [ + [], + {}, + [null], + [item, item], + [{ ...item, slug: "wrong" }], + [{ ...item, name: "../bad" }], + [{ ...item, directory: "../outside" }], + [{ ...item, directory: "/tmp" }], + [{ ...item, version: "1.2.3-beta" }], + ]) { + assert.throws( + () => service.parse(JSON.stringify(packages)), + /Expected|unique/, + ); + } +}); + +test("publication rejects changed artifact IDs, filenames, source commits and release versions", async () => { + const service = collector([artifact(10)]); + const [valid] = await service.collect(JSON.stringify([item])); + for (const entry of [ + { ...valid, version: "1.2.2" }, + { ...valid, "artifact-id": "10" }, + { ...valid, "artifact-name": "other.tgz" }, + ]) { + await assert.rejects( + service.verify(JSON.stringify([entry]), "1.2.3"), + /version|artifact/, + ); + } + await assert.rejects( + collector([ + artifact(10, 1, { workflow_run: { id: 42, head_sha: "old" } }), + ]).verify(JSON.stringify([valid]), "1.2.3"), + /prepared commit/, + ); +}); + +test("checks the packaged manifest without executing package lifecycle scripts", (t) => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ci-tarball-")); + t.after(() => fs.rmSync(directory, { recursive: true, force: true })); + fs.mkdirSync(path.join(directory, "package")); + const tarballPath = path.join(directory, "package.tgz"); + for (const manifest of [ + { name: item.name, version: item.version }, + { name: "wrong", version: item.version }, + { name: item.name, version: "1.2.2" }, + { name: item.name, version: item.version, private: true }, + ]) { + fs.writeFileSync( + path.join(directory, "package/package.json"), + JSON.stringify({ ...manifest, scripts: { prepublishOnly: "exit 1" } }), + ); + execFileSync("tar", ["-czf", tarballPath, "package"], { cwd: directory }); + const verify = () => + ReleaseArtifacts.verifyTarball({ ...item, tarballPath }); + if ( + manifest.name === item.name && + manifest.version === item.version && + !manifest.private + ) + assert.doesNotThrow(verify); + else assert.throws(verify, /expected public package/); + } +});