Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

98 lines · 3.7 KBRaw History
1import { mkdirSync, writeFileSync } from "node:fs";
2import path from "node:path";
3import { afterEach, beforeEach, describe, expect, it } from "vitest";
4import { parseInputs } from "../src/commands/workflow.js";
5import { fakeServer, isolatedEnv, run, tempDir, type FakeServer } from "./helpers.js";
6
7const GOOD = `name: CI
8on: [push]
9jobs:
10 test:
11 runs-on: ubuntu-latest
12 steps:
13 - run: echo hi
14`;
15
16const BAD = `name: Broken
17on: push
18jobs:
19 build:
20 runs-on: ubuntu-latest
21 steps:
22 - run: echo hi
23 bogus: true
24 deploy:
25 needs: missing-job
26 runs-on: ubuntu-latest
27 steps:
28 - run: echo deploy
29`;
30
31describe("workflow validate", () => {
32 function project(files: Record<string, string>): string {
33 const dir = tempDir("diffinery-wf-");
34 for (const [p, content] of Object.entries(files)) {
35 mkdirSync(path.dirname(path.join(dir, p)), { recursive: true });
36 writeFileSync(path.join(dir, p), content);
37 }
38 return dir;
39 }
40
41 it("passes valid files in both workflow directories", async () => {
42 const cwd = project({ ".github/workflows/ci.yml": GOOD, ".diffinery/workflows/other.yaml": GOOD, ".github/workflows/README.md": "not yaml" });
43 const r = await run(["workflow", "validate"], { cwd, env: isolatedEnv() });
44 expect(r.code).toBe(0);
45 expect(r.stdout).toContain(".github/workflows/ci.yml");
46 expect(r.stdout).toContain(".diffinery/workflows/other.yaml");
47 expect(r.stderr).toContain("2 files checked: no errors");
48 });
49
50 it("reports errors with file:line and exits 1", async () => {
51 const cwd = project({ ".github/workflows/ci.yml": GOOD, ".github/workflows/bad.yml": BAD });
52 const r = await run(["workflow", "validate"], { cwd, env: isolatedEnv() });
53 expect(r.code).toBe(1);
54 expect(r.stdout).toMatch(/\.github\/workflows\/bad\.yml:10(:\d+)?: error: .*missing-job/);
55 expect(r.stdout).toMatch(/\.github\/workflows\/bad\.yml:8(:\d+)?: (error|warning): .*bogus/);
56 expect(r.stderr).toMatch(/2 files checked: \d+ errors?/);
57 });
58
59 it("validates explicit files and reports YAML syntax errors", async () => {
60 const cwd = project({ "x.yml": "jobs: [unclosed\n" });
61 const r = await run(["workflow", "validate", "x.yml", "--json"], { cwd, env: isolatedEnv() });
62 expect(r.code).toBe(1);
63 const res = JSON.parse(r.stdout);
64 expect(res[0].file).toBe("x.yml");
65 expect(res[0].valid).toBe(false);
66 expect(res[0].problems[0].message).toMatch(/YAML/);
67 });
68
69 it("is fine with no workflow files", async () => {
70 const r = await run(["workflow", "validate"], { cwd: project({}), env: isolatedEnv() });
71 expect(r.code).toBe(0);
72 expect(r.stderr).toContain("No workflow files");
73 });
74});
75
76describe("workflow run", () => {
77 let srv: FakeServer;
78 beforeEach(async () => {
79 srv = await fakeServer();
80 });
81 afterEach(() => srv.close());
82
83 it("dispatches with inputs on the default branch", async () => {
84 srv.on("GET", "/api/v3/repos/o/r", { body: { default_branch: "trunk" } });
85 srv.on("POST", /\/actions\/workflows\/.+\/dispatches$/, { status: 201, body: { run_id: "run1", html_url: "http://x" } });
86 const env = isolatedEnv({ DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy" });
87 const r = await run(["workflow", "run", "deploy.yml", "-R", "o/r", "-f", "env=prod", "-f", "dry=true"], { env });
88 expect(r.code).toBe(0);
89 const req = srv.find("POST", /dispatches$/)[0]!;
90 expect(req.path).toBe("/api/v3/repos/o/r/actions/workflows/deploy.yml/dispatches");
91 expect(req.body).toEqual({ ref: "trunk", inputs: { env: "prod", dry: "true" } });
92 });
93
94 it("parses -f pairs", () => {
95 expect(parseInputs(["a=1", "b=x=y", "c="])).toEqual({ a: "1", b: "x=y", c: "" });
96 expect(() => parseInputs(["nope"])).toThrow();
97 });
98});