| 1 | import { mkdirSync, writeFileSync } from "node:fs"; |
| 2 | import path from "node:path"; |
| 3 | import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
| 4 | import { parseInputs } from "../src/commands/workflow.js"; |
| 5 | import { fakeServer, isolatedEnv, run, tempDir, type FakeServer } from "./helpers.js"; |
| 6 | |
| 7 | const GOOD = `name: CI |
| 8 | on: [push] |
| 9 | jobs: |
| 10 | test: |
| 11 | runs-on: ubuntu-latest |
| 12 | steps: |
| 13 | - run: echo hi |
| 14 | `; |
| 15 | |
| 16 | const BAD = `name: Broken |
| 17 | on: push |
| 18 | jobs: |
| 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 | |
| 31 | describe("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 | |
| 76 | describe("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 | }); |