Skip to content

Commit 0955d41

Browse files
authored
Merge pull request #37 from AgentWorkforce/feat/installable-persona-sources
Publish packages in lockstep
2 parents 429fd79 + 390bd6b commit 0955d41

4 files changed

Lines changed: 111 additions & 103 deletions

File tree

‎.github/workflows/publish.yml‎

Lines changed: 108 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
name: Publish Package
1+
name: Publish Packages
22

33
on:
44
workflow_dispatch:
55
inputs:
6-
package:
7-
description: 'Which package(s) to publish'
8-
required: true
9-
default: cli
10-
type: choice
11-
options:
12-
- all
13-
- workload-router
14-
- harness-kit
15-
- cli
16-
- agentworkforce
176
version:
187
description: 'Version bump type (ignored if custom_version is set)'
198
required: true
@@ -66,15 +55,15 @@ jobs:
6655
versions: ${{ steps.bump.outputs.versions }}
6756
steps:
6857
- name: Checkout
69-
uses: actions/checkout@v4
58+
uses: actions/checkout@v6
7059
with:
7160
fetch-depth: 0
7261

7362
- name: Setup pnpm
74-
uses: pnpm/action-setup@v4
63+
uses: pnpm/action-setup@v5
7564

7665
- name: Setup Node
77-
uses: actions/setup-node@v4
66+
uses: actions/setup-node@v6
7867
with:
7968
node-version: '22.14.0'
8069
registry-url: 'https://registry.npmjs.org'
@@ -83,89 +72,111 @@ jobs:
8372
- name: Install deps
8473
run: pnpm install --frozen-lockfile
8574

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

9481
- name: Run tests
9582
run: pnpm -r run test
9683

