Files
shared-actions/.github/workflows/node-pr.yaml
T

260 lines
10 KiB
YAML

name: Node Pull Request
# Checks for a Node, Vue or Astro project: install without touching the lockfile,
# then run lint, typecheck, test and build. A script that does not exist is skipped.
on:
workflow_call:
inputs:
node-version:
description: "Node.js version to use"
required: false
type: string
default: "22"
working-directory:
description: "Directory that holds package.json"
required: false
type: string
default: "."
workspace:
description: "The project is a pnpm workspace. working-directory then holds pnpm-workspace.yaml and pnpm-lock.yaml, and needs no package.json. Dependencies install once there, and scripts run in each of packages."
required: false
type: boolean
default: false
packages:
description: "With workspace, comma-separated workspace package directories, relative to working-directory."
required: false
type: string
default: ""
scripts:
description: "Comma-separated package.json scripts to run, in order. A script that does not exist is skipped."
required: false
type: string
default: "lint,typecheck,test,build"
pnpm-version:
description: "pnpm version to use, for a repo whose package.json has no packageManager field. Empty uses the packageManager field, or the newest pnpm."
required: false
type: string
default: ""
require-tests:
description: "Fail when package.json has no test script"
required: false
type: boolean
default: false
lfs:
description: "Enable Git LFS checkout (for repos with large files)"
required: false
type: boolean
default: false
npm-registry-scope:
description: "Scope served by a private npm registry, for example '@fortawesome'. Used only when npm-registry-url and the npm-registry-token secret are also set."
required: false
type: string
default: ""
npm-registry-url:
description: "URL of the private npm registry, for example 'https://npm.fontawesome.com/'. Used only when npm-registry-scope and the npm-registry-token secret are also set."
required: false
type: string
default: ""
runner:
description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest."
required: false
type: string
default: ""
secrets:
npm-registry-token:
description: "Auth token for the private npm registry"
required: false
concurrency:
group: node-pr-${{ github.repository }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}-${{ inputs.working-directory }}
cancel-in-progress: true
jobs:
pre:
name: Detect duplicate run
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 5
permissions:
pull-requests: read
outputs:
skip: ${{ steps.dup.outputs.skip }}
steps:
# A push to a branch with an open PR also fires pull_request; run only the PR one.
- name: Skip push run when a PR is open
id: dup
uses: actions/github-script@v9
with:
script: |
let skip = false;
if (context.eventName === 'push') {
try {
const { data } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}`,
state: 'open',
});
skip = data.length > 0;
} catch (e) {
core.warning(`PR lookup failed, running checks: ${e.message}`);
}
}
core.setOutput('skip', String(skip));
if (skip) core.notice('Open PR found for this branch; checks run on the pull_request event.');
checks:
name: Checks (${{ inputs.working-directory }})
needs: pre
if: needs.pre.outputs.skip != 'true'
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && (inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest') || format('"{0}"', inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: ${{ inputs.lfs }}
- name: Run workflow prepare script
shell: bash
run: |
if [ -f "./workflow-prepare.sh" ]; then
chmod +x ./workflow-prepare.sh
./workflow-prepare.sh
fi
- name: Detect the package manager
id: detect
working-directory: ${{ inputs.working-directory }}
env:
WORKSPACE: ${{ inputs.workspace }}
PACKAGES: ${{ inputs.packages }}
run: |
if [ "$WORKSPACE" = true ]; then
if [ ! -f pnpm-workspace.yaml ] || [ ! -f pnpm-lock.yaml ]; then
echo "::error::workspace needs pnpm-workspace.yaml and pnpm-lock.yaml in ${{ inputs.working-directory }}."
exit 1
fi
count=0
IFS=',' read -r -a packages <<<"$PACKAGES"
for package in "${packages[@]}"; do
package=$(echo "$package" | xargs)
[ -n "$package" ] || continue
if [ ! -f "$package/package.json" ]; then
echo "::error::packages lists $package, but ${{ inputs.working-directory }}/$package/package.json does not exist."
exit 1
fi
count=$((count + 1))
done
if [ "$count" -eq 0 ]; then
echo "::error::workspace needs packages: the packages to check."
exit 1
fi
echo "pm=pnpm" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ ! -f package.json ]; then
echo "::error::No package.json in ${{ inputs.working-directory }}."
exit 1
fi
pm=npm
if [ -f pnpm-lock.yaml ]; then pm=pnpm
elif [ -f yarn.lock ]; then pm=yarn
fi
echo "pm=$pm" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ inputs.node-version }}
# The npmrc lives in RUNNER_TEMP, not the checkout, so no git command can pick the token up.
- name: Configure the private npm registry
env:
NPM_SCOPE: ${{ inputs.npm-registry-scope }}
NPM_URL: ${{ inputs.npm-registry-url }}
NPM_TOKEN: ${{ secrets.npm-registry-token }}
run: |
[ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ] || exit 0
echo "::add-mask::$NPM_TOKEN"
url="${NPM_URL%/}/"
npmrc="$RUNNER_TEMP/private-registry.npmrc"
(umask 077 && printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN" > "$npmrc")
echo "NPM_CONFIG_USERCONFIG=$npmrc" >> "$GITHUB_ENV"
echo "Using a private npm registry for $NPM_SCOPE."
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
env:
PM: ${{ steps.detect.outputs.pm }}
PNPM_VERSION: ${{ inputs.pnpm-version }}
run: |
# Install without updating the lockfile, so a lockfile that does not match package.json fails.
case "$PM" in
pnpm | yarn)
command -v corepack >/dev/null || npm install --global corepack
corepack enable "$PM"
;;
esac
if [ "$PM" = pnpm ] && [ -n "$PNPM_VERSION" ]; then
corepack prepare "pnpm@$PNPM_VERSION" --activate
fi
case "$PM" in
pnpm) pnpm install --frozen-lockfile ;;
yarn) yarn install --immutable ;;
npm)
if [ -f package-lock.json ]; then npm ci; else npm install; fi
;;
esac
- name: Run scripts
working-directory: ${{ inputs.working-directory }}
env:
PM: ${{ steps.detect.outputs.pm }}
SCRIPTS: ${{ inputs.scripts }}
REQUIRE_TESTS: ${{ inputs.require-tests }}
WORKSPACE: ${{ inputs.workspace }}
PACKAGES: ${{ inputs.packages }}
run: |
# A single project is the one package ".". A workspace is each listed package.
packages=(.)
if [ "$WORKSPACE" = true ]; then
IFS=',' read -r -a packages <<<"$PACKAGES"
fi
if [ "$REQUIRE_TESTS" = true ]; then
for package in "${packages[@]}"; do
package=$(echo "$package" | xargs)
[ -n "$package" ] || continue
if ! jq -e '.scripts.test' "$package/package.json" >/dev/null; then
echo "::error::$package/package.json has no test script."
exit 1
fi
done
fi
ran=()
IFS=',' read -r -a scripts <<<"$SCRIPTS"
for script in "${scripts[@]}"; do
script=$(echo "$script" | xargs)
[ -n "$script" ] || continue
for package in "${packages[@]}"; do
package=$(echo "$package" | xargs)
[ -n "$package" ] || continue
where=""
[ "$package" = . ] || where=" ($package)"
if jq -e --arg s "$script" '.scripts[$s]' "$package/package.json" >/dev/null; then
echo "::group::$PM run $script$where"
(cd "$package" && "$PM" run "$script")
echo "::endgroup::"
ran+=("$script$where")
else
echo "::notice::No '$script' script$where, skipping."
fi
done
done
if [ "${#ran[@]}" -eq 0 ]; then
echo "::warning::None of the scripts ($SCRIPTS) exist in package.json, so nothing was checked."
fi
echo "Ran: ${ran[*]:-none} (${PM})" >> "$GITHUB_STEP_SUMMARY"