From 07ae5bd415f23fdabcc55da547f16de9aaaaa8df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:25:35 +0000 Subject: [PATCH 1/6] Initial plan From e773dc8a3cc3ab50a5411d0a48e34f7f9c9ea134 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:42:26 +0000 Subject: [PATCH 2/6] feat(workflows): add release.yml reusable workflow and documentation Co-authored-by: neilime <314088+neilime@users.noreply.github.com> --- .github/workflows/release.md | 384 +++++++++++++++++++++++++ .github/workflows/release.yml | 517 ++++++++++++++++++++++++++++++++++ README.md | 4 + 3 files changed, 905 insertions(+) create mode 100644 .github/workflows/release.md create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.md b/.github/workflows/release.md new file mode 100644 index 0000000..82b423f --- /dev/null +++ b/.github/workflows/release.md @@ -0,0 +1,384 @@ + + +# 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: + +- Building the package before publishing +- Generating documentation (optional) +- Publishing to various registries (npm, GitHub Packages) +- Provenance attestation for npm packages +- Distribution tags for versioning + +### Permissions + +- **`contents`**: `read` +- **`id-token`**: `write` (required for provenance) +- **`packages`**: `write` + + + + + +## Usage + +```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: + # 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. + registry-token: ${{ secrets.NPM_TOKEN }} + + # Secrets to be used during the build step (optional). + build-secrets: "" + with: + # JSON array of runner(s) to use. + # Default: `["ubuntu-latest"]` + runs-on: '["ubuntu-latest"]' + + # Build parameters. Set to empty string to disable. + # Default: `build` + build: build + + # Documentation generation parameters. + # Set to empty string or `false` to disable. + docs: "" + + # Registry configuration. + # Use `npm`, `github`, or a URL/JSON object. + # Default: `npm` + registry: npm + + # Command to run for publishing. + # Default: `publish` + publish-command: publish + + # npm distribution tag. + # Default: `latest` + tag: latest + + # Whether to perform a dry run. + # Default: `false` + dry-run: false + + # Whether to generate provenance attestation. + # Default: `true` + provenance: true + + # Working directory where the package is located. + # Default: `.` + working-directory: . +``` + + + + + + + +## Inputs + +### Workflow Call Inputs + +| **Input** | **Description** | **Required** | **Type** | **Default** | +| ----------------------- | ---------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- | +| **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` | +| | See . | | | | +| **`build`** | Build parameters. Must be a string or a JSON object. | **false** | **string** | `build` | +| | Set to empty string to disable build step. | | | | +| | For string, provide a list of commands to run during the build step, one per line. | | | | +| | For JSON object, provide `commands` array and optional `env` object. | | | | +| **`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. | | +| **`build-secrets`** | Secrets to be used during the build step. | **false** | +| | Must be a multi-line env formatted string. | | + + + + + +## 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 + build: | + { + "commands": ["build"], + "env": { + "NODE_ENV": "production" + } + } +``` + + + + + +## 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..12b2273 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,517 @@ +# Workflow to release Node.js packages: +# +# - Build (optional) +# - 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: + description: | + Build parameters. Must be a string or a JSON object. + Set to empty string to disable build step. + For string, provide a list of commands to run during the build step, one per line. + For JSON object, provide the following properties: + + - `commands`: Array of commands to run during the build step. + - `env`: Object of environment variables to set during the build step. + + Example: + ```json + { + "commands": [ + "build", + "generate-artifacts" + ], + "env": { + "NODE_ENV": "production" + } + } + ``` + type: string + required: false + default: "build" + 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 + build-secrets: + description: | + Secrets to be used during the build step. + Must be a multi-line env formatted string. + Example: + ```txt + SECRET_EXAMPLE=$\{{ secrets.SECRET_EXAMPLE }} + ``` + required: false + 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 }} + build-commands: ${{ steps.parse-build.outputs.commands }} + build-env: ${{ steps.parse-build.outputs.env }} + 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-build + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + BUILD_INPUT: ${{ inputs.build }} + with: + script: | + const buildInput = process.env.BUILD_INPUT.trim(); + + if (!buildInput) { + core.info('Build step disabled'); + return; + } + + let commands = []; + let env = {}; + + // Build commands is a list of command(s), one per line + const buildCommandsIsList = !buildInput.startsWith('{') && !buildInput.startsWith('['); + if (buildCommandsIsList) { + commands = buildInput.split('\n').map(command => command.trim()).filter(Boolean); + } else { + let build; + try { + build = JSON.parse(buildInput); + } catch (error) { + return core.setFailed(`Failed to parse build input as JSON: ${error.message}`); + } + + // Build commands is a JSON array of commands + if (Array.isArray(build)) { + commands = build; + } + // Build commands is a JSON object + else { + commands = build.commands ?? ['build']; + env = build.env ?? {}; + } + } + + if (commands.some(command => typeof command !== 'string')) { + return core.setFailed('Build commands array must only contain strings'); + } + + const sanitizedCommands = commands.map(command => command.trim()).filter(Boolean); + if (!sanitizedCommands.length) { + core.info('No build commands specified'); + return; + } + + core.setOutput('commands', sanitizedCommands.join('\n')); + core.setOutput('env', JSON.stringify(env)); + + - 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 }} + + - 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: Build + if: needs.prepare.outputs.build-commands != '' + uses: ./self-workflow/actions/build + with: + working-directory: ${{ inputs.working-directory }} + build-commands: ${{ needs.prepare.outputs.build-commands }} + build-env: ${{ needs.prepare.outputs.build-env || '{}' }} + build-secrets: ${{ secrets.build-secrets }} + + - 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 }} + 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 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; + + // Build publish arguments + const args = [publishCommand]; + + // 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. From e711e6714304fc1f5c69e702e1d8cd2471f91f69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:14:45 +0000 Subject: [PATCH 3/6] feat(workflows): add build artifact ID output support to release workflow Co-authored-by: neilime <314088+neilime@users.noreply.github.com> --- .github/workflows/release.md | 13 +++++---- .github/workflows/release.yml | 54 ++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.md b/.github/workflows/release.md index 82b423f..412f97a 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -123,7 +123,7 @@ jobs: | **`build`** | Build parameters. Must be a string or a JSON object. | **false** | **string** | `build` | | | Set to empty string to disable build step. | | | | | | For string, provide a list of commands to run during the build step, one per line. | | | | -| | For JSON object, provide `commands` array and optional `env` object. | | | | +| | For JSON object, provide `commands` array, optional `env` object, and `artifact`. | | | | | **`docs`** | Documentation generation parameters. | **false** | **string** | - | | | Set to empty string or `false` to disable. | | | | | | Set to `true` for default command (`docs`). | | | | @@ -161,11 +161,12 @@ jobs: ## 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). | +| **Output** | **Description** | +| ----------------------- | ----------------------------------------------- | +| **`version`** | The version of the published package. | +| **`package-name`** | The name of the published package. | +| **`build-artifact-id`** | ID of the build artifact (if uploaded). | +| **`docs-artifact-id`** | ID of the documentation artifact (if uploaded). | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12b2273..e1da3f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,7 @@ on: - `commands`: Array of commands to run during the build step. - `env`: Object of environment variables to set during the build step. + - `artifact`: String or array of strings specifying paths to artifacts to upload after the build. Example: ```json @@ -35,7 +36,11 @@ on: ], "env": { "NODE_ENV": "production" - } + }, + "artifact": [ + "dist/", + "packages/package-a/build/" + ] } ``` type: string @@ -154,6 +159,9 @@ on: package-name: description: "The name of the published package." value: ${{ jobs.release.outputs.package-name }} + build-artifact-id: + description: "ID of the build artifact (if uploaded)." + value: ${{ jobs.release.outputs.build-artifact-id }} docs-artifact-id: description: "ID of the documentation artifact (if uploaded)." value: ${{ jobs.release.outputs.docs-artifact-id }} @@ -170,6 +178,7 @@ jobs: registry-scope: ${{ steps.parse-registry.outputs.registry-scope }} build-commands: ${{ steps.parse-build.outputs.commands }} build-env: ${{ steps.parse-build.outputs.env }} + build-artifact: ${{ steps.parse-build.outputs.artifact }} docs-command: ${{ steps.parse-docs.outputs.command }} docs-output: ${{ steps.parse-docs.outputs.output }} docs-artifact: ${{ steps.parse-docs.outputs.artifact }} @@ -226,9 +235,13 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: BUILD_INPUT: ${{ inputs.build }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} with: script: | + const path = require('node:path'); + const buildInput = process.env.BUILD_INPUT.trim(); + const workingDirectory = process.env.WORKING_DIRECTORY || '.'; if (!buildInput) { core.info('Build step disabled'); @@ -258,6 +271,42 @@ jobs: else { commands = build.commands ?? ['build']; env = build.env ?? {}; + + if (build.artifact) { + let buildArtifact = build.artifact; + + if (typeof buildArtifact === 'string' || Array.isArray(buildArtifact)) { + buildArtifact = { + paths: buildArtifact + }; + } + + if (typeof buildArtifact.paths === 'string') { + buildArtifact.paths = buildArtifact.paths.split('\n'); + } else if (!Array.isArray(buildArtifact.paths)) { + return core.setFailed('Build artifact paths must be a string or an array of strings'); + } + + buildArtifact.paths = buildArtifact.paths + .map(artifact => artifact.trim()) + .filter(Boolean) + .map(artifact => { + // FIXME: Workaround to preserve full path to artifact + const fullpath = artifact.startsWith('/') ? artifact : path.join(workingDirectory, artifact); + + // Add a wildcard to the first folder of the path + return fullpath.replace(/\/([^/]+)/, '/*$1'); + }).join('\n'); + + if (!buildArtifact.paths) { + return core.setFailed('No valid build artifact paths found'); + } + + // Generate a unique name for the artifact + buildArtifact.name = `${process.env.GITHUB_JOB}-build-${process.env.GITHUB_RUN_ID}-${Math.random().toString(36).substring(2, 8)}`; + + core.setOutput('artifact', JSON.stringify(buildArtifact)); + } } } @@ -327,6 +376,7 @@ jobs: outputs: version: ${{ steps.package-info.outputs.version }} package-name: ${{ steps.package-info.outputs.name }} + build-artifact-id: ${{ steps.build.outputs.artifact-id }} docs-artifact-id: ${{ steps.upload-docs.outputs.artifact-id }} steps: - uses: hoverkraft-tech/ci-github-common/actions/checkout@5ac504609f6ef35c5ac94bd8199063aa32104721 # 0.31.3 @@ -383,6 +433,7 @@ jobs: core.setOutput('version', version); - name: Build + id: build if: needs.prepare.outputs.build-commands != '' uses: ./self-workflow/actions/build with: @@ -390,6 +441,7 @@ jobs: build-commands: ${{ needs.prepare.outputs.build-commands }} build-env: ${{ needs.prepare.outputs.build-env || '{}' }} build-secrets: ${{ secrets.build-secrets }} + build-artifact: ${{ needs.prepare.outputs.build-artifact }} - name: Generate documentation if: needs.prepare.outputs.docs-command != '' From c498eccff3f37c387e0e42584a4b1aba5162014c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:03:51 +0000 Subject: [PATCH 4/6] refactor(release): remove build functionality, build is done in CI workflow Co-authored-by: neilime <314088+neilime@users.noreply.github.com> --- .github/workflows/release.md | 32 ++------ .github/workflows/release.yml | 150 ---------------------------------- 2 files changed, 5 insertions(+), 177 deletions(-) diff --git a/.github/workflows/release.md b/.github/workflows/release.md index 412f97a..7c04a51 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -25,7 +25,6 @@ Workflow to release Node.js packages with support for: -- Building the package before publishing - Generating documentation (optional) - Publishing to various registries (npm, GitHub Packages) - Provenance attestation for npm packages @@ -64,18 +63,11 @@ jobs: # For npm: Use an npm access token with publish permissions. # For GitHub Packages: Use `GITHUB_TOKEN` or a PAT with `packages:write` permission. registry-token: ${{ secrets.NPM_TOKEN }} - - # Secrets to be used during the build step (optional). - build-secrets: "" with: # JSON array of runner(s) to use. # Default: `["ubuntu-latest"]` runs-on: '["ubuntu-latest"]' - # Build parameters. Set to empty string to disable. - # Default: `build` - build: build - # Documentation generation parameters. # Set to empty string or `false` to disable. docs: "" @@ -120,10 +112,6 @@ jobs: | ----------------------- | ---------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- | | **`runs-on`** | JSON array of runner(s) to use. | **false** | **string** | `["ubuntu-latest"]` | | | See . | | | | -| **`build`** | Build parameters. Must be a string or a JSON object. | **false** | **string** | `build` | -| | Set to empty string to disable build step. | | | | -| | For string, provide a list of commands to run during the build step, one per line. | | | | -| | For JSON object, provide `commands` array, optional `env` object, and `artifact`. | | | | | **`docs`** | Documentation generation parameters. | **false** | **string** | - | | | Set to empty string or `false` to disable. | | | | | | Set to `true` for default command (`docs`). | | | | @@ -152,8 +140,6 @@ jobs: | **`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. | | -| **`build-secrets`** | Secrets to be used during the build step. | **false** | -| | Must be a multi-line env formatted string. | | @@ -161,12 +147,11 @@ jobs: ## Outputs -| **Output** | **Description** | -| ----------------------- | ----------------------------------------------- | -| **`version`** | The version of the published package. | -| **`package-name`** | The name of the published package. | -| **`build-artifact-id`** | ID of the build artifact (if uploaded). | -| **`docs-artifact-id`** | ID of the documentation artifact (if uploaded). | +| **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). | @@ -348,13 +333,6 @@ jobs: registry-token: ${{ secrets.NPM_TOKEN }} with: working-directory: packages/my-package - build: | - { - "commands": ["build"], - "env": { - "NODE_ENV": "production" - } - } ``` diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1da3f7..5ff4d86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,5 @@ # Workflow to release Node.js packages: # -# - Build (optional) # - Generate documentation (optional) # - Publish to registry (npm, GitHub Packages) @@ -16,36 +15,6 @@ on: type: string default: '["ubuntu-latest"]' required: false - build: - description: | - Build parameters. Must be a string or a JSON object. - Set to empty string to disable build step. - For string, provide a list of commands to run during the build step, one per line. - For JSON object, provide the following properties: - - - `commands`: Array of commands to run during the build step. - - `env`: Object of environment variables to set during the build step. - - `artifact`: String or array of strings specifying paths to artifacts to upload after the build. - - Example: - ```json - { - "commands": [ - "build", - "generate-artifacts" - ], - "env": { - "NODE_ENV": "production" - }, - "artifact": [ - "dist/", - "packages/package-a/build/" - ] - } - ``` - type: string - required: false - default: "build" docs: description: | Documentation generation parameters. @@ -143,15 +112,6 @@ on: 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 - build-secrets: - description: | - Secrets to be used during the build step. - Must be a multi-line env formatted string. - Example: - ```txt - SECRET_EXAMPLE=$\{{ secrets.SECRET_EXAMPLE }} - ``` - required: false outputs: version: description: "The version of the published package." @@ -159,9 +119,6 @@ on: package-name: description: "The name of the published package." value: ${{ jobs.release.outputs.package-name }} - build-artifact-id: - description: "ID of the build artifact (if uploaded)." - value: ${{ jobs.release.outputs.build-artifact-id }} docs-artifact-id: description: "ID of the documentation artifact (if uploaded)." value: ${{ jobs.release.outputs.docs-artifact-id }} @@ -176,9 +133,6 @@ jobs: outputs: registry-url: ${{ steps.parse-registry.outputs.registry-url }} registry-scope: ${{ steps.parse-registry.outputs.registry-scope }} - build-commands: ${{ steps.parse-build.outputs.commands }} - build-env: ${{ steps.parse-build.outputs.env }} - build-artifact: ${{ steps.parse-build.outputs.artifact }} docs-command: ${{ steps.parse-docs.outputs.command }} docs-output: ${{ steps.parse-docs.outputs.output }} docs-artifact: ${{ steps.parse-docs.outputs.artifact }} @@ -231,98 +185,6 @@ jobs: core.setOutput('registry-url', registryUrl); core.setOutput('registry-scope', registryScope); - - id: parse-build - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - env: - BUILD_INPUT: ${{ inputs.build }} - WORKING_DIRECTORY: ${{ inputs.working-directory }} - with: - script: | - const path = require('node:path'); - - const buildInput = process.env.BUILD_INPUT.trim(); - const workingDirectory = process.env.WORKING_DIRECTORY || '.'; - - if (!buildInput) { - core.info('Build step disabled'); - return; - } - - let commands = []; - let env = {}; - - // Build commands is a list of command(s), one per line - const buildCommandsIsList = !buildInput.startsWith('{') && !buildInput.startsWith('['); - if (buildCommandsIsList) { - commands = buildInput.split('\n').map(command => command.trim()).filter(Boolean); - } else { - let build; - try { - build = JSON.parse(buildInput); - } catch (error) { - return core.setFailed(`Failed to parse build input as JSON: ${error.message}`); - } - - // Build commands is a JSON array of commands - if (Array.isArray(build)) { - commands = build; - } - // Build commands is a JSON object - else { - commands = build.commands ?? ['build']; - env = build.env ?? {}; - - if (build.artifact) { - let buildArtifact = build.artifact; - - if (typeof buildArtifact === 'string' || Array.isArray(buildArtifact)) { - buildArtifact = { - paths: buildArtifact - }; - } - - if (typeof buildArtifact.paths === 'string') { - buildArtifact.paths = buildArtifact.paths.split('\n'); - } else if (!Array.isArray(buildArtifact.paths)) { - return core.setFailed('Build artifact paths must be a string or an array of strings'); - } - - buildArtifact.paths = buildArtifact.paths - .map(artifact => artifact.trim()) - .filter(Boolean) - .map(artifact => { - // FIXME: Workaround to preserve full path to artifact - const fullpath = artifact.startsWith('/') ? artifact : path.join(workingDirectory, artifact); - - // Add a wildcard to the first folder of the path - return fullpath.replace(/\/([^/]+)/, '/*$1'); - }).join('\n'); - - if (!buildArtifact.paths) { - return core.setFailed('No valid build artifact paths found'); - } - - // Generate a unique name for the artifact - buildArtifact.name = `${process.env.GITHUB_JOB}-build-${process.env.GITHUB_RUN_ID}-${Math.random().toString(36).substring(2, 8)}`; - - core.setOutput('artifact', JSON.stringify(buildArtifact)); - } - } - } - - if (commands.some(command => typeof command !== 'string')) { - return core.setFailed('Build commands array must only contain strings'); - } - - const sanitizedCommands = commands.map(command => command.trim()).filter(Boolean); - if (!sanitizedCommands.length) { - core.info('No build commands specified'); - return; - } - - core.setOutput('commands', sanitizedCommands.join('\n')); - core.setOutput('env', JSON.stringify(env)); - - id: parse-docs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: @@ -376,7 +238,6 @@ jobs: outputs: version: ${{ steps.package-info.outputs.version }} package-name: ${{ steps.package-info.outputs.name }} - build-artifact-id: ${{ steps.build.outputs.artifact-id }} docs-artifact-id: ${{ steps.upload-docs.outputs.artifact-id }} steps: - uses: hoverkraft-tech/ci-github-common/actions/checkout@5ac504609f6ef35c5ac94bd8199063aa32104721 # 0.31.3 @@ -432,17 +293,6 @@ jobs: core.setOutput('name', name); core.setOutput('version', version); - - name: Build - id: build - if: needs.prepare.outputs.build-commands != '' - uses: ./self-workflow/actions/build - with: - working-directory: ${{ inputs.working-directory }} - build-commands: ${{ needs.prepare.outputs.build-commands }} - build-env: ${{ needs.prepare.outputs.build-env || '{}' }} - build-secrets: ${{ secrets.build-secrets }} - build-artifact: ${{ needs.prepare.outputs.build-artifact }} - - name: Generate documentation if: needs.prepare.outputs.docs-command != '' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 From 56b60f9fd2f3b8af2d25e6e1a107d6cb4bdc07aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:35:37 +0000 Subject: [PATCH 5/6] feat(release): add support for build artifacts, tarballs, and access control Co-authored-by: neilime <314088+neilime@users.noreply.github.com> --- .github/workflows/release.md | 103 ++++++++++++++++++++++++---------- .github/workflows/release.yml | 70 +++++++++++++++++++++++ 2 files changed, 144 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release.md b/.github/workflows/release.md index 7c04a51..0357987 100644 --- a/.github/workflows/release.md +++ b/.github/workflows/release.md @@ -25,10 +25,13 @@ Workflow to release Node.js packages with support for: -- Generating documentation (optional) - 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 @@ -42,6 +45,8 @@ Workflow to release Node.js packages with support for: ## Usage +### Basic Release from Source + ```yaml name: Release @@ -59,43 +64,77 @@ jobs: packages: write id-token: write secrets: - # 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. + 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: - # JSON array of runner(s) to use. - # Default: `["ubuntu-latest"]` - runs-on: '["ubuntu-latest"]' + # Download build artifacts from CI job + build-artifact-id: ${{ needs.ci.outputs.build-artifact-id }} + access: public +``` - # Documentation generation parameters. - # Set to empty string or `false` to disable. - docs: "" +### Release Pre-built Tarball - # Registry configuration. - # Use `npm`, `github`, or a URL/JSON object. - # Default: `npm` - registry: npm +```yaml +name: Release - # Command to run for publishing. - # Default: `publish` - publish-command: publish +on: + push: + tags: ["*"] - # npm distribution tag. - # Default: `latest` - tag: latest +permissions: {} - # Whether to perform a dry run. - # Default: `false` - dry-run: false +jobs: + ci: + uses: ./.github/workflows/__shared-ci.yml + permissions: + contents: read + id-token: write + secrets: inherit - # Whether to generate provenance attestation. - # Default: `true` + 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 - - # Working directory where the package is located. - # Default: `.` - working-directory: . ``` @@ -112,6 +151,12 @@ jobs: | ----------------------- | ---------------------------------------------------------------------------------- | ------------ | ----------- | ------------------- | | **`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`). | | | | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ff4d86..9cb4ea2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,31 @@ on: 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. @@ -253,6 +278,13 @@ jobs: 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: + artifact-ids: ${{ inputs.build-artifact-id }} + path: / + - id: package-info uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: @@ -345,6 +377,8 @@ jobs: 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 }} @@ -355,6 +389,7 @@ jobs: script: | const fs = require('node:fs'); const path = require('node:path'); + const { glob } = require('glob'); let workingDirectory = process.env.WORKING_DIRECTORY || '.'; if (!path.isAbsolute(workingDirectory)) { @@ -366,15 +401,50 @@ jobs: } 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; + // Determine what to publish + let publishTarget = null; + + if (packageTarball) { + // Publishing a specific tarball file + const tarballPattern = path.isAbsolute(packageTarball) + ? packageTarball + : path.join(workingDirectory, packageTarball); + + const matches = await glob(tarballPattern, { nodir: true }); + + if (matches.length === 0) { + return core.setFailed(`No tarball found matching pattern: ${packageTarball}`); + } + + 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); From 1bba5314717d4d4333f027baf7f324be65eedae6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:40:04 +0000 Subject: [PATCH 6/6] fix(release): use fs instead of glob for tarball matching, fix artifact download path Co-authored-by: neilime <314088+neilime@users.noreply.github.com> --- .github/workflows/release.yml | 48 ++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cb4ea2..7cfe115 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -282,8 +282,8 @@ jobs: if: inputs.build-artifact-id != '' uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - artifact-ids: ${{ inputs.build-artifact-id }} - path: / + name: ${{ inputs.build-artifact-id }} + path: ${{ github.workspace }} - id: package-info uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 @@ -389,7 +389,6 @@ jobs: script: | const fs = require('node:fs'); const path = require('node:path'); - const { glob } = require('glob'); let workingDirectory = process.env.WORKING_DIRECTORY || '.'; if (!path.isAbsolute(workingDirectory)) { @@ -409,25 +408,50 @@ jobs: 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 - const tarballPattern = path.isAbsolute(packageTarball) - ? packageTarball - : path.join(workingDirectory, packageTarball); - - const matches = await glob(tarballPattern, { nodir: true }); - + 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}`); + 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}`); }