Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions .github/workflows/node-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Reusable "Node.js build" workflow (LEAF).
#
# Runs a package-manager-agnostic build job, with an optional upload of the
# build output as a workflow artifact.
#
# Caller pattern:
#
# jobs:
# build:
# uses: netresearch/.github/.github/workflows/node-build.yml@main
# with:
# build-cmd: "bun run build:lib"
# upload-artifact: true
# artifact-path: "dist/"
#
# SECURITY: caller-supplied commands are routed through `env:` and run via
# `bash -c "$VAR"`, so the caller can't break out of argument position via
# quoting/metacharacters. Third-party actions are SHA-pinned, harden-runner
# is enabled, and checkout uses `persist-credentials: false`.

name: Node.js Build (reusable)

on:
workflow_call:
inputs:
runs-on:
description: "Runner label."
type: string
default: "ubuntu-latest"
timeout-minutes:
description: "Per-job timeout in minutes."
type: number
default: 15

package-manager:
description: "Package manager to use (one of: bun, npm, pnpm, yarn)."
type: string
default: "bun"
node-version:
description: "Node.js version."
type: string
default: "24"
bun-version:
description: "Bun version passed to oven-sh/setup-bun (when package-manager is bun)."
type: string
default: "latest"

install-cmd:
description: "Dependency install command."
type: string
default: "bun install --frozen-lockfile"
build-cmd:
description: "Build command."
type: string
default: "bun run build"
pre-command:
description: "Optional shell command to run after install but before the build command."
type: string
default: ""

upload-artifact:
description: "Upload the build output as a workflow artifact."
type: boolean
default: false
artifact-name:
description: "Artifact name used when `upload-artifact` is true."
type: string
default: "dist"
artifact-path:
description: "Path(s) uploaded when `upload-artifact` is true."
type: string
default: "dist/"
artifact-retention-days:
description: "Retention for the artifact (1-90)."
type: number
default: 7

# CALLER REQUIREMENTS
# ===================
# When calling this reusable workflow, the caller's job-level
# `permissions:` block MUST grant at least this full set, otherwise
# the run fails with `startup_failure` before any job executes
# (GitHub rejects the workflow at startup when the reusable declares
# a permission the caller did not grant). NOTE: setting ANY scope in a
# `permissions:` block forces every UNLISTED scope to `none`, and a job
# that omits `permissions:` inherits the repository default
# (default_workflow_permissions) — so a repo hardened to `read` will
# startup-fail here unless these scopes are granted explicitly.
#
# jobs:
# build:
# uses: netresearch/.github/.github/workflows/node-build.yml@main
# permissions:
# contents: read
permissions: {}

jobs:
node-build:
name: build
runs-on: ${{ inputs.runs-on }}
timeout-minutes: ${{ inputs.timeout-minutes }}
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Validate package-manager input
env:
PACKAGE_MANAGER: ${{ inputs.package-manager }}
run: |
case "$PACKAGE_MANAGER" in
bun|npm|pnpm|yarn) : ;;
*)
echo "::error title=Invalid package-manager::Unsupported package-manager '$PACKAGE_MANAGER'. Expected one of: bun, npm, pnpm, yarn."
exit 1
;;
esac

- name: Set up Bun
if: ${{ inputs.package-manager == 'bun' }}
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ inputs.bun-version }}

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ inputs.node-version }}

- name: Pre-command
if: ${{ inputs.pre-command != '' }}
env:
PRE_COMMAND: ${{ inputs.pre-command }}
run: bash -c "$PRE_COMMAND"

- name: Install dependencies
env:
INSTALL_CMD: ${{ inputs.install-cmd }}
run: bash -c "$INSTALL_CMD"

- name: Build
env:
BUILD_CMD: ${{ inputs.build-cmd }}
run: bash -c "$BUILD_CMD"

- name: Upload build artifact
if: ${{ inputs.upload-artifact }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.artifact-path }}
if-no-files-found: error
retention-days: ${{ inputs.artifact-retention-days }}
231 changes: 231 additions & 0 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Reusable "Node.js CI" gold-standard workflow (META).
#
# Composes the granular Node.js/TypeScript reusables into one CI entry
# point: TS lint/type/format checks, tests (+ optional coverage), build,
# dependency audit, CodeQL, secret scanning, and dependency review. Each
# job calls a single-purpose reusable so consumers get one call site.
#
# Caller pattern:
#
# name: CI
# on:
# push:
# branches: [main]
# pull_request:
# permissions: {}
# jobs:
# node-ci:
# uses: netresearch/.github/.github/workflows/node-ci.yml@main
# permissions:
# actions: read
# contents: read
# security-events: write
# pull-requests: write
# with:
# coverage-upload: true
# build-artifact-path: "dist/"
# secrets:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
#
# SECURITY: secrets are forwarded explicitly (CODECOV_TOKEN, GITLEAKS_LICENSE),
# never via `secrets: inherit`. Composed first-party reusables track `@main`
# by policy (see .github/zizmor.yml); third-party actions inside those leaves
# are SHA-pinned.

name: Node.js CI (reusable)

