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
207 changes: 108 additions & 99 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
name: Publish Package
name: Publish Packages

on:
workflow_dispatch:
inputs:
package:
description: 'Which package(s) to publish'
required: true
default: cli
type: choice
options:
- all
- workload-router
- harness-kit
- cli
- agentworkforce
version:
description: 'Version bump type (ignored if custom_version is set)'
required: true
Expand Down Expand Up @@ -66,15 +55,15 @@ jobs:
versions: ${{ steps.bump.outputs.versions }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5

- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '22.14.0'
registry-url: 'https://registry.npmjs.org'
Expand All @@ -83,89 +72,111 @@ jobs:
- name: Install deps
run: pnpm install --frozen-lockfile

# Build + test everything regardless of which package is being released —
# the CLI depends on harness-kit which depends on workload-router via
# workspace:*. `pnpm publish` rewrites those specs to concrete versions
# at pack time, and we want every package's dist/ to be fresh when that
# happens.
# Packages publish in lockstep. Build + test the whole workspace so every
# package's dist/ is fresh before `pnpm pack` rewrites workspace:* deps
# to concrete versions at pack time.
- name: Build workspace
run: pnpm -r run build

- name: Run tests
run: pnpm -r run test

- name: Resolve target packages (dep order)
- name: Resolve target packages
id: targets
run: |
case "${{ github.event.inputs.package }}" in
all)
# Must be in dependency order: router → harness-kit → cli →
# agentworkforce (the wrapper depends on cli).
echo "packages=workload-router harness-kit cli agentworkforce" >> "$GITHUB_OUTPUT"
;;
workload-router|harness-kit|cli|agentworkforce)
echo "packages=${{ github.event.inputs.package }}" >> "$GITHUB_OUTPUT"
;;
*)
echo "Unknown package: ${{ github.event.inputs.package }}" >&2
exit 1
;;
esac

# Catches the failure mode that bit us on 2026-04-23: a previous
# publish run shipped @agentworkforce/*@0.3.0 to npm but failed at the
# final `git push origin HEAD --follow-tags` step (a concurrent PR
# merge made main non-fast-forwardable), so neither the *-v0.3.0 tags
# nor the chore(release) commit landed on main. A naive re-run would
# clone at 0.2.x, bump to 0.3.0 again, and hit npm's "cannot publish
# over previously published versions: 0.3.0."
# Dependency order: workload-router → harness-kit → cli → agentworkforce.
# The top-level `agentworkforce` wrapper depends on `@agentworkforce/cli`,
# so it must publish last.
echo "packages=workload-router harness-kit cli agentworkforce" >> "$GITHUB_OUTPUT"

# Lockstep baseline heal. The workspace publishes every package at the
# same version, so if any package's local version lags either its own
# npm `latest` or another workspace package, pull it up to the highest
# stable version across the whole set before the bump step runs. This
# absorbs two failure modes:
#
# If npm's highest stable version is greater than what package.json
# has, abort here with a clear remediation message rather than
# letting the bump produce a doomed version downstream.
- name: Verify local versions are in sync with npm
# 1. A previous publish run shipped @agentworkforce/*@X to npm but
# failed at the Tag + push step, so main did not receive the
# release commit or tags.
# 2. Packages drifted because older releases were allowed to publish
# only one package at a time.
#
# The downstream "Verify new versions are not yet published" step still
# catches the case where the post-bump version collides with an existing
# npm version.
- name: Heal local versions to lockstep baseline
run: |
set -euo pipefail
for pkg in ${{ steps.targets.outputs.packages }}; do
NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name")
LOCAL=$(node -p "require('./packages/$pkg/package.json').version")
REMOTE=$(npm view "$NPM_NAME" versions --json 2>/dev/null \
| node -e '
const raw = require("fs").readFileSync(0, "utf8").trim() || "[]";
const parsed = JSON.parse(raw);
const arr = Array.isArray(parsed) ? parsed : [parsed];
const stable = arr.filter((v) => typeof v === "string" && !v.includes("-"));
stable.sort((a, b) => {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i++) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
}
return 0;
});
process.stdout.write(stable.length ? stable[stable.length - 1] : "");
' || echo "")
if [ -z "$REMOTE" ]; then
echo "$NPM_NAME: no stable releases on npm yet — OK"
continue
fi
CMP=$(node -e '
const [a, b] = process.argv.slice(1);
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i++) {
const da = pa[i] || 0;
const db = pb[i] || 0;
if (da !== db) { console.log(da < db ? -1 : 1); process.exit(0); }
}
console.log(0);
' "$LOCAL" "$REMOTE")
if [ "$CMP" = "-1" ]; then
echo "::error title=npm/git version drift::$NPM_NAME: package.json is at $LOCAL but npm has $REMOTE published. A previous publish run likely succeeded on npm but failed before tagging. Bump packages/$pkg/package.json to >= $REMOTE on a branch, push, then re-run this workflow. (You may also want to tag the prior release commit as $pkg-v$REMOTE so the changelog generator picks the right baseline.)"
exit 1
fi
echo "$NPM_NAME: local=$LOCAL, npm=$REMOTE — OK"
done
cat > /tmp/lockstep-heal.mjs << 'HEALEOF'
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';

const packages = process.argv.slice(2);
const cmp = (a, b) => {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
const da = pa[i] || 0;
const db = pb[i] || 0;
if (da !== db) return da - db;
}
return 0;
};
const isStable = (v) => typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v);