97-
- name: Resolve target packages (dep order)
84+
- name: Resolve target packages
9885
id: targets
9986
run: |
100-
case "${{ github.event.inputs.package }}" in
101-
all)
102-
# Must be in dependency order: router → harness-kit → cli →
103-
# agentworkforce (the wrapper depends on cli).
104-
echo "packages=workload-router harness-kit cli agentworkforce" >> "$GITHUB_OUTPUT"
105-
;;
106-
workload-router|harness-kit|cli|agentworkforce)
107-
echo "packages=${{ github.event.inputs.package }}" >> "$GITHUB_OUTPUT"
108-
;;
109-
*)
110-
echo "Unknown package: ${{ github.event.inputs.package }}" >&2
111-
exit 1
112-
;;
113-
esac
114-
115-
# Catches the failure mode that bit us on 2026-04-23: a previous
116-
# publish run shipped @agentworkforce/*@0.3.0 to npm but failed at the
117-
# final `git push origin HEAD --follow-tags` step (a concurrent PR
118-
# merge made main non-fast-forwardable), so neither the *-v0.3.0 tags
119-
# nor the chore(release) commit landed on main. A naive re-run would
120-
# clone at 0.2.x, bump to 0.3.0 again, and hit npm's "cannot publish
121-
# over previously published versions: 0.3.0."
87+
# Dependency order: workload-router → harness-kit → cli → agentworkforce.
88+
# The top-level `agentworkforce` wrapper depends on `@agentworkforce/cli`,
89+
# so it must publish last.
90+
echo "packages=workload-router harness-kit cli agentworkforce" >> "$GITHUB_OUTPUT"
91+
92+
# Lockstep baseline heal. The workspace publishes every package at the
93+
# same version, so if any package's local version lags either its own
94+
# npm `latest` or another workspace package, pull it up to the highest
95+
# stable version across the whole set before the bump step runs. This
96+
# absorbs two failure modes:
12297
#
123-
# If npm's highest stable version is greater than what package.json
124-
# has, abort here with a clear remediation message rather than
125-
# letting the bump produce a doomed version downstream.
126-
- name: Verify local versions are in sync with npm
98+
# 1. A previous publish run shipped @agentworkforce/*@X to npm but
99+
# failed at the Tag + push step, so main did not receive the
100+
# release commit or tags.
101+
# 2. Packages drifted because older releases were allowed to publish
102+
# only one package at a time.
103+
#
104+
# The downstream "Verify new versions are not yet published" step still
105+
# catches the case where the post-bump version collides with an existing
106+
# npm version.
107+
- name: Heal local versions to lockstep baseline
127108
run: |
128109
set -euo pipefail
129-
for pkg in ${{ steps.targets.outputs.packages }}; do
130-
NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name")
131-
LOCAL=$(node -p "require('./packages/$pkg/package.json').version")
132-
REMOTE=$(npm view "$NPM_NAME" versions --json 2>/dev/null \
133-
| node -e '
134-
const raw = require("fs").readFileSync(0, "utf8").trim() || "[]";
135-
const parsed = JSON.parse(raw);
136-
const arr = Array.isArray(parsed) ? parsed : [parsed];
137-
const stable = arr.filter((v) => typeof v === "string" && !v.includes("-"));
138-
stable.sort((a, b) => {
139-
const pa = a.split(".").map(Number);
140-
const pb = b.split(".").map(Number);
141-
for (let i = 0; i < 3; i++) {
142-
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
143-
}
144-
return 0;
145-
});
146-
process.stdout.write(stable.length ? stable[stable.length - 1] : "");
147-
' || echo "")
148-
if [ -z "$REMOTE" ]; then
149-
echo "$NPM_NAME: no stable releases on npm yet — OK"
150-
continue
151-
fi
152-
CMP=$(node -e '
153-
const [a, b] = process.argv.slice(1);
154-
const pa = a.split(".").map(Number);
155-
const pb = b.split(".").map(Number);
156-
for (let i = 0; i < 3; i++) {
157-
const da = pa[i] || 0;
158-
const db = pb[i] || 0;
159-
if (da !== db) { console.log(da < db ? -1 : 1); process.exit(0); }
160-
}
161-
console.log(0);
162-
' "$LOCAL" "$REMOTE")
163-
if [ "$CMP" = "-1" ]; then
164-
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.)"
165-
exit 1
166-
fi
167-
echo "$NPM_NAME: local=$LOCAL, npm=$REMOTE — OK"
168-
done
110+
cat > /tmp/lockstep-heal.mjs << 'HEALEOF'
111+
import { execSync } from 'node:child_process';
112+
import { readFileSync } from 'node:fs';
113+
114+
const packages = process.argv.slice(2);
115+
const cmp = (a, b) => {
116+
const pa = a.split('.').map(Number);
117+
const pb = b.split('.').map(Number);
118+
for (let i = 0; i < 3; i++) {
119+
const da = pa[i] || 0;
120+
const db = pb[i] || 0;
121+
if (da !== db) return da - db;
122+
}
123+
return 0;
124+
};
125+
const isStable = (v) => typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v);
126+
127+
const info = packages.map((pkg) => {
128+
const json = JSON.parse(readFileSync(`packages/${pkg}/package.json`, 'utf8'));
129+
let npmHighest = null;
130+
try {
131+
const raw = execSync(`npm view ${json.name} versions --json`, {
132+
encoding: 'utf8',
133+
stdio: ['ignore', 'pipe', 'ignore'],
134+
}).trim() || '[]';
135+
const parsed = JSON.parse(raw);
136+
const arr = Array.isArray(parsed) ? parsed : [parsed];
137+
const stable = arr.filter(isStable).sort(cmp);
138+
if (stable.length) npmHighest = stable[stable.length - 1];
139+
} catch {
140+
// unpublished package: leave npmHighest null
141+
}
142+
return { pkg, name: json.name, local: json.version, npmHighest };
143+
});
144+
145+
const candidates = info.flatMap((e) => [e.local, e.npmHighest]).filter(isStable);
146+
if (candidates.length === 0) {
147+
console.log('Lockstep baseline: (no stable versions yet, skipping heal)');
148+
process.exit(0);
149+
}
150+
candidates.sort(cmp);
151+
const baseline = candidates[candidates.length - 1];
152+
console.log(`Lockstep baseline: ${baseline}`);
153+
154+
const heals = [];
155+
for (const e of info) {
156+
const remote = e.npmHighest ?? 'unpublished';
157+
if (isStable(e.local) && cmp(e.local, baseline) < 0) {
158+
heals.push(e);
159+
console.log(` ${e.name}: local=${e.local} npm=${remote} - healing to ${baseline}`);
160+
} else {
161+
console.log(` ${e.name}: local=${e.local} npm=${remote} - OK`);
162+
}
163+
}
164+
165+
for (const e of heals) {
166+
execSync(`npm version ${baseline} --no-git-tag-version --allow-same-version`, {
167+
cwd: `packages/${e.pkg}`,
168+
stdio: 'inherit',
169+
});
170+
}
171+
172+
if (heals.length === 0) {
173+
console.log('All packages at baseline - no heal needed.');
174+
} else {
175+
console.log(`Healed ${heals.length} package(s) up to ${baseline}.`);
176+
}
177+
HEALEOF
178+
179+
node /tmp/lockstep-heal.mjs ${{ steps.targets.outputs.packages }}
169180
170181
- name: Bump versions
171182
id: bump
@@ -191,11 +202,11 @@ jobs:
191202
done
192203
echo "versions=${VERSIONS# }" >> "$GITHUB_OUTPUT"
193204
194-
# Belt-and-suspenders alongside the parity check above: even if the
195-
# local→npm baseline is in sync, the computed bump might still collide
196-
# with an existing version (e.g. a one-off publish from another
197-
# branch). Catch it before we waste a build + before npm rejects with
198-
# a less specific error.
205+
# Belt-and-suspenders alongside the baseline heal above: even if the
206+
# local and npm baselines are aligned, the computed bump might still
207+
# collide with an existing version (e.g. a one-off publish from another
208+
# branch). Catch it before we waste a build + before npm rejects with a
209+
# less specific error.
199210
- name: Verify new versions are not yet published
200211
run: |
201212
set -euo pipefail
@@ -452,11 +463,9 @@ jobs:
452463
453464
# One GitHub Release per publish run. Per-package git tags are still pushed
454465
# above (so the next publish's changelog generator can find them), but the
455-
# public GitHub Release is anchored to a single canonical tag — `agentworkforce`
456-
# if the wrapper was bumped, otherwise the first package in the run. The
457-
# release body lists every published package and inlines each one's
458-
# CHANGELOG block, so the releases page has one item per version instead of
459-
# 4 near-duplicate entries per `all` publish.
466+
# public GitHub Release is anchored to the `agentworkforce` tag for lockstep
467+
# publishes. The release body lists every published package and inlines each
468+
# one's CHANGELOG block, so the releases page has one item per version.
460469
create-release:
461470
name: Create GitHub Release
462471
needs: publish
@@ -500,7 +509,7 @@ jobs:
500509
fi
501510
502511
- name: Checkout
503-
uses: actions/checkout@v4
512+
uses: actions/checkout@v6
504513
with:
505514
# Need the canonical tag that the publish job just pushed — it
506515
# points at the chore(release) commit with all bumped CHANGELOGs.
@@ -599,7 +608,7 @@ jobs:
599608
node /tmp/build-release-notes.mjs
600609
601610
- name: Create GitHub Release
602-
uses: softprops/action-gh-release@v2
611+
uses: softprops/action-gh-release@v3
603612
with:
604613
tag_name: ${{ steps.release.outputs.tag_name }}
605614
name: ${{ steps.notes.outputs.release_name }}

‎README.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
![AgentWorkforce banner](./workforce-readme-banner.png)
22

3-
# workforce
3+
Saved configurations of coding agents you can save and share with your collegues.
44

5-
Shared AgentWorkforce primitives for persona-driven orchestration.
65

76
## Core frame
87

‎packages/harness-kit/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"package.json"
1919
],
2020
"dependencies": {
21-
"@agentworkforce/workload-router": "workspace:^"
21+
"@agentworkforce/workload-router": "workspace:*"
2222
},
2323
"repository": {
2424
"type": "git",

‎pnpm-lock.yaml‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)