on:
workflow_call:
inputs:
package-manager:
description: "Package manager to use (one of: bun, npm, pnpm, yarn)."
type: string
default: "bun"
node-version:
description: "Node.js version (also the coverage-upload matrix cell)."
type: string
default: "24"
test-matrix:
description: >-
JSON array string of Node.js versions to matrix the test job over
(e.g. '["20","22"]'). When empty, a single-element matrix of
[node-version] is used.
type: string
default: ""

install-cmd:
description: "Dependency install command."
type: string
default: "bun install --frozen-lockfile"
lint-cmd:
description: "Lint command."
type: string
default: "bun run lint"
type-check-cmd:
description: "Type-check command."
type: string
default: "bun run build"
format-check-cmd:
description: "Format-check command."
type: string
default: "bunx prettier --check ."
test-cmd:
description: "Test command."
type: string
default: "bun run test:coverage"
build-cmd:
description: "Build command."
type: string
default: "bun run build:lib"
pre-command:
description: "Optional shell command to run after install but before the TS checks."
type: string
default: ""

enable-format-check:
description: "Run the Prettier format check in the TS job."
type: boolean
default: true
enable-test:
description: "Run the test job."
type: boolean
default: true
enable-build:
description: "Run the build job."
type: boolean
default: true
build-artifact-path:
description: >-
Path(s) to upload as a build artifact. When non-empty, the build
job uploads this path; when empty, no artifact is uploaded.
type: string
default: ""

coverage-upload:
description: "Upload test coverage to Codecov (in the test job)."
type: boolean
default: false
audit-level:
description: >-
Minimum severity to fail on for both the dependency audit and the
dependency-review job (low, moderate, high, critical).
type: string
default: "high"
codeql-languages:
description: "CodeQL language(s) to analyze."
type: string
default: "javascript-typescript"
secrets:
CODECOV_TOKEN:
description: "Codecov upload token. Required for private repositories when `coverage-upload` is true."
required: false
GITLEAKS_LICENSE:
description: "gitleaks-action license key (required for org repositories). Forwarded to the gitleaks job."
required: false

# CALLER REQUIREMENTS
# ===================
# This meta-reusable composes leaves that need SARIF-upload and PR-write
# scopes, so its caller must grant the UNION of every composed job's
# requirements. When calling this reusable workflow, the caller's job-level
# `permissions:` block MUST grant at least this full set, otherwise the run
# fails with `startup_failure` before any job executes (GitHub rejects the
# workflow at startup when a nested reusable declares a permission the
# caller did not grant). NOTE: setting ANY scope in a `permissions:` block
# forces every UNLISTED scope to `none`, and a job that omits `permissions:`
# inherits the repository default (default_workflow_permissions) — so a repo
# hardened to `read` will startup-fail here unless these scopes are granted
# explicitly. Secrets are forwarded explicitly (CODECOV_TOKEN); never use
# `secrets: inherit`.
#
# jobs:
# node-ci:
# uses: netresearch/.github/.github/workflows/node-ci.yml@main
# permissions:
# actions: read
# contents: read
# security-events: write
# pull-requests: write
permissions: {}

jobs:
ts:
name: TS Check
uses: netresearch/.github/.github/workflows/ts-check.yml@main
permissions:
contents: read
with:
install-cmd: ${{ inputs.install-cmd }}
lint-cmd: ${{ inputs.lint-cmd }}
type-check-cmd: ${{ inputs.type-check-cmd }}
format-check-cmd: ${{ inputs.format-check-cmd }}
enable-format-check: ${{ inputs.enable-format-check }}
pre-command: ${{ inputs.pre-command }}

test:
name: Test
if: ${{ inputs.enable-test }}
uses: netresearch/.github/.github/workflows/node-test.yml@main
permissions:
contents: read
with:
package-manager: ${{ inputs.package-manager }}
node-version: ${{ inputs.node-version }}
test-matrix: ${{ inputs.test-matrix }}
install-cmd: ${{ inputs.install-cmd }}
test-cmd: ${{ inputs.test-cmd }}
coverage-upload: ${{ inputs.coverage-upload }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

build:
name: Build
if: ${{ inputs.enable-build }}
uses: netresearch/.github/.github/workflows/node-build.yml@main
permissions:
contents: read
with:
package-manager: ${{ inputs.package-manager }}
node-version: ${{ inputs.node-version }}
install-cmd: ${{ inputs.install-cmd }}
build-cmd: ${{ inputs.build-cmd }}
upload-artifact: ${{ inputs.build-artifact-path != '' }}
artifact-path: ${{ inputs.build-artifact-path }}

node-audit:
name: Node.js Audit
uses: netresearch/.github/.github/workflows/node-audit.yml@main
permissions:
contents: read
with:
package-manager: ${{ inputs.package-manager }}
audit-level: ${{ inputs.audit-level }}

codeql:
name: CodeQL
uses: netresearch/.github/.github/workflows/codeql.yml@main
permissions:
actions: read
contents: read
security-events: write
with:
languages: ${{ inputs.codeql-languages }}

gitleaks:
name: Secret Scanning
uses: netresearch/.github/.github/workflows/gitleaks.yml@main
permissions:
contents: read
security-events: write
secrets:
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}

dependency-review:
name: Dependency Review
if: ${{ github.event_name == 'pull_request' }}
uses: netresearch/.github/.github/workflows/dependency-review.yml@main
permissions:
contents: read
pull-requests: write
with:
fail-on-severity: ${{ inputs.audit-level }}
Loading
Loading