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
35 changes: 27 additions & 8 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ jobs:
(github.event.workflow_run.conclusion == 'success'
&& github.event.workflow_run.head_branch == 'main'
&& github.event.workflow_run.event == 'push')
# GitHub-hosted (node-quickbooks uses self-hosted): timezest is PUBLIC, so
# npm attaches a provenance attestation on publish, which npm only allows
# from GitHub-hosted runners (self-hosted → HTTP 422). Post-merge trusted
# code, and only runs on real version-bump releases.
# GitHub-hosted: timezest is PUBLIC, so npm attaches a provenance
# attestation on publish, which npm only allows from GitHub-hosted runners
# (self-hosted → HTTP 422). Post-merge trusted code, and only runs on real
# version-bump releases.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -58,8 +58,9 @@ jobs:
cache-dependency-path: |
package-lock.json

# Pinned: org standard is npm 12 (matches shared-actions validate-codebase). Bump deliberately.
- name: Update npm
run: npm install -g npm@latest
run: npm install -g npm@12.0.2

- name: Install dependencies
run: npm ci
Expand All @@ -69,8 +70,26 @@ jobs:
- name: Build
run: npm run build

# `npm publish` is a no-op failure if the version already exists, so a
# push without a version bump simply doesn't release. --provenance: public
# package on a GitHub-hosted runner.
# Version gate. This is the single place a version bump is enforced:
# merges that don't bump package.json (Dependabot, docs, CI) are a clean
# no-op here rather than a failed run, and npm itself refuses to publish
# over an existing version, so main can never re-release silently.
- name: Check if version already published
id: check
shell: bash
run: |
set -euo pipefail
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "${NAME}@${VERSION}" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "::notice::${NAME}@${VERSION} already on npm — no version bump on this commit, skipping publish."
else
echo "published=false" >> "$GITHUB_OUTPUT"
echo "${NAME}@${VERSION} not yet published — will publish."
fi

# --provenance: public package on a GitHub-hosted runner.
- name: Publish to npm
if: steps.check.outputs.published == 'false'
run: npm publish --provenance
76 changes: 22 additions & 54 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,82 +18,50 @@ jobs:
# Push events (merge to main) always validate — that run is what gates
# publishing. PR events only run when the PR's head branch lives in THIS
# repo (which requires write access); PRs from forks have a different head
# repo and skip the job, so untrusted fork code never reaches the
# self-hosted runner.
# repo and skip the job. On the ephemeral GitHub-hosted runner below this
# is a POLICY choice, not a runner-safety requirement (fork runs are
# sandboxed and get no secrets) — drop the `if` to give fork PRs CI.
#
# We gate on head-repo identity rather than author_association because the
# latter downgrades private org members to "CONTRIBUTOR" in the event
# payload, which would wrongly skip our own team's PRs. head.repo is null
# for deleted forks, so the comparison safely fails closed (skips).
#
# Dependabot PRs use in-repo branches, so they satisfy the head-repo check
# and still RUN here (we want their test coverage) — but they are NOT
# exempt from the version-bump gate below, so they fail until a human turns
# the dependency update into a real, version-bumped release. That keeps
# main from drifting out of sync with what's published to npm.
# and run here like any other PR. There is deliberately no version-bump
# gate at PR time: a merge that doesn't bump package.json simply doesn't
# publish (see npm-publish.yml), so dependency-only PRs can land on their
# own and get released with the next version bump.
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
runs-on: self-hosted
# GitHub-hosted, like npm-publish.yml: the org's self-hosted runner group
# does not accept jobs from public repos (allows_public_repositories=false,
# on purpose — it keeps fork PRs off the fleet), so `self-hosted` here
# could never be scheduled. Nothing below depends on runner-local state.
runs-on: ubuntu-latest
steps:
# Up-front checkout: required so the local composite actions
# (./.github/actions/*) exist on the runner before they're referenced,
# and gives the version-bump step the base branch to diff against.
# (./.github/actions/*) exist on the runner before they're referenced.
# The vendored actions live here (not in the private pncit/shared-actions)
# because this repo is PUBLIC and can't clone a private action.
- uses: actions/checkout@v6

# Install the repo's Node (.nvmrc) before the toolchain check: the hosted
# image's system Node is whatever Ubuntu ships, not necessarily ours.
# validate-codebase below runs setup-node again; that's a cache hit.
- uses: actions/setup-node@v6
with:
fetch-depth: 2
node-version-file: .nvmrc

# Env check: fail fast if the runner's Node/npm majors don't match what
# the repo expects (NODE_MAJOR_VERSION / NPM_MAJOR_VERSION are pncit
# org-level Actions variables).
# Env check: fail fast if the Node/npm majors don't match what the repo
# expects (NODE_MAJOR_VERSION / NPM_MAJOR_VERSION are pncit org-level
# Actions variables). Also catches .nvmrc drifting from the org standard.
- uses: ./.github/actions/verify-node-toolchain
with:
node-major-version: ${{ vars.NODE_MAJOR_VERSION }}
npm-major-version: ${{ vars.NPM_MAJOR_VERSION }}

# Version-bump gate: timezest is a published library, so every PR into
# main must bump package.json's version — no exceptions. Dependabot PRs
# don't bump the package's own version, so they fail here by design; a
# maintainer rolls the dependency update into a versioned release (bumping
# the version) to land it. This prevents an unversioned change from
# merging and desyncing main from the published npm package.
- name: Verify version changed
if: github.event_name == 'pull_request'
shell: bash
run: |
set -euo pipefail
CUR=$(node -p "require('./package.json').version || ''")

# Get the base branch (usually main)
BASE_BRANCH="${GITHUB_BASE_REF:-main}"
git fetch origin "$BASE_BRANCH:$BASE_BRANCH" || true

if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
PREV=$(git show "$BASE_BRANCH:package.json" 2>/dev/null \
| node -p "(() => { const s=require('fs').readFileSync(0,'utf8'); try { return JSON.parse(s).version || '' } catch { return '' } })()" \
|| true)
else
PREV=""
fi

if [ -z "$CUR" ]; then
echo "::error::No version found in package.json"
exit 1
fi

if [ -n "$PREV" ] && [ "$CUR" = "$PREV" ]; then
echo "::error::package.json version unchanged ($PREV -> $CUR). Version must be bumped for PRs."
exit 1
fi

if [ -z "$PREV" ]; then
echo "Could not determine previous version, skipping version check"
else
echo "Version changed: $PREV -> $CUR ✓"
fi

# Codebase checks: npm ci + lint + typecheck + test. This action does its
# own checkout + setup-node (node-version-file: .nvmrc) and matches the
# @pncit scope / npmjs registry by default.
Expand Down
Loading