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
102 changes: 96 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ concurrency:

permissions:
contents: read
pull-requests: write

jobs:
# ── Change Detection ───────────────────────────────────────────────
Expand Down Expand Up @@ -84,7 +85,12 @@ jobs:
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup
- run: pnpm --filter @yavio/shared test
- run: pnpm --filter @yavio/shared exec vitest run --coverage
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: coverage-shared
path: packages/shared/coverage/

# ── Tests: db (requires PostgreSQL + ClickHouse) ───────────────────
test-db:
Expand Down Expand Up @@ -121,11 +127,16 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup
- run: pnpm --filter @yavio/shared build
- run: pnpm --filter @yavio/db test
- run: pnpm --filter @yavio/db exec vitest run --coverage
env:
DATABASE_URL: postgres://yavio_service:test@localhost:5432/yavio_test
CLICKHOUSE_URL: http://localhost:8123
CLICKHOUSE_PASSWORD: test
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: coverage-db
path: packages/db/coverage/

# ── Tests: ingest (requires ClickHouse for e2e) ────────────────────
test-ingest:
Expand All @@ -149,10 +160,15 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup
- run: pnpm turbo build --filter=@yavio/ingest...
- run: pnpm --filter @yavio/ingest test
- run: pnpm --filter @yavio/ingest exec vitest run --coverage
env:
CLICKHOUSE_URL: http://localhost:8123
CLICKHOUSE_PASSWORD: test
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: coverage-ingest
path: packages/ingest/coverage/

# ── Tests: dashboard (requires PostgreSQL + ClickHouse) ────────────
test-dashboard:
Expand Down Expand Up @@ -189,11 +205,16 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup
- run: pnpm turbo build --filter=@yavio/dashboard...
- run: pnpm --filter @yavio/dashboard test
- run: pnpm --filter @yavio/dashboard exec vitest run --coverage
env:
DATABASE_URL: postgres://yavio_service:test@localhost:5432/yavio_test
CLICKHOUSE_URL: http://localhost:8123
CLICKHOUSE_PASSWORD: test
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: coverage-dashboard
path: packages/dashboard/coverage/

# ── Tests: cli ─────────────────────────────────────────────────────
test-cli:
Expand All @@ -204,7 +225,12 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup
- run: pnpm turbo build --filter=@yavio/cli...
- run: pnpm --filter @yavio/cli test
- run: pnpm --filter @yavio/cli exec vitest run --coverage
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: coverage-cli
path: packages/cli/coverage/

# ── Tests: sdk ─────────────────────────────────────────────────────
test-sdk:
Expand All @@ -215,7 +241,71 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup
- run: pnpm turbo build --filter=@yavio/sdk...
- run: pnpm --filter @yavio/sdk test
- run: pnpm --filter @yavio/sdk exec vitest run --coverage
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: coverage-sdk
path: packages/sdk/coverage/

# ── Coverage Report (PR comment) ──────────────────────────────────
coverage-report:
if: github.event_name == 'pull_request' && always()
needs: [test-shared, test-db, test-ingest, test-dashboard, test-cli, test-sdk]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
pattern: coverage-*
merge-multiple: false

- name: Coverage report — shared
if: needs.test-shared.result == 'success'
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2.9.3
with:
name: shared
json-summary-path: coverage-shared/coverage-summary.json
json-final-path: coverage-shared/coverage-final.json

- name: Coverage report — db
if: needs.test-db.result == 'success'
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2.9.3
with:
name: db
json-summary-path: coverage-db/coverage-summary.json
json-final-path: coverage-db/coverage-final.json

- name: Coverage report — ingest
if: needs.test-ingest.result == 'success'
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2.9.3
with:
name: ingest
json-summary-path: coverage-ingest/coverage-summary.json
json-final-path: coverage-ingest/coverage-final.json

- name: Coverage report — dashboard
if: needs.test-dashboard.result == 'success'
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2.9.3
with:
name: dashboard
json-summary-path: coverage-dashboard/coverage-summary.json
json-final-path: coverage-dashboard/coverage-final.json

- name: Coverage report — cli
if: needs.test-cli.result == 'success'
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2.9.3
with:
name: cli
json-summary-path: coverage-cli/coverage-summary.json
json-final-path: coverage-cli/coverage-final.json

- name: Coverage report — sdk
if: needs.test-sdk.result == 'success'
uses: davelosert/vitest-coverage-report-action@2500dafcee7dd64f85ab689c0b83798a8359770e # v2.9.3
with:
name: sdk
json-summary-path: coverage-sdk/coverage-summary.json
json-final-path: coverage-sdk/coverage-final.json

