diff --git a/.github/workflows/release.md b/.github/workflows/release.md new file mode 100644 index 0000000..0357987 --- /dev/null +++ b/.github/workflows/release.md @@ -0,0 +1,408 @@ + + +# GitHub Reusable Workflow: Node.js Release + +
+ 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 + +Workflow to release Node.js packages with support for: + +- Publishing to various registries (npm, GitHub Packages) +- Publishing from build artifacts or source code +- Publishing pre-built package tarballs +- Generating documentation (optional) +- Provenance attestation for npm packages +- Distribution tags for versioning +- Scoped package access control + +### Permissions + +- **`contents`**: `read` +- **`id-token`**: `write` (required for provenance) +- **`packages`**: `write` + + + + + +## Usage + +### Basic Release from Source + +```yaml +name: Release + +on: + release: + types: [published] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} +``` + +### Release with Build Artifacts from CI + +```yaml +name: Release + +on: + push: + tags: ["*"] + +permissions: {} + +jobs: + ci: + uses: ./.github/workflows/__shared-ci.yml + permissions: + contents: read + id-token: write + packages: read + secrets: inherit + + release: + needs: ci + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} + with: + # Download build artifacts from CI job + build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} + access: public +``` + +### Release Pre-built Tarball + +```yaml +name: Release + +on: + push: + tags: ["*"] + +permissions: {} + +jobs: + ci: + uses: ./.github/workflows/__shared-ci.yml + permissions: + contents: read + id-token: write + secrets: inherit + + release: + needs: ci + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} + with: + build-artifact-id: ${{ needs.ci.outputs.package-tarball-artifact-id }} + package-tarball: "*.tgz" + access: public + provenance: true +``` + + + + + + + +## Inputs + +### Workflow Call Inputs + +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | ---------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- | +| **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` | +| | See . | | | | +| **`build-artifact-id`** | Build artifact ID from CI to download before publishing. | **false** | **string** | - | +| | Contains built package or tarball from a previous job. | | | | +| **`package-tarball`** | Path/pattern to pre-built tarball to publish (e.g., `*.tgz`). | **false** | **string** | - | +| | Use when publishing a specific tarball instead of from source. | | | | +| **`access`** | Package access level: `public` or `restricted`. | **false** | **string** | - | +| | Leave empty to use package.json default. | | | | +| **`docs`** | Documentation generation parameters. | **false** | **string** | - | +| | Set to empty string or `false` to disable. | | | | +| | Set to `true` for default command (`docs`). | | | | +| | Accepts JSON object with `command`, `output`, and `artifact` properties. | | | | +| **`registry`** | Registry configuration for package publishing. | **false** | **string** | `npm` | +| | Supported values: `npm`, `github`, URL, or JSON object with `url` and `scope`. | | | | +| **`publish-command`** | Command to run for publishing the package. | **false** | **string** | `publish` | +| | Defaults to `publish` which runs `npm publish` or equivalent. | | | | +| **`tag`** | npm distribution tag for the published package. | **false** | **string** | `latest` | +| | Common values: `latest`, `next`, `canary`. | | | | +| **`dry-run`** | Whether to perform a dry run (no actual publish). | **false** | **boolean** | `false` | +| **`provenance`** | Whether to generate provenance attestation for the published package. | **false** | **boolean** | `true` | +| | Requires npm 9.5.0+ and appropriate permissions. | | | | +| **`working-directory`** | Working directory where the package is located. | **false** | **string** | `.` | + + + + + + + +## Secrets + +| **Secret** | **Description** | **Required** | +| -------------------- | ---------------------------------------------------------------------------------- | ------------ | +| **`registry-token`** | Authentication token for the registry. | **true** | +| | For npm: Use an npm access token with publish permissions. | | +| | For GitHub Packages: Use `GITHUB_TOKEN` or a PAT with `packages:write` permission. | | + + + + + +## Outputs + +| **Output** | **Description** | +| ---------------------- | ----------------------------------------------- | +| **`version`** | The version of the published package. | +| **`package-name`** | The name of the published package. | +| **`docs-artifact-id`** | ID of the documentation artifact (if uploaded). | + + + + + +## Examples + +### Basic Release to npm + +```yaml +name: Release to npm + +on: + release: + types: [published] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} +``` + +### Release to GitHub Packages + +```yaml +name: Release to GitHub Packages + +on: + release: + types: [published] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.GITHUB_TOKEN }} + with: + registry: github + provenance: false +``` + +### Release with Documentation + +```yaml +name: Release with Documentation + +on: + release: + types: [published] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} + with: + docs: | + { + "command": "build:docs", + "output": "docs-dist", + "artifact": true + } +``` + +### Prerelease with Next Tag + +```yaml +name: Pre-release + +on: + release: + types: [prereleased] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} + with: + tag: next +``` + +### Custom Registry + +```yaml +name: Release to Custom Registry + +on: + release: + types: [published] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.CUSTOM_REGISTRY_TOKEN }} + with: + registry: | + { + "url": "https://my-registry.example.com", + "scope": "@myorg" + } + provenance: false +``` + +### Dry Run for Testing + +```yaml +name: Test Release + +on: + workflow_dispatch: + +permissions: {} + +jobs: + test-release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} + with: + dry-run: true +``` + +### Monorepo Package Release + +```yaml +name: Release Package + +on: + release: + types: [published] + +permissions: {} + +jobs: + release: + uses: hoverkraft-tech/ci-github-nodejs/.github/workflows/release.yml@main + permissions: + contents: read + packages: write + id-token: write + secrets: + registry-token: ${{ secrets.NPM_TOKEN }} + with: + working-directory: packages/my-package +``` + + + + + +## 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 © 2025 hoverkraft-tech + +For more details, see the [license](http://choosealicense.com/licenses/mit/). + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7cfe115 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,513 @@ +# Workflow to release Node.js packages: +# +# - Generate documentation (optional) +# - Publish to registry (npm, GitHub Packages) + +name: Node.js Release + +on: + workflow_call: + inputs: + runs-on: + description: | + JSON array of runner(s) to use. + See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job. + type: string + default: '["ubuntu-latest"]' + required: false + build-artifact-id: + description: | + Build artifact ID from a previous CI job to download before publishing. + This artifact typically contains the built package or tarball. + If not provided, the workflow will publish from source. + type: string + required: false + default: "" + package-tarball: + description: | + Path to a pre-built package tarball to publish (e.g., `my-package-1.0.0.tgz`). + Use this when publishing a specific tarball file instead of running npm publish from source. + Supports glob patterns to match tarball files. + type: string + required: false + default: "" + access: + description: | + Package access level for npm publish. + - `public` — Publicly accessible package + - `restricted` — Scoped package with restricted access + Leave empty to use package.json default. + type: string + required: false + default: "" + docs: + description: | + Documentation generation parameters. + Set to empty string or `false` to disable documentation generation. + Set to `true` or a string to enable documentation generation with the default command `docs`. + Accepts a JSON object for advanced options: + + - `command`: Command to run for documentation generation (default: `docs`). + - `output`: Output directory for documentation (default: `docs`). + - `artifact`: Whether to upload documentation as an artifact (default: `false`). + + Example: + ```json + { + "command": "build:docs", + "output": "docs-dist", + "artifact": true + } + ``` + type: string + required: false + default: "" + registry: + description: | + Registry configuration for package publishing. + Accepts a string for the registry URL or a JSON object for advanced options. + + Supported registries: + - `npm` or `https://registry.npmjs.org` — npm public registry (default) + - `github` or `https://npm.pkg.github.com` — GitHub Packages + + JSON object format: + - `url`: Registry URL + - `scope`: Package scope (e.g., `@myorg`). Defaults to repository owner for GitHub Packages. + + Example: + ```json + { + "url": "https://npm.pkg.github.com", + "scope": "@myorg" + } + ``` + type: string + required: false + default: "npm" + publish-command: + description: | + Command to run for publishing the package. + Defaults to `publish` which runs `npm publish` or equivalent for the detected package manager. + Can be customized for monorepo setups or specific publish requirements. + + Examples: + - `publish` — Default npm/pnpm/yarn publish + - `release` — Custom publish script in package.json + - `publish --access public` — Publish with specific npm flags + type: string + required: false + default: "publish" + tag: + description: | + npm distribution tag for the published package. + Common values: + - `latest` — Default tag for stable releases + - `next` — Pre-release or beta versions + - `canary` — Canary/nightly builds + + See https://docs.npmjs.com/adding-dist-tags-to-packages. + type: string + required: false + default: "latest" + dry-run: + description: | + Whether to perform a dry run (no actual publish). + Useful for testing the release workflow without publishing. + type: boolean + required: false + default: false + provenance: + description: | + Whether to generate provenance attestation for the published package. + Requires npm 9.5.0+ and appropriate permissions. + See https://docs.npmjs.com/generating-provenance-statements. + type: boolean + required: false + default: true + working-directory: + description: "Working directory where the package is located." + type: string + required: false + default: "." + secrets: + registry-token: + description: | + Authentication token for the registry. + For npm: Use an npm access token with publish permissions. + For GitHub Packages: Use `GITHUB_TOKEN` or a PAT with `packages:write` permission. + required: true + outputs: + version: + description: "The version of the published package." + value: ${{ jobs.release.outputs.version }} + package-name: + description: "The name of the published package." + value: ${{ jobs.release.outputs.package-name }} + docs-artifact-id: + description: "ID of the documentation artifact (if uploaded)." + value: ${{ jobs.release.outputs.docs-artifact-id }} + +permissions: {} + +jobs: + prepare: + name: 📦 Prepare configuration + runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} + permissions: {} + outputs: + registry-url: ${{ steps.parse-registry.outputs.registry-url }} + registry-scope: ${{ steps.parse-registry.outputs.registry-scope }} + docs-command: ${{ steps.parse-docs.outputs.command }} + docs-output: ${{ steps.parse-docs.outputs.output }} + docs-artifact: ${{ steps.parse-docs.outputs.artifact }} + steps: + - id: parse-registry + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + REGISTRY_INPUT: ${{ inputs.registry }} + REPO_OWNER: ${{ github.repository_owner }} + with: + script: | + const registryInput = process.env.REGISTRY_INPUT.trim(); + const repoOwner = process.env.REPO_OWNER; + + let registryUrl = 'https://registry.npmjs.org'; + let registryScope = ''; + + // Handle shorthand values + if (registryInput === 'npm' || registryInput === '') { + registryUrl = 'https://registry.npmjs.org'; + } else if (registryInput === 'github') { + registryUrl = 'https://npm.pkg.github.com'; + registryScope = `@${repoOwner}`; + } else if (registryInput.startsWith('https://') || registryInput.startsWith('http://')) { + registryUrl = registryInput; + // Auto-detect GitHub Packages scope + if (registryInput.includes('npm.pkg.github.com')) { + registryScope = `@${repoOwner}`; + } + } else if (registryInput.startsWith('{')) { + // Parse JSON object + try { + const config = JSON.parse(registryInput); + registryUrl = config.url || 'https://registry.npmjs.org'; + registryScope = config.scope || ''; + // Auto-detect GitHub Packages scope if not specified + if (registryUrl.includes('npm.pkg.github.com') && !registryScope) { + registryScope = `@${repoOwner}`; + } + } catch (error) { + return core.setFailed(`Failed to parse registry input as JSON: ${error.message}`); + } + } else { + return core.setFailed(`Invalid registry input: ${registryInput}`); + } + + core.info(`Registry URL: ${registryUrl}`); + core.info(`Registry scope: ${registryScope || '(none)'}`); + + core.setOutput('registry-url', registryUrl); + core.setOutput('registry-scope', registryScope); + + - id: parse-docs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + DOCS_INPUT: ${{ inputs.docs }} + with: + script: | + const docsInput = process.env.DOCS_INPUT.trim(); + + if (!docsInput || docsInput === 'false') { + core.info('Documentation generation disabled'); + return; + } + + let command = 'docs'; + let output = 'docs'; + let artifact = false; + + if (docsInput === 'true') { + // Use defaults + } else if (docsInput.startsWith('{')) { + try { + const config = JSON.parse(docsInput); + command = config.command || 'docs'; + output = config.output || 'docs'; + artifact = config.artifact === true; + } catch (error) { + return core.setFailed(`Failed to parse docs input as JSON: ${error.message}`); + } + } else { + // Treat as custom command + command = docsInput; + } + + core.info(`Docs command: ${command}`); + core.info(`Docs output: ${output}`); + core.info(`Docs artifact: ${artifact}`); + + core.setOutput('command', command); + core.setOutput('output', output); + core.setOutput('artifact', artifact ? 'true' : ''); + + release: + name: 🚀 Release + runs-on: ${{ inputs.runs-on && fromJson(inputs.runs-on) || 'ubuntu-latest' }} + needs: + - prepare + permissions: + contents: read + packages: write + id-token: write # Required for provenance + outputs: + version: ${{ steps.package-info.outputs.version }} + package-name: ${{ steps.package-info.outputs.name }} + docs-artifact-id: ${{ steps.upload-docs.outputs.artifact-id }} + steps: + - uses: hoverkraft-tech/ci-github-common/actions/checkout@5ac504609f6ef35c5ac94bd8199063aa32104721 # 0.31.3 + + - id: local-workflow-actions + uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@5ac504609f6ef35c5ac94bd8199063aa32104721 # 0.31.3 + with: + actions-path: actions + + - id: setup-node + uses: ./self-workflow/actions/setup-node + with: + working-directory: ${{ inputs.working-directory }} + registry-url: ${{ needs.prepare.outputs.registry-url }} + + - name: Download build artifacts + if: inputs.build-artifact-id != '' + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: ${{ inputs.build-artifact-id }} + path: ${{ github.workspace }} + + - id: package-info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + script: | + const fs = require('node:fs'); + const path = require('node:path'); + + let workingDirectory = process.env.WORKING_DIRECTORY || '.'; + if (!path.isAbsolute(workingDirectory)) { + workingDirectory = path.join(process.env.GITHUB_WORKSPACE, workingDirectory); + } + + const packageJsonPath = path.join(workingDirectory, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + return core.setFailed(`package.json not found at ${packageJsonPath}`); + } + + let packageJson; + try { + packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + } catch (error) { + return core.setFailed(`Failed to parse package.json: ${error.message}`); + } + + const name = packageJson.name; + const version = packageJson.version; + + if (!name) { + return core.setFailed('Package name is not defined in package.json'); + } + if (!version) { + return core.setFailed('Package version is not defined in package.json'); + } + + core.info(`Package: ${name}@${version}`); + core.setOutput('name', name); + core.setOutput('version', version); + + - name: Generate documentation + if: needs.prepare.outputs.docs-command != '' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + DOCS_COMMAND: ${{ needs.prepare.outputs.docs-command }} + RUN_SCRIPT_COMMAND: ${{ steps.setup-node.outputs.run-script-command }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + script: | + const fs = require('node:fs'); + const path = require('node:path'); + + let workingDirectory = process.env.WORKING_DIRECTORY || '.'; + if (!path.isAbsolute(workingDirectory)) { + workingDirectory = path.join(process.env.GITHUB_WORKSPACE, workingDirectory); + } + + if (!fs.existsSync(workingDirectory)) { + return core.setFailed(`Working directory does not exist: ${workingDirectory}`); + } + + const docsCommand = process.env.DOCS_COMMAND; + const runScriptCommand = process.env.RUN_SCRIPT_COMMAND; + + core.info(`Running documentation command: ${docsCommand}`); + + try { + const exitCode = await exec.exec(runScriptCommand, [docsCommand], { + cwd: workingDirectory, + ignoreReturnCode: true + }); + + if (exitCode !== 0) { + return core.setFailed(`Documentation generation failed with exit code ${exitCode}`); + } + } catch (error) { + return core.setFailed(`Documentation generation failed: ${error.message}`); + } + + - id: upload-docs + if: needs.prepare.outputs.docs-artifact == 'true' + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: documentation-${{ github.run_id }} + path: ${{ inputs.working-directory }}/${{ needs.prepare.outputs.docs-output }} + if-no-files-found: error + + - name: Publish package + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + NODE_AUTH_TOKEN: ${{ secrets.registry-token }} + PUBLISH_COMMAND: ${{ inputs.publish-command }} + PACKAGE_TARBALL: ${{ inputs.package-tarball }} + ACCESS: ${{ inputs.access }} + TAG: ${{ inputs.tag }} + DRY_RUN: ${{ inputs.dry-run }} + PROVENANCE: ${{ inputs.provenance }} + RUN_SCRIPT_COMMAND: ${{ steps.setup-node.outputs.run-script-command }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + REGISTRY_URL: ${{ needs.prepare.outputs.registry-url }} + with: + script: | + const fs = require('node:fs'); + const path = require('node:path'); + + let workingDirectory = process.env.WORKING_DIRECTORY || '.'; + if (!path.isAbsolute(workingDirectory)) { + workingDirectory = path.join(process.env.GITHUB_WORKSPACE, workingDirectory); + } + + if (!fs.existsSync(workingDirectory)) { + return core.setFailed(`Working directory does not exist: ${workingDirectory}`); + } + + const publishCommand = process.env.PUBLISH_COMMAND; + const packageTarball = process.env.PACKAGE_TARBALL; + const access = process.env.ACCESS; + const tag = process.env.TAG; + const dryRun = process.env.DRY_RUN === 'true'; + const provenance = process.env.PROVENANCE === 'true'; + const runScriptCommand = process.env.RUN_SCRIPT_COMMAND; + const registryUrl = process.env.REGISTRY_URL; + + // Simple glob matching function + function matchGlob(pattern, filename) { + const regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + return new RegExp(`^${regexPattern}$`).test(filename); + } + + // Determine what to publish + let publishTarget = null; + + if (packageTarball) { + // Publishing a specific tarball file + let searchDir = workingDirectory; + let filePattern = packageTarball; + + if (path.isAbsolute(packageTarball)) { + searchDir = path.dirname(packageTarball); + filePattern = path.basename(packageTarball); + } else if (packageTarball.includes('/')) { + searchDir = path.join(workingDirectory, path.dirname(packageTarball)); + filePattern = path.basename(packageTarball); + } + + if (!fs.existsSync(searchDir)) { + return core.setFailed(`Search directory does not exist: ${searchDir}`); + } + + // Find matching files + const allFiles = fs.readdirSync(searchDir); + const matches = allFiles + .filter(file => matchGlob(filePattern, file)) + .map(file => path.join(searchDir, file)) + .filter(file => fs.statSync(file).isFile()); + + if (matches.length === 0) { + return core.setFailed(`No tarball found matching pattern: ${packageTarball} in ${searchDir}`); + } + + if (matches.length > 1) { + core.warning(`Multiple tarballs found: ${matches.join(', ')}. Using first match.`); + } + + publishTarget = matches[0]; + core.info(`Publishing tarball: ${publishTarget}`); + } + + // Build publish arguments + const args = [publishCommand]; + + // Add tarball target if specified + if (publishTarget) { + args.push(publishTarget); + } + + // Add access flag + if (access) { + args.push('--access', access); + } + + // Add tag + if (tag) { + args.push('--tag', tag); + } + + // Add dry run flag + if (dryRun) { + args.push('--dry-run'); + core.info('Dry run mode enabled - package will not be published'); + } + + // Add provenance flag (npm only, requires npm 9.5.0+) + if (provenance && registryUrl.includes('registry.npmjs.org')) { + args.push('--provenance'); + core.info('Provenance attestation enabled'); + } + + core.info(`Publishing with command: ${runScriptCommand} ${args.join(' ')}`); + + try { + const exitCode = await exec.exec(runScriptCommand, args, { + cwd: workingDirectory, + ignoreReturnCode: true + }); + + if (exitCode !== 0) { + return core.setFailed(`Package publish failed with exit code ${exitCode}`); + } + + core.info('Package published successfully!'); + } catch (error) { + return core.setFailed(`Package publish failed: ${error.message}`); + } + + # jscpd:ignore-start + - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@5ac504609f6ef35c5ac94bd8199063aa32104721 # 0.31.3 + if: always() && steps.local-workflow-actions.outputs.repository + with: + actions-path: actions + repository: ${{ steps.local-workflow-actions.outputs.repository }} + ref: ${{ steps.local-workflow-actions.outputs.ref }} + # jscpd:ignore-end diff --git a/README.md b/README.md index 6b773f8..e8bcf26 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ _Actions focused on discovering and preparing the Node.js environment._ - [Continuous Integration](.github/workflows/continuous-integration.md) — documentation for the reusable Node.js CI workflow. +### Release + +- [Release](.github/workflows/release.md) — documentation for the reusable Node.js release workflow with support for multiple registries and documentation generation. + ## Contributing Contributions are welcome! Please review the [contributing guidelines](CONTRIBUTING.md) before opening a PR.