const info = packages.map((pkg) => {
const json = JSON.parse(readFileSync(`packages/${pkg}/package.json`, 'utf8'));
let npmHighest = null;
try {
const raw = execSync(`npm view ${json.name} versions --json`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim() || '[]';
const parsed = JSON.parse(raw);
const arr = Array.isArray(parsed) ? parsed : [parsed];
const stable = arr.filter(isStable).sort(cmp);
if (stable.length) npmHighest = stable[stable.length - 1];
} catch {
// unpublished package: leave npmHighest null
}
return { pkg, name: json.name, local: json.version, npmHighest };
});

const candidates = info.flatMap((e) => [e.local, e.npmHighest]).filter(isStable);
if (candidates.length === 0) {
console.log('Lockstep baseline: (no stable versions yet, skipping heal)');
process.exit(0);
}
candidates.sort(cmp);
const baseline = candidates[candidates.length - 1];
console.log(`Lockstep baseline: ${baseline}`);

const heals = [];
for (const e of info) {
const remote = e.npmHighest ?? 'unpublished';
if (isStable(e.local) && cmp(e.local, baseline) < 0) {
heals.push(e);
console.log(` ${e.name}: local=${e.local} npm=${remote} - healing to ${baseline}`);
} else {
console.log(` ${e.name}: local=${e.local} npm=${remote} - OK`);
}
}

for (const e of heals) {
execSync(`npm version ${baseline} --no-git-tag-version --allow-same-version`, {
cwd: `packages/${e.pkg}`,
stdio: 'inherit',
});
Comment on lines +166 to +169
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip lockstep heal when version bump is none

The new baseline-heal step runs npm version and can mutate package versions even when the dispatch input uses version: none (the documented no-bump path). In that mode, the later commit/tag steps are skipped by their if conditions, but Pack + publish still runs, so you can publish healed versions that are never committed or tagged in git. This reintroduces npm/git drift and breaks later changelog/tag baselines for any run where heal changes versions but no explicit bump/custom version was requested.

Useful? React with 👍 / 👎.

}

if (heals.length === 0) {
console.log('All packages at baseline - no heal needed.');
} else {
console.log(`Healed ${heals.length} package(s) up to ${baseline}.`);
}
HEALEOF

node /tmp/lockstep-heal.mjs ${{ steps.targets.outputs.packages }}

- name: Bump versions
id: bump
Expand All @@ -191,11 +202,11 @@ jobs:
done
echo "versions=${VERSIONS# }" >> "$GITHUB_OUTPUT"

# Belt-and-suspenders alongside the parity check above: even if the
# local→npm baseline is in sync, the computed bump might still collide
# with an existing version (e.g. a one-off publish from another
# branch). Catch it before we waste a build + before npm rejects with
# a less specific error.
# Belt-and-suspenders alongside the baseline heal above: even if the
# local and npm baselines are aligned, the computed bump might still
# collide with an existing version (e.g. a one-off publish from another
# branch). Catch it before we waste a build + before npm rejects with a
# less specific error.
- name: Verify new versions are not yet published
run: |
set -euo pipefail
Expand Down Expand Up @@ -452,11 +463,9 @@ jobs:

# One GitHub Release per publish run. Per-package git tags are still pushed
# above (so the next publish's changelog generator can find them), but the
# public GitHub Release is anchored to a single canonical tag — `agentworkforce`
# if the wrapper was bumped, otherwise the first package in the run. The
# release body lists every published package and inlines each one's
# CHANGELOG block, so the releases page has one item per version instead of
# 4 near-duplicate entries per `all` publish.
# public GitHub Release is anchored to the `agentworkforce` tag for lockstep
# publishes. The release body lists every published package and inlines each
# one's CHANGELOG block, so the releases page has one item per version.
create-release:
name: Create GitHub Release
needs: publish
Expand Down Expand Up @@ -500,7 +509,7 @@ jobs:
fi

- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
# Need the canonical tag that the publish job just pushed — it
# points at the chore(release) commit with all bumped CHANGELOGs.
Expand Down Expand Up @@ -599,7 +608,7 @@ jobs:
node /tmp/build-release-notes.mjs

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.release.outputs.tag_name }}
name: ${{ steps.notes.outputs.release_name }}
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
![AgentWorkforce banner](./workforce-readme-banner.png)

# workforce
Saved configurations of coding agents you can save and share with your collegues.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in the new tagline.

Line 3 has a spelling error: colleguescolleagues.

✏️ Proposed fix
-Saved configurations of coding agents you can save and share with your collegues.
+Saved configurations of coding agents you can save and share with your colleagues.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Saved configurations of coding agents you can save and share with your collegues.
Saved configurations of coding agents you can save and share with your colleagues.
🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Ensure spelling is correct
Context: ...agents you can save and share with your collegues. ## Core frame A persona is the runtime...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 3, Update the tagline string "Saved configurations of
coding agents you can save and share with your collegues." by correcting the
misspelling "collegues" to "colleagues" so the sentence reads "Saved
configurations of coding agents you can save and share with your colleagues.";
locate and edit the exact line containing that tagline in README.md.


Shared AgentWorkforce primitives for persona-driven orchestration.

## Core frame

Expand Down
2 changes: 1 addition & 1 deletion packages/harness-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"package.json"
],
"dependencies": {
"@agentworkforce/workload-router": "workspace:^"
"@agentworkforce/workload-router": "workspace:*"
},
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.