Skip to content

Commit 43c0fb2

Browse files
NeftedollarRoman Melnikov
andauthored
feat(dev-workflow): PR E — real release pipeline (all defineFunction) (#224)
* feat(dev-workflow): PR E — real release pipeline (all defineFunction) Swaps 4 noop nodes in release.ts for defineFunction implementations: - bump: semver bump via package.json rewrite (patch/minor/major from labels) - changelog: append dated section to CHANGELOG.md at repo root - publish: npm publish in PUBLISH_ORDER (critical path); respects dryRun via plan:true mode that prints instead of executes - cleanup: git tag the release commit (no auto-push — operator pushes) No LLM agents — release mechanics are deterministic. Bumps @ageflow/dev-workflow 0.0.13 → 0.0.14 (private). * fix(dev-workflow): PR E follow-up — 3 Codex P1 on #224 1. bump: parse affectedPackages from issue body (regex @ageflow/X); throw when list is empty to prevent silent no-op releases. 2. PUBLISH_ORDER: add @ageflow/runner-anthropic (non-private package, was omitted — would silently skip its publish on release). 3. publish: throw when any npm publish fails, instead of continuing to changelog+tag. Prevents misleading release tags on partial failure. Bumps @ageflow/dev-workflow 0.0.14 → 0.0.15 (private). * fix(dev-workflow): PR #224 lint — biome one-line regex declaration * fix(dev-workflow): PR #224 test — skip throw when in plan:true mode P1-3 fix (throw on publish failure) was too eager: it fired even in plan:true mode where package lookup failures are informational during dry-run. Restructured: skip directory lookup entirely in plan mode, treating it as a pure dry-run. --------- Co-authored-by: Roman Melnikov <roman@neftedollar.com>
1 parent 91b5c80 commit 43c0fb2

4 files changed

Lines changed: 521 additions & 38 deletions

File tree

bun.lock

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

packages/dev-workflow/__tests__/pipelines.test.ts

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// Smoke tests for the pipeline factories — confirms that `feature`, `bugfix`,
2-
// and `docs` build valid DAGs at definition time with the role prompts
3-
// loaded from disk.
2+
// `docs`, and `release` build valid DAGs at definition time.
43

5-
import { describe, expect, it } from "vitest";
4+
import { describe, expect, it, vi } from "vitest";
65
import { createBugfixPipeline } from "../pipelines/bugfix.js";
76
import { createDocsPipeline } from "../pipelines/docs.js";
87
import { createFeaturePipeline } from "../pipelines/feature.js";
8+
import {
9+
PUBLISH_ORDER,
10+
bumpFn,
11+
createReleasePipeline,
12+
publishFn,
13+
semverBump,
14+
} from "../pipelines/release.js";
915
import type { WorkflowInput } from "../shared/types.js";
1016

1117
const FAKE_INPUT: WorkflowInput = {
@@ -241,3 +247,152 @@ describe("docs pipeline", () => {
241247
expect(publish.dependsOn).toContain("review");
242248
});
243249
});
250+
251+
describe("release pipeline", () => {
252+
it("builds a workflow named release-pipeline with 4 tasks", () => {
253+
const wf = createReleasePipeline(FAKE_INPUT);
254+
expect(wf.name).toBe("release-pipeline");
255+
const keys = Object.keys(wf.tasks).sort();
256+
expect(keys).toEqual(["bump", "changelog", "cleanup", "publish"]);
257+
});
258+
259+
it("all 4 tasks are defineFunction (fn), not agent", () => {
260+
const wf = createReleasePipeline(FAKE_INPUT);
261+
for (const key of ["bump", "changelog", "publish", "cleanup"] as const) {
262+
const task = wf.tasks[key] as { agent?: unknown; fn?: unknown };
263+
expect(task.fn).toBeDefined();
264+
expect(task.agent).toBeUndefined();
265+
}
266+
});
267+
268+
it("bump has no dependsOn", () => {
269+
const wf = createReleasePipeline(FAKE_INPUT);
270+
const bump = wf.tasks.bump as { dependsOn?: readonly string[] };
271+
expect(bump.dependsOn).toBeUndefined();
272+
});
273+
274+
it("changelog dependsOn bump", () => {
275+
const wf = createReleasePipeline(FAKE_INPUT);
276+
const changelog = wf.tasks.changelog as { dependsOn?: readonly string[] };
277+
expect(changelog.dependsOn).toContain("bump");
278+
});
279+
280+
it("publish dependsOn changelog and bump", () => {
281+
const wf = createReleasePipeline(FAKE_INPUT);
282+
const publish = wf.tasks.publish as { dependsOn?: readonly string[] };
283+
expect(publish.dependsOn).toContain("changelog");
284+
expect(publish.dependsOn).toContain("bump");
285+
});
286+
287+
it("cleanup dependsOn publish and bump", () => {
288+
const wf = createReleasePipeline(FAKE_INPUT);
289+
const cleanup = wf.tasks.cleanup as { dependsOn?: readonly string[] };
290+
expect(cleanup.dependsOn).toContain("publish");
291+
expect(cleanup.dependsOn).toContain("bump");
292+
});
293+
});
294+
295+
describe("bumpFn.execute — P1-1 guard", () => {
296+
it("throws when affectedPackages is empty", async () => {
297+
await expect(
298+
bumpFn.execute({
299+
issueNumber: 1,
300+
labels: ["patch"],
301+
issueBody: "no package references here",
302+
worktreePath: "/tmp/fake-wt",
303+
affectedPackages: [],
304+
}),
305+
).rejects.toThrow("affectedPackages is empty");
306+
});
307+
308+
it("does not throw when affectedPackages has at least one entry", async () => {
309+
// The package dir won't exist on disk, so bumps will be empty but no throw.
310+
const result = await bumpFn.execute({
311+
issueNumber: 1,
312+
labels: ["patch"],
313+
issueBody: "@ageflow/core",
314+
worktreePath: "/tmp/fake-wt-nonexistent",
315+
affectedPackages: ["@ageflow/core"],
316+
});
317+
// No throw — bumps empty because dir doesn't exist, bumpKind defaults patch.
318+
expect(result.bumpKind).toBe("patch");
319+
expect(result.bumps).toEqual([]);
320+
});
321+
});
322+
323+
describe("publishFn.execute — P1-3 throw on failure", () => {
324+
it("throws when any npm publish fails (skipped.length > 0)", async () => {
325+
// Mock execa to reject for @ageflow/core, succeed for nothing else.
326+
vi.mock("execa", () => ({
327+
execa: vi
328+
.fn()
329+
.mockRejectedValue(new Error("E403 Forbidden — auth required")),
330+
}));
331+
332+
await expect(
333+
publishFn.execute({
334+
bumps: [{ package: "@ageflow/core", before: "0.6.0", after: "0.6.1" }],
335+
worktreePath: "/tmp/fake-wt-nonexistent",
336+
plan: false,
337+
}),
338+
).rejects.toThrow("publish failed for");
339+
340+
vi.restoreAllMocks();
341+
});
342+
343+
it("does not throw in plan:true mode (dry-run — no real publish)", async () => {
344+
// plan:true path never calls execa, so no failures.
345+
const result = await publishFn.execute({
346+
bumps: [{ package: "@ageflow/core", before: "0.6.0", after: "0.6.1" }],
347+
worktreePath: "/tmp/fake-wt-nonexistent",
348+
plan: true,
349+
});
350+
expect(result.published).toContain("@ageflow/core");
351+
expect(result.skipped).toHaveLength(0);
352+
});
353+
});
354+
355+
describe("PUBLISH_ORDER — P1-2 runner-anthropic included", () => {
356+
it("contains @ageflow/runner-anthropic", () => {
357+
expect(PUBLISH_ORDER).toContain("@ageflow/runner-anthropic");
358+
});
359+
360+
it("@ageflow/runner-anthropic appears after @ageflow/runner-api", () => {
361+
const apiIdx = PUBLISH_ORDER.indexOf("@ageflow/runner-api");
362+
const anthropicIdx = PUBLISH_ORDER.indexOf("@ageflow/runner-anthropic");
363+
expect(apiIdx).toBeGreaterThanOrEqual(0);
364+
expect(anthropicIdx).toBeGreaterThan(apiIdx);
365+
});
366+
367+
it("@ageflow/runner-anthropic appears before @ageflow/testing", () => {
368+
const anthropicIdx = PUBLISH_ORDER.indexOf("@ageflow/runner-anthropic");
369+
const testingIdx = PUBLISH_ORDER.indexOf("@ageflow/testing");
370+
expect(anthropicIdx).toBeLessThan(testingIdx);
371+
});
372+
});
373+
374+
describe("semverBump", () => {
375+
it("patch: increments patch, leaves major/minor", () => {
376+
expect(semverBump("1.2.3", "patch")).toBe("1.2.4");
377+
expect(semverBump("0.0.0", "patch")).toBe("0.0.1");
378+
expect(semverBump("1.0.0", "patch")).toBe("1.0.1");
379+
});
380+
381+
it("minor: increments minor, resets patch", () => {
382+
expect(semverBump("1.2.3", "minor")).toBe("1.3.0");
383+
expect(semverBump("0.5.9", "minor")).toBe("0.6.0");
384+
expect(semverBump("2.0.0", "minor")).toBe("2.1.0");
385+
});
386+
387+
it("major: increments major, resets minor + patch", () => {
388+
expect(semverBump("1.2.3", "major")).toBe("2.0.0");
389+
expect(semverBump("0.9.9", "major")).toBe("1.0.0");
390+
expect(semverBump("3.4.5", "major")).toBe("4.0.0");
391+
});
392+
393+
it("throws on invalid semver string", () => {
394+
expect(() => semverBump("not-a-version", "patch")).toThrow(
395+
"invalid semver",
396+
);
397+
});
398+
});

packages/dev-workflow/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ageflow/dev-workflow",
3-
"version": "0.0.13",
3+
"version": "0.0.15",
44
"private": true,
55
"type": "module",
66
"scripts": {

0 commit comments

Comments
 (0)