diff --git a/.github/workflows/node-pr.yaml b/.github/workflows/node-pr.yaml new file mode 100644 index 0000000..d0e196a --- /dev/null +++ b/.github/workflows/node-pr.yaml @@ -0,0 +1,166 @@ +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: "." + 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" + 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 + 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: "" + +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@v6 + 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 }} + run: | + 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@v6 + with: + node-version: ${{ inputs.node-version }} + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + env: + PM: ${{ steps.detect.outputs.pm }} + 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 + 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 }} + run: | + if [ "$REQUIRE_TESTS" = true ] && ! jq -e '.scripts.test' package.json >/dev/null; then + echo "::error::package.json has no test script." + exit 1 + fi + ran=() + IFS=',' read -r -a scripts <<<"$SCRIPTS" + for script in "${scripts[@]}"; do + script=$(echo "$script" | xargs) + [ -n "$script" ] || continue + if jq -e --arg s "$script" '.scripts[$s]' package.json >/dev/null; then + echo "::group::$PM run $script" + "$PM" run "$script" + echo "::endgroup::" + ran+=("$script") + else + echo "::notice::No '$script' script, skipping." + fi + 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" diff --git a/README.md b/README.md index d51779e..4c2bdcf 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,45 @@ jobs: secrets: inherit ``` +### `node-pr.yaml` + +Pull request checks for a Node, Vue or Astro project. It installs without changing the lockfile, then runs `lint`, `typecheck`, `test` and `build`. A script that does not exist is skipped. A push to a branch with an open PR skips the duplicate run, and stale runs on the same branch are cancelled. Call it once per project directory. + +```yaml +name: Pull Request + +on: + pull_request: + branches: + - main + push: + branches: + - "**" + - "!main" + +permissions: + contents: read + pull-requests: read + +jobs: + frontend: + uses: lukaszraczylo/shared-actions/.github/workflows/node-pr.yaml@main + with: + working-directory: web +``` + +**Inputs:** +| Input | Default | Description | +|-------|---------|-------------| +| `node-version` | `22` | Node.js version | +| `working-directory` | `.` | Directory that holds `package.json` | +| `scripts` | `lint,typecheck,test,build` | Scripts to run in order. A missing script is skipped. | +| `require-tests` | `false` | Fail when there is no `test` script | +| `lfs` | `false` | Git LFS checkout | +| `runner` | | Runner label or runner group, as in the other workflows | + +pnpm is used when `pnpm-lock.yaml` exists, yarn for `yarn.lock`, otherwise npm. pnpm and yarn run through Corepack, so set `packageManager` in `package.json`. + ### `go-autoupdate.yaml` Automatic dependency updates for Go. See `renovate-autoupdate.yaml` for the Renovate-based replacement that also covers npm, pnpm and yarn.