# ── Build ──────────────────────────────────────────────────────────
build:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@vitest/coverage-v8": "^3.2.4",
"dotenv-cli": "^11.0.0",
"tsup": "^8.3.6",
"turbo": "^2.3.3",
Expand Down
137 changes: 137 additions & 0 deletions packages/cli/src/__tests__/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,142 @@ describe("docker utilities", () => {
const version = await getComposeVersion();
expect(version).toBe("2.28.0");
});

it("returns null when version format is unrecognized", async () => {
const { execa } = await import("execa");
vi.mocked(execa).mockResolvedValueOnce({
stdout: "Docker Compose unknown",
} as never);

const { getComposeVersion } = await import("../util/docker.js");
const version = await getComposeVersion();
expect(version).toBeNull();
});

it("returns null on failure", async () => {
const { execa } = await import("execa");
vi.mocked(execa).mockRejectedValueOnce(new Error("not found"));

const { getComposeVersion } = await import("../util/docker.js");
const version = await getComposeVersion();
expect(version).toBeNull();
});
});

describe("hasDockerCompose", () => {
it("returns true when docker compose is available", async () => {
const { execa } = await import("execa");
vi.mocked(execa).mockResolvedValueOnce({
stdout: "Docker Compose version v2.28.0",
} as never);

const { hasDockerCompose } = await import("../util/docker.js");
expect(await hasDockerCompose()).toBe(true);
});

it("returns false when docker compose is not available", async () => {
const { execa } = await import("execa");
vi.mocked(execa).mockRejectedValueOnce(new Error("not found"));

const { hasDockerCompose } = await import("../util/docker.js");
expect(await hasDockerCompose()).toBe(false);
});
});

describe("removeVolumes", () => {
it("calls docker volume rm with volume names", async () => {
const { execa } = await import("execa");
vi.mocked(execa).mockResolvedValueOnce({} as never);

const { removeVolumes } = await import("../util/docker.js");
await removeVolumes(["vol1", "vol2"]);

expect(execa).toHaveBeenCalledWith("docker", ["volume", "rm", "--force", "vol1", "vol2"]);
});

it("does nothing for empty array", async () => {
const { execa } = await import("execa");

const { removeVolumes } = await import("../util/docker.js");
await removeVolumes([]);

expect(execa).not.toHaveBeenCalled();
});
});

describe("getContainerStatus", () => {
it("parses container JSON output", async () => {
const { execa } = await import("execa");
const composePath = join(tempDir, "docker-compose.yml");
writeFileSync(composePath, "services: {}");
vi.mocked(execa).mockResolvedValueOnce({
stdout: '{"Name":"c1","Service":"web","State":"running","Status":"Up","Health":"healthy"}',
} as never);

const { getContainerStatus } = await import("../util/docker.js");
const result = await getContainerStatus(composePath);
expect(result).toHaveLength(1);
expect(result[0].Service).toBe("web");
});

it("returns empty array on empty stdout", async () => {
const { execa } = await import("execa");
const composePath = join(tempDir, "docker-compose.yml");
writeFileSync(composePath, "services: {}");
vi.mocked(execa).mockResolvedValueOnce({ stdout: "" } as never);

const { getContainerStatus } = await import("../util/docker.js");
const result = await getContainerStatus(composePath);
expect(result).toEqual([]);
});

it("returns empty array on failure", async () => {
const { getContainerStatus } = await import("../util/docker.js");
const result = await getContainerStatus("/nonexistent/compose.yml");
expect(result).toEqual([]);
});
});

describe("execCompose", () => {
it("uses files array when provided", async () => {
const { execa } = await import("execa");
const composePath = join(tempDir, "docker-compose.yml");
writeFileSync(composePath, "services: {}");
vi.mocked(execa).mockResolvedValueOnce({ stdout: "", stderr: "" } as never);

const { execCompose } = await import("../util/docker.js");
await execCompose(["up", "-d"], { files: [composePath] });

expect(execa).toHaveBeenCalledWith("docker", ["compose", "-f", composePath, "up", "-d"], {
stdio: "pipe",
});
});

it("passes stdio option through", async () => {
const { execa } = await import("execa");
const composePath = join(tempDir, "docker-compose.yml");
writeFileSync(composePath, "services: {}");
vi.mocked(execa).mockResolvedValueOnce({ stdout: "", stderr: "" } as never);

const { execCompose } = await import("../util/docker.js");
await execCompose(["logs"], { files: [composePath], stdio: "inherit" });

expect(execa).toHaveBeenCalledWith("docker", ["compose", "-f", composePath, "logs"], {
stdio: "inherit",
});
});
});

describe("getDockerVersion (extended)", () => {
it("returns null when version format is unrecognized", async () => {
const { execa } = await import("execa");
vi.mocked(execa).mockResolvedValueOnce({
stdout: "Docker unknown format",
} as never);

const { getDockerVersion } = await import("../util/docker.js");
const version = await getDockerVersion();
expect(version).toBeNull();
});
});
});
Loading
Loading