Skip to content

feat(monitoring): ProcessFootprintMonitor — per-machine process-footp… #1636

feat(monitoring): ProcessFootprintMonitor — per-machine process-footp…

feat(monitoring): ProcessFootprintMonitor — per-machine process-footp… #1636

Workflow file for this run

name: Publish to npm
on:
push:
branches: [main]
# Only one publish at a time
concurrency:
group: publish
cancel-in-progress: false
jobs:
# Note: no CI gate inside this workflow. ci.yml (lint + unit [node 20+22] +
# build + integration + e2e) runs on the same main push and is enforced as a
# required status check on PRs via branch protection. Duplicating it here
# added ~10 min per publish with no additional safety.
publish:
name: Bump & Publish
runs-on: ubuntu-latest
# Skip version-bump commits to prevent infinite loop
if: "!contains(github.event.head_commit.message, '[skip ci]')"
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
# Always fetch latest HEAD, not the triggering SHA — avoids version
# collisions when a prior queued run already bumped the version.
ref: main
# RELEASE_TOKEN is a PAT that can bypass branch protection for version bump commits.
# Falls back to GITHUB_TOKEN but that will fail if branch protection is enabled.
token: ${{ secrets.RELEASE_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- name: Assemble release-note fragments
# Release notes are authored as per-PR FRAGMENTS in upgrades/next/<slug>.md
# so concurrent PRs never collide on a single shared NEXT.md. This pre-step
# folds every fragment (+ any legacy upgrades/NEXT.md) into upgrades/NEXT.md
# BEFORE the existing pipeline runs — everything downstream is UNCHANGED.
# No fragments AND no legacy NEXT.md → it does nothing and the next step's
# skip logic fires exactly as before. A malformed fragment exits non-zero
# and fails the run loudly (better than shipping a broken guide).
run: node scripts/assemble-next-md.mjs
- name: Check if NEXT.md has content to publish
id: guide-check
run: |
if [ ! -f "upgrades/NEXT.md" ]; then
echo "No NEXT.md found — nothing to publish"
echo "skip=true" >> "$GITHUB_OUTPUT"
elif grep -q '\[Feature name\]' upgrades/NEXT.md && grep -q '\[Capability\]' upgrades/NEXT.md; then
echo "NEXT.md is still a template — nothing to publish"
echo "skip=true" >> "$GITHUB_OUTPUT"
elif grep -qE '<!--[[:space:]]*auto-draft-unreviewed' upgrades/NEXT.md; then
# Layer A auto-drafted this guide but a human hasn't reviewed it yet
# (release-readiness-visibility spec §4.1.1). Skipping keeps the
# silent-skip semantics correct for the unreviewed case; the
# release-readiness sentinel surfaces the blocked release as a signal.
#
# Match the comment SHAPE (`<!-- auto-draft-unreviewed...`), not the
# bare substring — a NEXT.md that describes the marker in prose
# (e.g. "this section carries an auto-draft-unreviewed marker")
# is a human-authored doc and must not trip the gate.
echo "NEXT.md still has auto-draft-unreviewed markers — awaiting human review, not publishing"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "NEXT.md has content — proceeding with publish"
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Release-tier gate (Layer 2)
# Deployment Lockdown — Layer 2.
# `.instar/release-tier.json` declares the active release line
# (patch | minor | major | hold). The workflow honors that file
# BEFORE any side-effectful step. This is the operator's
# authoritative "no deploy" signal — without it, the workflow has
# no concept of a holding pattern and ships every PR as a patch.
# That's the exact gap that caused the 2026-05-19 misalignment.
# Resolution logic lives in scripts/resolve-release-tier.mjs so it
# is unit-tested (tests/unit/resolve-release-tier.test.ts) rather
# than buried in inline bash.
if: steps.guide-check.outputs.skip != 'true'
id: tier-gate
run: |
NPM_VERSION=$(npm view instar version 2>/dev/null || echo "0.0.0")
LOCAL_VERSION=$(node -p "require('./package.json').version")
echo "npm has: $NPM_VERSION"
echo "package.json: $LOCAL_VERSION"
set +e
DECISION=$(node scripts/resolve-release-tier.mjs "$LOCAL_VERSION" "$NPM_VERSION" 2> /tmp/tier-reason.txt)
STATUS=$?
set -e
REASON=$(cat /tmp/tier-reason.txt)
echo "tier reason: $REASON"
if [ "$STATUS" -ne 0 ]; then
echo "::error::release-tier: $REASON"
exit "$STATUS"
fi
if [ "$DECISION" = "skip" ]; then
{
echo "## Publish skipped — Layer 2 release-tier gate"
echo ""
echo "$REASON"
echo ""
echo "To resume publishing, edit \`.instar/release-tier.json\` (currently the tier blocks this release)."
} >> "$GITHUB_STEP_SUMMARY"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Determine next version
if: steps.guide-check.outputs.skip != 'true' && steps.tier-gate.outputs.skip != 'true'
id: bump
run: |
# Version-truth policy (added 2026-05-19 after the v1.0.0 deployment
# misalignment incident — docs/incidents/2026-05-19-v1-deployment-misalignment.md):
#
# package.json is the source of authority for an operator-intended
# version. The registry is the source of truth for what already
# shipped. Reconcile them:
#
# LOCAL > NPM → publish at LOCAL (operator-intended leap:
# major, minor, or an explicit patch jump)
# LOCAL == NPM → routine patch: bump from NPM (+1 patch). This is
# the common case — a PR that does not touch
# package.json leaves LOCAL equal to the last
# released version.
# LOCAL < NPM → stale package.json (e.g. a queued run that landed
# after an earlier publish already bumped). Do NOT
# downgrade — bump from NPM (+1 patch).
#
# The old behavior (always derive from NPM, ignore package.json) made
# an operator-intended major bump structurally impossible. That is the
# exact failure this policy closes.
NPM_VERSION=$(npm view instar version 2>/dev/null || echo "0.0.0")
LOCAL_VERSION=$(node -p "require('./package.json').version")
echo "npm has: $NPM_VERSION"
echo "package.json: $LOCAL_VERSION"
# Resolution policy lives in scripts/resolve-publish-version.mjs so it
# is unit-tested (tests/unit/resolve-publish-version.test.ts) rather
# than buried in inline bash.
NEXT=$(node scripts/resolve-publish-version.mjs "$LOCAL_VERSION" "$NPM_VERSION")
echo "resolved: $NEXT"
# Apply to package.json
npm version "$NEXT" --no-git-tag-version --allow-same-version
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
echo "Resolved publish version: $NEXT"
- name: Rename NEXT.md to version-specific guide
if: steps.guide-check.outputs.skip != 'true' && steps.tier-gate.outputs.skip != 'true'
run: |
if [ -f "upgrades/NEXT.md" ]; then
mv upgrades/NEXT.md "upgrades/${{ steps.bump.outputs.version }}.md"
echo "Renamed upgrades/NEXT.md → upgrades/${{ steps.bump.outputs.version }}.md"
else
echo "No upgrades/NEXT.md found — check-upgrade-guide will enforce"
fi
# Consume the per-PR fragments now that their content has been folded
# into the version-specific guide. The commit step's `git add upgrades/`
# stages these deletions, so released fragments don't accumulate.
if ls upgrades/next/*.md >/dev/null 2>&1; then
rm -f upgrades/next/*.md
echo "Removed consumed release-note fragments from upgrades/next/"
fi
- name: Check upgrade guide
if: steps.guide-check.outputs.skip != 'true' && steps.tier-gate.outputs.skip != 'true'
run: node scripts/check-upgrade-guide.js
continue-on-error: false
- name: Publish to npm
if: steps.guide-check.outputs.skip != 'true' && steps.tier-gate.outputs.skip != 'true'
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Post-publish smoke test (clean install + --version)
if: steps.guide-check.outputs.skip != 'true' && steps.tier-gate.outputs.skip != 'true'
run: node scripts/post-publish-smoke.mjs "${{ steps.bump.outputs.version }}"
- name: Commit version bump & tag
if: steps.guide-check.outputs.skip != 'true' && steps.tier-gate.outputs.skip != 'true'
env:
HUSKY: '0'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json upgrades/
git commit -m "chore: release v${{ steps.bump.outputs.version }} [skip ci]"
# Push the release commit, rebasing onto any PR MERGE that landed on
# main while this publish was running. Publishes are already serialized
# against each other by the workflow `concurrency` group, but a
# concurrent merge (a different workflow) can still move main between
# our fetch and our push, rejecting it. Concurrent merges are NOT
# releases — they never bump npm — so the version this run resolved
# stays valid: a clean rebase + re-push, no version re-resolution.
# The common path (push succeeds first try) is unchanged; the retry
# only runs on a rejected push.
pushed=false
for attempt in 1 2 3 4 5; do
if git push origin main; then pushed=true; break; fi
echo "push rejected — main moved (likely a concurrent merge); rebasing and retrying (attempt ${attempt})"
git fetch origin main
if ! git rebase origin/main; then
echo "::error::release commit could not be rebased cleanly onto main (conflict) — aborting rather than force-pushing"
git rebase --abort || true
exit 1
fi
done
if [ "${pushed}" != "true" ]; then
echo "::error::could not push the release commit after 5 rebase-retries"
exit 1
fi
# Tag the FINAL (possibly rebased) commit and push the tag separately,
# so the tag always points at the commit that actually landed on main.
git tag "v${{ steps.bump.outputs.version }}"
git push origin "v${{ steps.bump.outputs.version }}"