diff --git a/.github/actions/node-prepare/action.yml b/.github/actions/node-prepare/action.yml new file mode 100644 index 00000000..2a2fdb2e --- /dev/null +++ b/.github/actions/node-prepare/action.yml @@ -0,0 +1,122 @@ +name: Prepare a Runner For Node.js Tests or Build +description: Setup node, configure registry auth, and install dependencies. Detects package manager from lockfile (pnpm or yarn). +author: Telicent + +# Requires permissions - +# - contents: read +# - id-token: write + +inputs: + node-version: + required: false + description: The node version to install. Falls back to .nvmrc if not provided. + frozen-lockfile: + required: false + description: Whether to use a frozen/immutable lockfile for install. + default: 'true' + network-concurrency: + required: false + description: Network concurrency for yarn install (ignored for pnpm). + default: '1' + verbose: + required: false + description: Whether to output verbose install logs. + default: 'false' + additional-args: + required: false + description: Additional arguments for the install command. + sanity-check: + required: false + description: Whether to output a sanity check of Node/NPM configuration. + default: 'false' + create-npmrc: + required: false + description: Whether to create the .npmrc file before installing dependencies. + default: 'true' + + # Secrets + font-awesome-key: + required: false + description: NPM Font Awesome token. + package-pat: + required: false + description: NPM package feed token. + +outputs: + package-manager: + description: Detected package manager (pnpm or yarn). + value: ${{ steps.detect-pm.outputs.pm }} + +runs: + using: composite + steps: + - name: Detect package manager + id: detect-pm + run: | + if [ -f pnpm-lock.yaml ]; then + echo "pm=pnpm" >> $GITHUB_OUTPUT + else + echo "pm=yarn" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Setup pnpm via corepack + if: steps.detect-pm.outputs.pm == 'pnpm' + run: | + corepack enable + PM_VERSION=$(node -p "require('./package.json').packageManager?.split('@')[1] || '11.4.0'") + corepack prepare "pnpm@${PM_VERSION}" --activate + shell: bash + + - name: Setup node (pnpm) + if: steps.detect-pm.outputs.pm == 'pnpm' + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version || '' }} + node-version-file: ${{ inputs.node-version && '' || '.nvmrc' }} + + - name: Get pnpm store path + if: steps.detect-pm.outputs.pm == 'pnpm' + id: pnpm-store + run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT + shell: bash + + - name: Cache pnpm store + if: steps.detect-pm.outputs.pm == 'pnpm' + uses: actions/cache@v5 + with: + path: ${{ steps.pnpm-store.outputs.dir }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Setup node (yarn) + if: steps.detect-pm.outputs.pm == 'yarn' + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version || '' }} + node-version-file: ${{ inputs.node-version && '' || '.nvmrc' }} + cache: 'yarn' + + - name: Create .npmrc file + if: ${{ inputs.create-npmrc == 'true' }} + run: ${{ github.action_path }}/create-npmrc-file.sh + shell: bash + env: + FONT_AWESOME_KEY: ${{ inputs.font-awesome-key }} + PACKAGE_PAT: ${{ inputs.package-pat }} + + - name: Sanity check registries/proxy (no secrets) + if: ${{ inputs.sanity-check == 'true' }} + run: ${{ github.action_path }}/sanity-check.sh + shell: bash + env: + DETECTED_PM: ${{ steps.detect-pm.outputs.pm }} + + - name: Install dependencies + run: ${{ github.action_path }}/install-dependencies.sh + shell: bash + env: + DETECTED_PM: ${{ steps.detect-pm.outputs.pm }} + FROZEN_LOCKFILE: ${{ inputs.frozen-lockfile }} + NETWORK_CONCURRENCY: ${{ inputs.network-concurrency }} + VERBOSE: ${{ inputs.verbose }} + ADDITIONAL_ARGS: ${{ inputs.additional-args }} diff --git a/.github/actions/node-prepare/create-npmrc-file.sh b/.github/actions/node-prepare/create-npmrc-file.sh new file mode 100755 index 00000000..9ecdd77a --- /dev/null +++ b/.github/actions/node-prepare/create-npmrc-file.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +cat > .npmrc << EOF +registry=https://registry.npmjs.org/ +@telicent-io:registry=https://npm.pkg.github.com/ +//npm.pkg.github.com/:_authToken=$PACKAGE_PAT +@awesome.me:registry=https://npm.fontawesome.com/ +@fortawesome:registry=https://npm.fontawesome.com/ +//npm.fontawesome.com/:_authToken=$FONT_AWESOME_KEY +always-auth=true +EOF diff --git a/.github/actions/node-prepare/install-dependencies.sh b/.github/actions/node-prepare/install-dependencies.sh new file mode 100755 index 00000000..2f9e5cba --- /dev/null +++ b/.github/actions/node-prepare/install-dependencies.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +export NODE_OPTIONS="${NODE_OPTIONS:-} --dns-result-order=ipv4first" + +if [ "$DETECTED_PM" = "pnpm" ]; then + ARGS="" + if [ "$FROZEN_LOCKFILE" = "true" ]; then + ARGS="$ARGS --frozen-lockfile" + fi + LOCAL_MACHINE=false pnpm install $ARGS +else + ARGS="" + if [ "$FROZEN_LOCKFILE" = "true" ]; then + ARGS="$ARGS --frozen-lockfile" + fi + if [ -n "$NETWORK_CONCURRENCY" ] && [ "$NETWORK_CONCURRENCY" != "" ]; then + ARGS="$ARGS --network-concurrency $NETWORK_CONCURRENCY" + fi + if [ "$VERBOSE" = "true" ]; then + ARGS="$ARGS --verbose" + fi + if [ -n "$ADDITIONAL_ARGS" ]; then + ARGS="$ARGS $ADDITIONAL_ARGS" + fi + LOCAL_MACHINE=false yarn install $ARGS +fi diff --git a/.github/actions/node-prepare/sanity-check.sh b/.github/actions/node-prepare/sanity-check.sh new file mode 100755 index 00000000..dfb4d4ef --- /dev/null +++ b/.github/actions/node-prepare/sanity-check.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +echo "node: $(node -v) | npm: $(npm -v)" +if [ "$DETECTED_PM" = "pnpm" ]; then + echo "pnpm: $(pnpm -v)" +else + echo "yarn: $(yarn -v)" +fi +echo "npm default registry: $(npm config get registry)" +echo "@telicent-io registry: $(npm config get @telicent-io:registry)" +echo "@fortawesome registry: $(npm config get @fortawesome:registry)" + +for v in HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy no_proxy; do + if [ -n "${!v:-}" ]; then echo "$v is set"; fi +done + +for h in registry.npmjs.org npm.pkg.github.com npm.fontawesome.com; do + echo "== $h ==" + getent hosts "$h" || true + curl -sSIL --max-time 10 "https://$h/" | head -n 1 || echo "curl FAIL: $h" +done + +if [ "$DETECTED_PM" = "pnpm" ] && [ -f pnpm-lock.yaml ]; then + echo "Lockfile: pnpm-lock.yaml" +elif [ -f yarn.lock ]; then + echo "Hosts from yarn.lock:" + awk '/^ resolved /{print $2}' yarn.lock | sed -E 's/^"//; s/"$//' | awk -F/ '{print $3}' | sort -u +fi diff --git a/.github/workflows/javascript-test-feature-branch.yml b/.github/workflows/javascript-test-feature-branch.yml index e167f3ba..be7d9d2b 100644 --- a/.github/workflows/javascript-test-feature-branch.yml +++ b/.github/workflows/javascript-test-feature-branch.yml @@ -7,11 +7,21 @@ on: default: "." type: string description: directory to run all run commands from - yarn-test-command: + test-command: required: false default: "test" type: string - description: Yarn test command e.g. "test" or "test:ci" + description: Test command e.g. "test" or "test:ci" + yarn-test-command: + required: false + default: "" + type: string + description: "DEPRECATED: use test-command instead" + package-manager: + required: false + default: "auto" + type: string + description: "Package manager to use: auto (detect from lockfile), yarn, or pnpm" jobs: quality: @@ -27,44 +37,110 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6.0.2 + + - name: Detect package manager + id: detect-pm + working-directory: ${{ inputs.working-directory }} + run: | + if [ "${{ inputs.package-manager }}" != "auto" ]; then + echo "pm=${{ inputs.package-manager }}" >> $GITHUB_OUTPUT + elif [ -f pnpm-lock.yaml ]; then + echo "pm=pnpm" >> $GITHUB_OUTPUT + else + echo "pm=yarn" >> $GITHUB_OUTPUT + fi + + - name: Resolve test command + id: test-cmd + run: | + if [ -n "${{ inputs.yarn-test-command }}" ] && [ "${{ inputs.yarn-test-command }}" != "test" ]; then + echo "cmd=${{ inputs.yarn-test-command }}" >> $GITHUB_OUTPUT + else + echo "cmd=${{ inputs.test-command }}" >> $GITHUB_OUTPUT + fi + - name: configure-node uses: actions/setup-node@v6.4.0 with: node-version: 20 + - name: Get node version id: node run: | echo "version=$(node -v)" >> $GITHUB_OUTPUT + + # ── pnpm path ── + - name: Setup pnpm via corepack + if: steps.detect-pm.outputs.pm == 'pnpm' + run: | + corepack enable + PM_VERSION=$(node -p "require('./package.json').packageManager?.split('@')[1] || '11.4.0'") + corepack prepare "pnpm@${PM_VERSION}" --activate + + - name: Get pnpm store path + if: steps.detect-pm.outputs.pm == 'pnpm' + id: pnpm-store + run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Cache pnpm store + if: steps.detect-pm.outputs.pm == 'pnpm' + uses: actions/cache@v5.0.5 + with: + path: ${{ steps.pnpm-store.outputs.dir }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ steps.node.outputs.version }} + + - name: Configure registry auth (pnpm) + if: steps.detect-pm.outputs.pm == 'pnpm' + run: | + npm config set "@telicent-oss:registry" "https://www.npmjs.com/" + npm config set "//www.npmjs.com//:_authToken" ${{ secrets.NPM_REGISTRY_TOKEN }} + + - name: Install dependencies (pnpm) + if: steps.detect-pm.outputs.pm == 'pnpm' + run: pnpm install --frozen-lockfile + + # ── yarn path ── - name: Get yarn cache directory path + if: steps.detect-pm.outputs.pm == 'yarn' id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - name: Cache node modules + + - name: Cache node modules (yarn) + if: steps.detect-pm.outputs.pm == 'yarn' uses: actions/cache@v5.0.5 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}-${{ steps.node.outputs.version }} + - name: create .yarnrc file + if: steps.detect-pm.outputs.pm == 'yarn' run: | npm config set "@telicent-oss:registry" "https://www.npmjs.com/" - npm config set "//www.npmjs.com//:_authToken" ${{ secrets.NPM_REGISTRY_TOKEN }} - - name: Install dependencies + npm config set "//www.npmjs.com//:_authToken" ${{ secrets.NPM_REGISTRY_TOKEN }} + + - name: Install dependencies (yarn) + if: steps.detect-pm.outputs.pm == 'yarn' run: yarn install - - name: yarn tsc --noEmit if needed + + # ── shared steps ── + - name: tsc --noEmit if needed run: | + PM="${{ steps.detect-pm.outputs.pm }}" if [ -f tsconfig.json ]; then echo "TypeScript config found." TSC_IGNORE=$(node -p "require('./package.json').ci?.['shared-workflows/.github/workflows/javascript-test-feature-branch.yml']?.['WARNING: TSC CHECK NO ERROR'] || ''") if [ "$TSC_IGNORE" != "" ]; then echo "WARNING flag found in package.json. Running tsc --noEmit (errors will be ignored)..." - yarn tsc --noEmit || echo "tsc failed but error ignored due to warning flag." + $PM tsc --noEmit || echo "tsc failed but error ignored due to warning flag." else echo "Running tsc --noEmit..." - yarn tsc --noEmit + $PM tsc --noEmit fi else echo "No TypeScript config found. Skipping tsc..." fi - - name: run integrated tests - run: yarn ${{ inputs.yarn-test-command }} + + - name: Run integrated tests + run: ${{ steps.detect-pm.outputs.pm }} ${{ steps.test-cmd.outputs.cmd }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}