feat(monitoring): ProcessFootprintMonitor — per-machine process-footp… #1636
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" |