From 6ad4d6a1380d52c9eb0c3d60295af704c85b2e15 Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Wed, 15 Oct 2025 17:51:44 +0200 Subject: [PATCH] chore: use github-scripts for cross-platform compatibility Signed-off-by: Emilien Escalle --- .../__test-action-get-package-manager.yml | 8 +- .github/workflows/continuous-integration.md | 8 +- .github/workflows/continuous-integration.yml | 6 +- AGENTS.md | 32 ++++ README.md | 86 +++++++++- actions/dependencies-cache/action.yml | 107 ++++++++---- actions/get-package-manager/action.yml | 156 +++++++++++------- actions/has-installed-dependencies/action.yml | 27 ++- actions/setup-node/action.yml | 113 ++++++++++--- 9 files changed, 407 insertions(+), 136 deletions(-) create mode 100644 AGENTS.md diff --git a/.github/workflows/__test-action-get-package-manager.yml b/.github/workflows/__test-action-get-package-manager.yml index 609ba43..17458fc 100644 --- a/.github/workflows/__test-action-get-package-manager.yml +++ b/.github/workflows/__test-action-get-package-manager.yml @@ -16,28 +16,28 @@ jobs: - working-directory: tests/npm package-manager: npm lock-file: package-lock.json - cache-dependency-path: "**/package-lock.json" + cache-dependency-path: "tests/npm/**/package-lock.json" install-command: npm ci run-script-command: npm run - working-directory: tests/pnpm package-manager: pnpm lock-file: pnpm-lock.yaml - cache-dependency-path: "**/pnpm-lock.yaml" + cache-dependency-path: "tests/pnpm/**/pnpm-lock.yaml" install-command: pnpm install --frozen-lockfile run-script-command: pnpm - working-directory: tests/pnpm-package-manager package-manager: pnpm lock-file: pnpm-lock.yaml - cache-dependency-path: "**/pnpm-lock.yaml" + cache-dependency-path: "tests/pnpm-package-manager/**/pnpm-lock.yaml" install-command: pnpm install --frozen-lockfile run-script-command: pnpm - working-directory: tests/yarn package-manager: yarn lock-file: yarn.lock - cache-dependency-path: "**/yarn.lock" + cache-dependency-path: "tests/yarn/**/yarn.lock" install-command: yarn install --frozen-lockfile run-script-command: yarn steps: diff --git a/.github/workflows/continuous-integration.md b/.github/workflows/continuous-integration.md index c58cfee..d6d4178 100644 --- a/.github/workflows/continuous-integration.md +++ b/.github/workflows/continuous-integration.md @@ -1,6 +1,6 @@ -# GitHub Reusable Workflow: NodeJS Continuous Integration +# GitHub Reusable Workflow: Node.js Continuous Integration
NodeJS Continuous Integration @@ -23,7 +23,7 @@ ## Overview -Workflow to performs continuous integration steps agains a NodeJs project: +Workflow to performs continuous integration steps agains a Node.js project: - CodeQL analysis - Linting @@ -99,13 +99,13 @@ jobs: | **Input** | **Description** | **Required** | **Type** | **Default** | | ----------------------- | ----------------------------------------------------------------------------------------- | ------------ | ----------- | ------------ | -| **`build`** | Build parameters. Must be a string or a json object. | **false** | **string** | `build` | +| **`build`** | Build parameters. Must be a string or a JSON object. | **false** | **string** | `build` | | **`checks`** | Optional flag to enable check steps. | **false** | **boolean** | `true` | | **`lint`** | Optional flag to enable linting. | **false** | **boolean** | `true` | | **`code-ql`** | Code QL analysis language. See . | **false** | **string** | `typescript` | | **`dependency-review`** | Enable dependency review scan. See . | **false** | **boolean** | `true` | | **`test`** | Optional flag to enable test. | **false** | **boolean** | `true` | -| **`coverage`** | Specifify code coverage reporter. Supported values: 'codecov'. | **false** | **string** | `codecov` | +| **`coverage`** | Specifify code coverage reporter. Supported values: 'Codecov'. | **false** | **string** | `codecov` | | **`working-directory`** | Working directory where the dependencies are installed. | **false** | **string** | `.` | diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4810d18..538d308 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,11 +1,11 @@ -# Workflow to performs continuous integration steps agains a NodeJs project: +# Workflow to performs continuous integration steps agains a Node.js project: # # - CodeQL analysis # - Linting # - Build # - Test -name: NodeJS Continuous Integration +name: Node.js Continuous Integration on: workflow_call: @@ -41,7 +41,7 @@ on: required: false default: true coverage: - description: "Specifify code coverage reporter. Supported values: 'codecov'." + description: "Specifify code coverage reporter. Supported values: `codecov`." type: string required: false default: "codecov" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1883ca0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# AGENTS.md — agent instructions and operational contract + +This file is written for automated coding agents (for example: Copilot coding agents). It exists to provide a concise operational contract and guardrails for agents working in this repository. It is not the canonical source for design or style rules. Those live in the developer documentation linked below. + +## Organization-wide guidelines (required) + +- Follow the prioritized shared instructions in [hoverkraft-tech/.github/AGENTS.md](https://github.com/hoverkraft-tech/.github/blob/main/AGENTS.md) before working in this repository. + +## Quick Start + +This project is a collection of **opinionated GitHub Actions** and **reusable workflows** tailored for Node.js continuous integration pipelines. For comprehensive documentation, see the main [README.md](README.md). + +### Key Sections to Reference + +- **[Overview](README.md#overview)** – Project purpose and scope +- **[Actions](README.md#actions)** – Catalog of available actions by category +- **[Reusable Workflows](README.md#reusable-workflows)** – Orchestration workflows for Node.js CI +- **[Development Workflow](README.md#development-workflow)** – Commands and conventions for local development +- **[Contributing](README.md#contributing)** – Guidelines for contributing to the project + +## Agent-Specific Development Patterns + +### Critical Workflow Knowledge + +```bash +# Essential commands for development +make lint # Run Super Linter (dockerized) +make lint-fix # Auto-fix linting issues +gh act -W .github/workflows/__test-workflow-continuous-integration.yml # Optional: exercise reusable workflows locally +``` + +For detailed documentation on each action and workflow, refer to the individual readme files linked in the main [README.md](README.md). diff --git a/README.md b/README.md index 3caae47..0a917f0 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,88 @@ [![License](https://img.shields.io/badge/License-MIT-blue)](#license) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) -Opinionated GitHub Actions and workflows for continuous integration in Node.js context +Opinionated GitHub Actions and reusable workflows for Node.js continuous integration pipelines. --- +## Overview + +This repository centralizes the Hoverkraft toolkit for building, testing, and shipping Node.js projects on GitHub. It bundles: + +- Composite actions that detect project tooling, manage dependencies, and bootstrap runtimes. +- Reusable workflows that apply those actions to deliver consistent CI pipelines across repositories. + ## Actions -### - [Get package manager](actions/get-package-manager/README.md) +### Dependencies + +_Actions dedicated to caching and validating Node.js dependencies._ + +#### - [Dependencies cache](actions/dependencies-cache/README.md) + +#### - [Has installed dependencies](actions/has-installed-dependencies/README.md) + +### Environment setup -### - [Has installed dependencies](actions/has-installed-dependencies/README.md) +_Actions focused on discovering and preparing the Node.js environment._ -### - [Setup node](actions/setup-node/README.md) +#### - [Get package manager](actions/get-package-manager/README.md) -## Workflows +#### - [Setup node](actions/setup-node/README.md) -### - [Continuous Integration](.github/workflows/continuous-integration.md) +## Reusable Workflows + +### Continuous Integration + +- [Continuous Integration](.github/workflows/continuous-integration.md) — documentation for the reusable Node.js CI workflow. ## Contributing -👍 If you wish to contribute to this project, please read the [CONTRIBUTING.md](CONTRIBUTING.md) file, PRs are Welcome ! +Contributions are welcome! Please review the [contributing guidelines](CONTRIBUTING.md) before opening a PR. + +### Action Structure Pattern + +All actions follow a consistent layout: + +```text +actions/{category}/{action-name}/ +├── action.yml # Action definition with inputs/outputs +├── README.md # Usage documentation and examples +└── index.js / scripts # Optional Node.js helpers (when required) +``` + +### Development Standards + +#### Action Definition Standards + +1. **Consistent branding**: Use `author: hoverkraft` with `color: blue` and a meaningful `icon`. +2. **Pinned dependencies**: Reference third-party actions via exact SHAs to guarantee reproducibility. +3. **Input validation**: Validate critical inputs early within composite steps or supporting scripts. +4. **Idempotent steps**: Ensure actions can run multiple times without leaving residual state in the workspace. +5. **Multi-platform support**: Test actions in both `ubuntu-latest` and `windows-latest` runners when applicable. +6. **Cross-platform compatibility**: Uses `actions/github-script` steps for cross-platform compatibility. Avoid `run` steps. +7. **Logging**: Use structured logs with clear prefixes (`[build-image]`, `[helm-test-chart]`, …) to simplify debugging. +8. **Security**: Avoid shell interpolation with untrusted inputs; prefer parameterized commands or `set -euo pipefail` wrappers. + +#### File Conventions + +- **Tests**: Located in `tests/` with fixtures for container builds and chart-testing scenarios. +- **Workflows**: Reusable definitions live in `.github/workflows/`; internal/private workflows are prefixed with `__`. + +#### JavaScript Development Patterns + +- Encapsulate reusable logic in modules under the action directory (for example, `actions/my-action/index.js`). +- Prefer async/await with explicit error handling when interacting with the GitHub API or filesystem. +- Centralize environment variable parsing and validation to keep composite YAML lean. + +### Development Workflow + +#### Linting & Testing + +```bash +make lint # Run the dockerized Super Linter +make lint-fix # Attempt auto-fixes for lint findings +``` ## Author @@ -34,5 +97,10 @@ Opinionated GitHub Actions and workflows for continuous integration in Node.js c ## License -📝 Copyright © 2023 [Hoverkraft ](https://hoverkraft.cloud).
-This project is [MIT](LICENSE) licensed. +This project is licensed under the MIT License. + +SPDX-License-Identifier: MIT + +Copyright © 2023 [Hoverkraft](https://hoverkraft.cloud). + +For more details, see the [license](http://choosealicense.com/licenses/mit/). diff --git a/actions/dependencies-cache/action.yml b/actions/dependencies-cache/action.yml index 6566293..56d1bff 100644 --- a/actions/dependencies-cache/action.yml +++ b/actions/dependencies-cache/action.yml @@ -10,7 +10,9 @@ inputs: description: "List of dependencies for which the cache should be managed." required: true working-directory: - description: "Working directory where the dependencies are installed." + description: | + Working directory where the dependencies are installed. + Can be absolute or relative to the repository root. required: false default: "." @@ -75,35 +77,80 @@ runs: - name: ♻️ Get Jest cache dir id: jest-cache-dir-path if: fromJson(steps.has-installed-dependencies.outputs.installed-dependencies).jest == true - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - case "${{ steps.get-package-manager.outputs.package-manager }}" in - npm) - JEST_CONFIG=$(${{ steps.get-package-manager.outputs.package-manager }} exec jest -- --showConfig) - ;; - *) - JEST_CONFIG=$(${{ steps.get-package-manager.outputs.package-manager }} jest --showConfig) - ;; - esac - - if [ -z "$JEST_CONFIG" ]; then - echo "::error::Unable to get Jest config" - exit 1 - fi - - echo "::debug::Jest config: $JEST_CONFIG" - - JEST_CACHE_DIR=$(echo "$JEST_CONFIG" | grep -oP '(?<="cacheDirectory": ")[^"]+(?=")') - - if [ -z "$JEST_CACHE_DIR" ]; then - echo "::error ::Unable to get Jest cache directory from config: $JEST_CONFIG" - exit 1 - fi - - echo "::debug::Jest cache directory: $JEST_CACHE_DIR" - - echo "dir=$JEST_CACHE_DIR" >> "$GITHUB_OUTPUT" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + PACKAGE_MANAGER: ${{ steps.get-package-manager.outputs.package-manager }} + with: + # jscpd:ignore-start + 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)) { + core.setFailed(`The specified working directory does not exist: ${workingDirectory}`); + return; + } + + workingDirectory = path.resolve(workingDirectory); + core.debug(`Running in working directory: ${workingDirectory}`); + process.chdir(workingDirectory); + + const packageManager = process.env.PACKAGE_MANAGER; + if (!packageManager) { + core.setFailed('Unable to determine package manager'); + return; + } + core.debug(`Using package manager: ${packageManager}`); + + const commandArgs = packageManager === 'npm' + ? ['exec', 'jest', '--', '--showConfig'] + : ['jest', '--showConfig']; + + let execResult; + try { + execResult = await exec.getExecOutput(packageManager, commandArgs, { cwd: workingDirectory }); + } catch (error) { + core.setFailed(`Unable to get Jest config: ${error.message}`); + return; + } + + if (execResult.exitCode !== 0) { + const errorMessage = execResult.stderr?.trim() || execResult.stdout?.trim(); + core.setFailed(`Unable to get Jest config (exit code ${execResult.exitCode}): ${errorMessage}`); + return; + } + + const jestConfigRaw = execResult.stdout.trim(); + + if (!jestConfigRaw) { + core.setFailed('Unable to get Jest config'); + return; + } + + core.debug(`Jest config: ${jestConfigRaw}`); + + // Find cacheDirectory in the config with regex + const cacheDirMatch = jestConfigRaw.match(/"cacheDirectory"\s*:\s*"([^"]+)"/); + if (!cacheDirMatch || cacheDirMatch.length < 2) { + core.setFailed('Unable to find cacheDirectory in Jest config'); + return; + } + + let jestCacheDir = cacheDirMatch[1]; + if (!path.isAbsolute(jestCacheDir)) { + jestCacheDir = path.join(workingDirectory, jestCacheDir); + } + jestCacheDir = path.resolve(jestCacheDir); + + core.debug(`Jest cache directory: ${jestCacheDir}`); + core.setOutput('dir', jestCacheDir); + # jscpd:ignore-end - name: ♻️ Test cache if: steps.jest-cache-dir-path.outputs.dir diff --git a/actions/get-package-manager/action.yml b/actions/get-package-manager/action.yml index e2f2f79..e5bb902 100644 --- a/actions/get-package-manager/action.yml +++ b/actions/get-package-manager/action.yml @@ -1,5 +1,5 @@ name: "Get package manager" -description: "Action to detect the package manager used. Supports Yarn and npm" +description: "Action to detect the package manager used. Supports Yarn, pnpm, and npm" author: Hoverkraft branding: icon: package @@ -7,7 +7,9 @@ branding: inputs: working-directory: - description: "Working directory where the dependencies are installed." + description: | + Working directory where the dependencies are installed. + Can be absolute or relative to the repository root. required: false default: "." @@ -29,62 +31,94 @@ runs: using: "composite" steps: - id: get-package-manager - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - echo "::debug::::Running in working directory: $(pwd)"; - - # Check if the package manager is set in package.json - PACKAGE_MANAGER_NAME="" - if [ -f "package.json" ]; then - PACKAGE_MANAGER=$(jq -r '.packageManager//empty' package.json) - if [ -n "$PACKAGE_MANAGER" ]; then - # Extract the package manager and version from the packageManager field - PACKAGE_MANAGER_NAME=$(echo "$PACKAGE_MANAGER" | cut -d'@' -f1) - fi - fi - - echo "::debug::Package manager from package.json: $PACKAGE_MANAGER_NAME"; - - if [ -z "$PACKAGE_MANAGER_NAME" ]; then - if [ -f "yarn.lock" ]; then - PACKAGE_MANAGER_NAME="yarn" - elif [ -f "pnpm-lock.yaml" ]; then - PACKAGE_MANAGER_NAME="pnpm" - elif [ -f "package-lock.json" ]; then - PACKAGE_MANAGER_NAME="npm" - fi - - if [ -z "$PACKAGE_MANAGER_NAME" ]; then - echo "::error ::Unable to detect package manager"; - exit 1; - fi - echo "::debug::Package manager from lock files: $PACKAGE_MANAGER_NAME"; - fi - - echo "package-manager=$PACKAGE_MANAGER_NAME" >> "$GITHUB_OUTPUT"; - - case "$PACKAGE_MANAGER_NAME" in - yarn) - echo "cache-dependency-path=**/yarn.lock" >> "$GITHUB_OUTPUT"; - echo "install-command=yarn install --frozen-lockfile" >> "$GITHUB_OUTPUT"; - echo "run-script-command=yarn" >> "$GITHUB_OUTPUT"; - exit 0; - ;; - pnpm) - echo "cache-dependency-path=**/pnpm-lock.yaml" >> "$GITHUB_OUTPUT"; - echo "install-command=pnpm install --frozen-lockfile" >> "$GITHUB_OUTPUT"; - echo "run-script-command=pnpm" >> "$GITHUB_OUTPUT"; - exit 0; - ;; - npm) - echo "cache-dependency-path=**/package-lock.json" >> "$GITHUB_OUTPUT"; - echo "install-command=npm ci" >> "$GITHUB_OUTPUT"; - echo "run-script-command=npm run" >> "$GITHUB_OUTPUT"; - exit 0; - ;; - *) - echo "::error ::Package manager $PACKAGE_MANAGER_NAME is not supported"; - exit 1; - ;; - esac + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + # jscpd:ignore-start + 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)) { + core.setFailed(`The specified working directory does not exist: ${workingDirectory}`); + return; + } + + workingDirectory = path.resolve(workingDirectory); + core.debug(`Running in working directory: ${workingDirectory}`); + process.chdir(workingDirectory); + + let packageManagerName = ''; + const packageJsonPath = path.join(workingDirectory, 'package.json'); + + if (fs.existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const packageManager = packageJson?.packageManager; + + if (typeof packageManager === 'string' && packageManager.trim() !== '') { + packageManagerName = packageManager.split('@')[0]; + } + } catch (error) { + core.warning(`Failed to parse package.json: ${error.message}`); + } + } + + core.debug(`Package manager from package.json: ${packageManagerName}`); + + if (!packageManagerName) { + const lockFiles = [ + { file: 'yarn.lock', name: 'yarn' }, + { file: 'pnpm-lock.yaml', name: 'pnpm' }, + { file: 'package-lock.json', name: 'npm' }, + ]; + + const detectedLockFile = lockFiles.find(({ file }) => fs.existsSync(path.join(workingDirectory, file))); + + if (!detectedLockFile) { + core.setFailed('Unable to detect package manager'); + return; + } + + packageManagerName = detectedLockFile.name; + core.debug(`Package manager from lock files: ${packageManagerName}`); + } + + const relativeWorkingDirectory = path.relative(process.env.GITHUB_WORKSPACE, workingDirectory) || '.'; + + const packageManagerConfig = { + yarn: { + cacheDependencyPath: `${relativeWorkingDirectory}/**/yarn.lock`, + installCommand: 'yarn install --frozen-lockfile', + runScriptCommand: 'yarn', + }, + pnpm: { + cacheDependencyPath: `${relativeWorkingDirectory}/**/pnpm-lock.yaml`, + installCommand: 'pnpm install --frozen-lockfile', + runScriptCommand: 'pnpm', + }, + npm: { + cacheDependencyPath: `${relativeWorkingDirectory}/**/package-lock.json`, + installCommand: 'npm ci', + runScriptCommand: 'npm run', + }, + }; + + const managerConfig = packageManagerConfig[packageManagerName]; + + if (!managerConfig) { + core.setFailed(`Package manager ${packageManagerName} is not supported`); + return; + } + + core.setOutput('package-manager', packageManagerName); + core.setOutput('cache-dependency-path', managerConfig.cacheDependencyPath); + core.setOutput('install-command', managerConfig.installCommand); + core.setOutput('run-script-command', managerConfig.runScriptCommand); + # jscpd:ignore-end diff --git a/actions/has-installed-dependencies/action.yml b/actions/has-installed-dependencies/action.yml index 11c02dc..f8e82b9 100644 --- a/actions/has-installed-dependencies/action.yml +++ b/actions/has-installed-dependencies/action.yml @@ -10,7 +10,9 @@ inputs: description: "The dependencies to check." required: true working-directory: - description: "Working directory where the dependencies are installed." + description: | + Working directory where the dependencies are installed. + Can be absolute or relative to the repository root. required: false default: "." @@ -39,8 +41,28 @@ runs: - id: has-dependencies uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} with: + # jscpd:ignore-start 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)) { + core.setFailed(`The specified working directory does not exist: ${workingDirectory}`); + return; + } + + workingDirectory = path.resolve(workingDirectory); + core.debug(`Running in working directory: ${workingDirectory}`); + process.chdir(workingDirectory); + const dependenciesPatterns = { storybook: /@storybook\/[-a-z]+/, nx: /@nx\/[-a-z]+/, @@ -97,7 +119,7 @@ runs: const parseDependencies = async (command) => { const { stdout } = await exec.getExecOutput(command, undefined, { - cwd: `${{ inputs.working-directory }}` || undefined, + cwd: workingDirectory, }); if (stdout === '') { @@ -145,3 +167,4 @@ runs: } core.setOutput('installed-dependencies', hasDependencies); + # jscpd:ignore-end diff --git a/actions/setup-node/action.yml b/actions/setup-node/action.yml index 21706bc..11ac5b5 100644 --- a/actions/setup-node/action.yml +++ b/actions/setup-node/action.yml @@ -11,7 +11,9 @@ inputs: required: false default: "" working-directory: - description: "Working directory where the dependencies are installed." + description: | + Working directory where the dependencies are installed. + Can be absolute or relative to the repository root. required: false default: "." @@ -33,33 +35,98 @@ runs: working-directory: ${{ inputs.working-directory }} - id: get-node-version-file - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - SUPPORTED_FILES=(".nvmrc" ".node-version") + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + # jscpd:ignore-start + script: | + const path = require('node:path'); + const fs = require('node:fs'); + + let workingDirectory = process.env.WORKING_DIRECTORY || '.'; + if (!path.isAbsolute(workingDirectory)) { + workingDirectory = path.join(process.env.GITHUB_WORKSPACE, workingDirectory); + } + + if (!fs.existsSync(workingDirectory)) { + core.setFailed(`The specified working directory does not exist: ${workingDirectory}`); + return; + } + + workingDirectory = path.resolve(workingDirectory); + core.debug(`Running in working directory: ${workingDirectory}`); + process.chdir(workingDirectory); - # Check if any of the supported files exist - for file in "${SUPPORTED_FILES[@]}"; do - if [ -f "$file" ]; then - NODE_VERSION_FILE=$(realpath -s --relative-to="$GITHUB_WORKSPACE" "$(pwd)/$file") - echo "node-version-file=$NODE_VERSION_FILE" >> $GITHUB_OUTPUT - exit 0 - fi - done + const candidates = ['.nvmrc', '.node-version']; + + for (const fileName of candidates) { + const candidatePath = path.resolve(workingDirectory, fileName); + + if (!fs.existsSync(candidatePath)) { + continue; + } + + const relativePath = path.relative(process.env.GITHUB_WORKSPACE, candidatePath); + core.debug(`Found Node version file: ${relativePath}`); + core.setOutput('node-version-file', relativePath); + return; + } + + core.debug('No Node version file found in supported list'); + # jscpd:ignore-end # FIXME: workaround until will be merged: https://github.com/actions/setup-node/pull/901 - id: get-pnpm-version if: steps.get-package-manager.outputs.package-manager == 'pnpm' - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - if [ -f "package.json" ]; then - if [ -n "$(jq -r '.packageManager//empty' package.json)" ]; then - # pnpm/action-setup supports "packageManager" field in package.json - exit 0 - fi - fi - echo "pnpm-version=latest" >> "$GITHUB_OUTPUT" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + with: + script: | + const path = require('node:path'); + const fs = require('node:fs'); + + let workingDirectory = process.env.WORKING_DIRECTORY || '.'; + if (!path.isAbsolute(workingDirectory)) { + workingDirectory = path.join(process.env.GITHUB_WORKSPACE, workingDirectory); + } + + if (!fs.existsSync(workingDirectory)) { + core.setFailed(`The specified working directory does not exist: ${workingDirectory}`); + return; + } + + workingDirectory = path.resolve(workingDirectory); + core.debug(`Running in working directory: ${workingDirectory}`); + process.chdir(workingDirectory); + + let packageJsonPath = path.join(workingDirectory, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + core.debug(`package.json not found in working directory "${workingDirectory}"; defaulting pnpm version to latest`); + core.setOutput('pnpm-version', 'latest'); + return; + } + packageJsonPath = path.resolve(packageJsonPath); + + let packageJson; + try { + const fileContent = fs.readFileSync(packageJsonPath, 'utf8'); + packageJson = JSON.parse(fileContent); + } catch (error) { + core.setFailed(`Unable to read ${packageJsonPath} to determine pnpm version: ${error.message}`); + return; + } + + const packageManagerField = (packageJson?.packageManager ?? '').trim(); + + if (packageManagerField) { + core.debug('package.json defines packageManager; pnpm/action-setup will use it'); + return; + } + + core.debug('packageManager field missing; defaulting pnpm version to latest'); + core.setOutput('pnpm-version', 'latest'); - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 if: steps.get-package-manager.outputs.package-manager == 'pnpm'