Compare commits

..

No commits in common. "9c117681441662d56df0321a67be35482ef7bdab" and "9a87d604e1ba0c1a0da5f11ed8611f4179211d6f" have entirely different histories.

39 changed files with 12790 additions and 5369 deletions

View File

@ -18,9 +18,8 @@
"github.vscode-github-actions",
"ms-vscode.makefile-tools",
"bierner.markdown-preview-github-styles",
"esbenp.prettier-vscode",
"biomejs.biome",
"vitest.explorer"
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh"

View File

@ -3,8 +3,6 @@ updates:
- package-ecosystem: github-actions
directory: /
open-pull-requests-limit: 20
cooldown:
default-days: 7
schedule:
interval: weekly
day: friday
@ -19,8 +17,6 @@ updates:
- "/test"
- "/"
open-pull-requests-limit: 20
cooldown:
default-days: 7
schedule:
interval: weekly
day: friday
@ -33,8 +29,6 @@ updates:
- package-ecosystem: docker-compose
directory: "/test"
open-pull-requests-limit: 20
cooldown:
default-days: 7
schedule:
interval: weekly
day: friday
@ -48,8 +42,6 @@ updates:
directory: "/"
open-pull-requests-limit: 20
versioning-strategy: increase
cooldown:
default-days: 7
schedule:
interval: weekly
day: friday
@ -70,8 +62,6 @@ updates:
- package-ecosystem: "devcontainers"
open-pull-requests-limit: 20
directory: "/"
cooldown:
default-days: 7
schedule:
interval: weekly
day: friday

View File

@ -7,7 +7,7 @@ permissions: {}
jobs:
linter:
uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
permissions:
actions: read
contents: read
@ -15,7 +15,7 @@ jobs:
statuses: write
with:
linter-env: |
FILTER_REGEX_EXCLUDE=dist/**/*|.github/social-preview.svg|.github/logo.svg
FILTER_REGEX_EXCLUDE=dist/**/*
VALIDATE_JSCPD=false
VALIDATE_TYPESCRIPT_STANDARD=false
VALIDATE_TYPESCRIPT_ES=false
@ -33,6 +33,7 @@ jobs:
packages: read
pull-requests: write
security-events: write
secrets: inherit
check-dist:
name: Test nodejs

View File

@ -3,14 +3,14 @@ name: Greetings
on:
issues:
types: [opened]
pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only reusable workflow with explicit write permissions and no checkout
pull_request_target:
branches: [main]
permissions: {}
jobs:
greetings:
uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
permissions:
contents: read
issues: write

View File

@ -27,6 +27,7 @@ jobs:
pull-requests: write
security-events: write
statuses: write
secrets: inherit
prepare-docs:
needs: ci
@ -64,9 +65,9 @@ jobs:
id: generate-token
with:
client-id: ${{ vars.CI_BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} # zizmor: ignore[secrets-outside-env] repository automation uses a dedicated app secret without untrusted code execution
private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }}
- uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
- uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
with:
github-token: ${{ steps.generate-token.outputs.token }}
branch: docs/actions-workflows-documentation-update

View File

@ -11,16 +11,15 @@ on:
description: "The SHA of the commit to get the diff for"
required: true
manual-base-ref:
description: >-
By default, the commit entered above is compared to the one directly
before it; to go back further, enter an earlier SHA here
description: "By default, the commit entered above is compared to the one directly
before it; to go back further, enter an earlier SHA here"
required: false
permissions: {}
jobs:
main:
uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
permissions:
contents: read
issues: write

View File

@ -22,3 +22,4 @@ jobs:
pull-requests: write
security-events: write
statuses: write
secrets: inherit

View File

@ -1,7 +1,7 @@
name: "Pull Request - Semantic Lint"
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] validates PR metadata only through a pinned reusable workflow without checkout
pull_request_target:
types:
- opened
- edited
@ -11,7 +11,7 @@ permissions: {}
jobs:
main:
uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
permissions:
contents: write
pull-requests: write

View File

@ -8,7 +8,7 @@ permissions: {}
jobs:
main:
uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
permissions:
issues: write
pull-requests: write

7
.gitignore vendored
View File

@ -12,6 +12,7 @@ lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
eslint-report.json
# Runtime data
pids
@ -53,6 +54,9 @@ typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
@ -98,6 +102,3 @@ __tests__/runner/*
.idea
.vscode
*.code-workspace
lint.sarif
biome-report.sarif
junit.xml

View File

@ -1,5 +1,12 @@
FROM ghcr.io/hoverkraft-tech/docker-base-images/super-linter:0.6.0
FROM ghcr.io/super-linter/super-linter:slim-v8.0.0
HEALTHCHECK --interval=5m --timeout=10s --start-period=30s --retries=3 CMD ["/bin/sh","-c","test -d /github/home"]
ARG UID=1000
ARG GID=1000
RUN chown -R ${UID}:${GID} /github/home
USER ${UID}:${GID}
ENV RUN_LOCAL=true
ENV USE_FIND_ALGORITHM=true
ENV LOG_LEVEL=WARN
ENV LOG_FILE="/github/home/logs"

View File

@ -7,13 +7,13 @@ lint: ## Execute linting
$(call run_linter,)
lint-fix: ## Execute linting and fix
@npm run format
$(call run_linter, \
-e FIX_JSON_PRETTIER=true \
-e FIX_JAVASCRIPT_PRETTIER=true \
-e FIX_YAML_PRETTIER=true \
-e FIX_MARKDOWN=true \
-e FIX_MARKDOWN_PRETTIER=true \
-e FIX_NATURAL_LANGUAGE=true \
-e FIX_SHELL_SHFMT=true \
-e FIX_BIOME_LINT=true \
-e FIX_BIOME_FORMAT=true \
)
ci: ## Execute all formats and checks
@ -29,8 +29,12 @@ define run_linter
docker build --build-arg UID=$(shell id -u) --build-arg GID=$(shell id -g) --tag $$LINTER_IMAGE .; \
docker run \
-e DEFAULT_WORKSPACE="$$DEFAULT_WORKSPACE" \
-e FILTER_REGEX_EXCLUDE="dist/**/*|.github/social-preview.svg|.github/logo.svg" \
-e FILTER_REGEX_INCLUDE="$(filter-out $@,$(MAKECMDGOALS))" \
-e IGNORE_GITIGNORED_FILES=true \
-e FILTER_REGEX_EXCLUDE=dist/**/* \
-e VALIDATE_TYPESCRIPT_ES=false \
-e VALIDATE_TYPESCRIPT_PRETTIER=false \
-e VALIDATE_JAVASCRIPT_ES=false \
$(1) \
-v $$VOLUME \
--rm \

View File

@ -1,34 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

23
dist/index.js generated vendored
View File

@ -43860,11 +43860,8 @@ class InputService {
}) || null);
}
getServiceLogLevel() {
const configuredLevel = getInput(InputNames.ServiceLogLevel, {
required: false,
});
if (configuredLevel &&
!Object.values(LogLevel).includes(configuredLevel)) {
const configuredLevel = getInput(InputNames.ServiceLogLevel, { required: false });
if (configuredLevel && !Object.values(LogLevel).includes(configuredLevel)) {
throw new Error(`Invalid service log level "${configuredLevel}". Valid values are: ${Object.values(LogLevel).join(", ")}`);
}
return configuredLevel || LogLevel.Debug;
@ -43946,12 +43943,12 @@ class DockerComposeService {
parts.push("Docker Compose command failed");
}
// Add error stream output if available
if (error.err?.trim()) {
if (error.err && error.err.trim()) {
parts.push("\nError output:");
parts.push(error.err.trim());
}
// Add standard output if available and different from error output
if (error.out?.trim() && error.out !== error.err) {
if (error.out && error.out.trim() && error.out !== error.err) {
parts.push("\nStandard output:");
parts.push(error.out.trim());
}
@ -48240,17 +48237,14 @@ class DockerComposeInstallerService {
constructor(manualInstallerAdapter) {
this.manualInstallerAdapter = manualInstallerAdapter;
}
async install({ composeVersion, cwd, githubToken, }) {
async install({ composeVersion, cwd, githubToken }) {
const currentVersion = await this.version({ cwd });
const normalizedCurrentVersion = currentVersion
? this.normalizeVersion(currentVersion)
: null;
const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null;
const normalizedRequestedVersion = composeVersion
? this.normalizeVersion(composeVersion)
: null;
const needsInstall = !currentVersion ||
(composeVersion &&
normalizedRequestedVersion !== normalizedCurrentVersion);
(composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion);
if (!needsInstall) {
return currentVersion;
}
@ -48264,8 +48258,7 @@ class DockerComposeInstallerService {
await this.installVersion(targetVersion);
const installedVersion = await this.version({ cwd });
if (!installedVersion ||
this.normalizeVersion(installedVersion) !==
this.normalizeVersion(targetVersion)) {
this.normalizeVersion(installedVersion) !== this.normalizeVersion(targetVersion)) {
throw new Error(`Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"`);
}
return installedVersion;

11
dist/post.js generated vendored
View File

@ -40112,11 +40112,8 @@ class InputService {
}) || null);
}
getServiceLogLevel() {
const configuredLevel = getInput(InputNames.ServiceLogLevel, {
required: false,
});
if (configuredLevel &&
!Object.values(LogLevel).includes(configuredLevel)) {
const configuredLevel = getInput(InputNames.ServiceLogLevel, { required: false });
if (configuredLevel && !Object.values(LogLevel).includes(configuredLevel)) {
throw new Error(`Invalid service log level "${configuredLevel}". Valid values are: ${Object.values(LogLevel).join(", ")}`);
}
return configuredLevel || LogLevel.Debug;
@ -40198,12 +40195,12 @@ class DockerComposeService {
parts.push("Docker Compose command failed");
}
// Add error stream output if available
if (error.err?.trim()) {
if (error.err && error.err.trim()) {
parts.push("\nError output:");
parts.push(error.err.trim());
}
// Add standard output if available and different from error output
if (error.out?.trim() && error.out !== error.err) {
if (error.out && error.out.trim() && error.out !== error.err) {
parts.push("\nStandard output:");
parts.push(error.out.trim());
}

3
eslint.config.mjs Normal file
View File

@ -0,0 +1,3 @@
import tsDevToolsCore from "@ts-dev-tools/core/dist/eslint-plugin-ts-dev-tools/index.js";
export default tsDevToolsCore.default;

10113
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,40 +25,102 @@
"node": ">=20"
},
"dependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1",
"@actions/core": "^3.0.0",
"@actions/github": "^9.1.0",
"@actions/tool-cache": "^4.0.0",
"@octokit/action": "^8.0.4",
"docker-compose": "^1.4.2"
},
"devDependencies": {
"@ts-dev-tools/core": "^1.12.4",
"@vercel/ncc": "^0.38.4"
"@ts-dev-tools/core": "^1.12.0",
"@vercel/ncc": "^0.38.4",
"eslint-plugin-github": "^6.0.0",
"eslint-plugin-jsonc": "^3.1.2"
},
"scripts": {
"package": "npm run package:index && npm run package:post",
"package:index": "ncc build src/index.ts -o dist --license licenses.txt",
"package:post": "ncc build src/post.ts -o dist/post && mv dist/post/index.js dist/post.js && rm -rf dist/post",
"package:watch": "npm run package -- --watch",
"lint": "biome lint --error-on-warnings .",
"lint:ci": "biome lint --error-on-warnings . --reporter=sarif | tee biome-report.sarif",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"lint:ci": "npm run lint -- --output-file eslint-report.json --format json",
"all": "npm run format && npm run lint:ci && npm run test:ci && npm run package",
"build": "tsc --noEmit",
"format": "biome format --write .",
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --reporter=default --reporter=junit --outputFile=junit.xml --coverage.enabled --coverage.reporter=lcov --coverage.reporter=text",
"test:ci": "npm run test:cov",
"prepare": "ts-dev-tools install",
"check": "biome check --error-on-warnings --write .",
"vitest": "vitest"
"format": "prettier --cache --write .",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --forceExit --maxWorkers=50%",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --maxWorkers=25%",
"test:cov": "npm run test -- --coverage",
"test:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --forceExit",
"prepare": "ts-dev-tools install"
},
"jest": {
"preset": "ts-jest/presets/default-esm",
"verbose": true,
"clearMocks": true,
"testEnvironment": "node",
"extensionsToTreatAsEsm": [
".ts"
],
"moduleFileExtensions": [
"js",
"ts"
],
"testMatch": [
"**/*.test.ts",
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test)?(.*).+(ts|tsx|js)"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
},
"transform": {
"^.+\\.ts$": [
"ts-jest",
{
"useESM": true,
"tsconfig": "tsconfig.json"
}
]
},
"injectGlobals": true,
"coverageReporters": [
"json-summary",
"text",
"lcov"
],
"collectCoverage": true,
"collectCoverageFrom": [
"./src/**",
"**/src/**/*.[jt]s?(x)"
]
},
"prettier": {
"semi": true,
"printWidth": 100,
"trailingComma": "es5",
"plugins": []
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix"
]
},
"importSort": {
".js, .jsx, .ts, .tsx": {
"style": "module",
"parser": "typescript"
}
},
"tsDevTools": {
"version": "20260604100000-migrate-to-vitest"
"version": "20250623095600-remove-prettier-oxc"
}
}

View File

@ -1,38 +1,38 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
// Mock @actions/core
const setFailedMock = vi.fn();
const setFailedMock = jest.fn();
vi.doMock("@actions/core", () => ({
jest.unstable_mockModule("@actions/core", () => ({
setFailed: setFailedMock,
getInput: vi.fn().mockReturnValue(""),
getMultilineInput: vi.fn().mockReturnValue([]),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
getInput: jest.fn().mockReturnValue(""),
getMultilineInput: jest.fn().mockReturnValue([]),
debug: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
}));
// Mock docker-compose
vi.doMock("docker-compose", () => ({
upAll: vi.fn(),
upMany: vi.fn(),
down: vi.fn(),
logs: vi.fn(),
version: vi
jest.unstable_mockModule("docker-compose", () => ({
upAll: jest.fn(),
upMany: jest.fn(),
down: jest.fn(),
logs: jest.fn(),
version: jest
.fn<() => Promise<{ data: { version: string } }>>()
.mockResolvedValue({ data: { version: "1.2.3" } }),
}));
// Mock node:fs
vi.doMock("node:fs", async () => {
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
jest.unstable_mockModule("node:fs", async () => {
const actualFs = await jest.requireActual<typeof import("node:fs")>("node:fs");
return {
...actualFs,
existsSync: vi.fn().mockReturnValue(true),
existsSync: jest.fn().mockReturnValue(true),
default: {
...actualFs,
existsSync: vi.fn().mockReturnValue(true),
existsSync: jest.fn().mockReturnValue(true),
},
};
});
@ -40,35 +40,26 @@ vi.doMock("node:fs", async () => {
// Dynamic imports after mock setup
const { run } = await import("./index-runner.js");
const { InputService } = await import("./services/input.service.js");
const { LoggerService, LogLevel } = await import(
"./services/logger.service.js"
);
const { DockerComposeInstallerService } = await import(
"./services/docker-compose-installer.service.js"
);
const { DockerComposeService } = await import(
"./services/docker-compose.service.js"
);
const { LoggerService, LogLevel } = await import("./services/logger.service.js");
const { DockerComposeInstallerService } =
await import("./services/docker-compose-installer.service.js");
const { DockerComposeService } = await import("./services/docker-compose.service.js");
describe("run", () => {
let infoMock: ReturnType<typeof vi.spyOn>;
let debugMock: ReturnType<typeof vi.spyOn>;
let getInputsMock: ReturnType<typeof vi.spyOn>;
let installMock: ReturnType<typeof vi.spyOn>;
let upMock: ReturnType<typeof vi.spyOn>;
let infoMock: jest.SpiedFunction<typeof LoggerService.prototype.info>;
let debugMock: jest.SpiedFunction<typeof LoggerService.prototype.debug>;
let getInputsMock: jest.SpiedFunction<typeof InputService.prototype.getInputs>;
let installMock: jest.SpiedFunction<typeof DockerComposeInstallerService.prototype.install>;
let upMock: jest.SpiedFunction<typeof DockerComposeService.prototype.up>;
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
infoMock = vi
.spyOn(LoggerService.prototype, "info")
.mockImplementation(() => {});
debugMock = vi
.spyOn(LoggerService.prototype, "debug")
.mockImplementation(() => {});
getInputsMock = vi.spyOn(InputService.prototype, "getInputs");
installMock = vi.spyOn(DockerComposeInstallerService.prototype, "install");
upMock = vi.spyOn(DockerComposeService.prototype, "up");
infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(() => {});
debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(() => {});
getInputsMock = jest.spyOn(InputService.prototype, "getInputs");
installMock = jest.spyOn(DockerComposeInstallerService.prototype, "install");
upMock = jest.spyOn(DockerComposeService.prototype, "up");
});
it("should install docker compose with specified version", async () => {
@ -94,12 +85,10 @@ describe("run", () => {
await run();
// Assert
expect(infoMock).toHaveBeenCalledWith(
"Setting up docker compose version 1.29.2",
);
expect(infoMock).toHaveBeenCalledWith("Setting up docker compose version 1.29.2");
expect(debugMock).toHaveBeenCalledWith(
'inputs: {"dockerFlags":[],"composeFiles":["docker-compose.yml"],"services":[],"composeFlags":[],"upFlags":[],"downFlags":[],"cwd":"/current/working/dir","composeVersion":"1.29.2","githubToken":null,"serviceLogLevel":"debug"}',
'inputs: {"dockerFlags":[],"composeFiles":["docker-compose.yml"],"services":[],"composeFlags":[],"upFlags":[],"downFlags":[],"cwd":"/current/working/dir","composeVersion":"1.29.2","githubToken":null,"serviceLogLevel":"debug"}'
);
expect(installMock).toHaveBeenCalledWith({

View File

@ -14,7 +14,7 @@ export async function run(): Promise<void> {
const loggerService = new LoggerService();
const inputService = new InputService();
const dockerComposeInstallerService = new DockerComposeInstallerService(
new ManualInstallerAdapter(),
new ManualInstallerAdapter()
);
const dockerComposeService = new DockerComposeService();
@ -23,7 +23,7 @@ export async function run(): Promise<void> {
loggerService.info(
"Setting up docker compose" +
(inputs.composeVersion ? ` version ${inputs.composeVersion}` : ""),
(inputs.composeVersion ? ` version ${inputs.composeVersion}` : "")
);
const installedVersion = await dockerComposeInstallerService.install({

View File

@ -1,73 +1,64 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
// Mock @actions/core
const setFailedMock = vi.fn();
const setFailedMock = jest.fn();
vi.doMock("@actions/core", () => ({
jest.unstable_mockModule("@actions/core", () => ({
setFailed: setFailedMock,
getInput: vi.fn().mockReturnValue(""),
getMultilineInput: vi.fn().mockReturnValue([]),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
getInput: jest.fn().mockReturnValue(""),
getMultilineInput: jest.fn().mockReturnValue([]),
debug: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
}));
// Mock docker-compose
vi.doMock("docker-compose", () => ({
upAll: vi.fn(),
upMany: vi.fn(),
down: vi.fn(),
logs: vi.fn(),
version: vi
jest.unstable_mockModule("docker-compose", () => ({
upAll: jest.fn(),
upMany: jest.fn(),
down: jest.fn(),
logs: jest.fn(),
version: jest
.fn<() => Promise<{ data: { version: string } }>>()
.mockResolvedValue({ data: { version: "1.2.3" } }),
}));
// Mock node:fs
vi.doMock("node:fs", async () => {
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
jest.unstable_mockModule("node:fs", async () => {
const actualFs = await jest.requireActual<typeof import("node:fs")>("node:fs");
return {
...actualFs,
existsSync: vi.fn().mockReturnValue(true),
existsSync: jest.fn().mockReturnValue(true),
default: {
...actualFs,
existsSync: vi.fn().mockReturnValue(true),
existsSync: jest.fn().mockReturnValue(true),
},
};
});
// Dynamic imports after mock setup
const { InputService } = await import("./services/input.service.js");
const { LoggerService, LogLevel } = await import(
"./services/logger.service.js"
);
const { DockerComposeInstallerService } = await import(
"./services/docker-compose-installer.service.js"
);
const { DockerComposeService } = await import(
"./services/docker-compose.service.js"
);
const { LoggerService, LogLevel } = await import("./services/logger.service.js");
const { DockerComposeInstallerService } =
await import("./services/docker-compose-installer.service.js");
const { DockerComposeService } = await import("./services/docker-compose.service.js");
let getInputsMock: ReturnType<typeof vi.spyOn>;
let debugMock: ReturnType<typeof vi.spyOn>;
let infoMock: ReturnType<typeof vi.spyOn>;
let installMock: ReturnType<typeof vi.spyOn>;
let upMock: ReturnType<typeof vi.spyOn>;
let getInputsMock: jest.SpiedFunction<typeof InputService.prototype.getInputs>;
let debugMock: jest.SpiedFunction<typeof LoggerService.prototype.debug>;
let infoMock: jest.SpiedFunction<typeof LoggerService.prototype.info>;
let installMock: jest.SpiedFunction<typeof DockerComposeInstallerService.prototype.install>;
let upMock: jest.SpiedFunction<typeof DockerComposeService.prototype.up>;
describe("index", () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
infoMock = vi
.spyOn(LoggerService.prototype, "info")
.mockImplementation(() => {});
debugMock = vi
.spyOn(LoggerService.prototype, "debug")
.mockImplementation(() => {});
getInputsMock = vi.spyOn(InputService.prototype, "getInputs");
installMock = vi.spyOn(DockerComposeInstallerService.prototype, "install");
upMock = vi.spyOn(DockerComposeService.prototype, "up");
infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(() => {});
debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(() => {});
getInputsMock = jest.spyOn(InputService.prototype, "getInputs");
installMock = jest.spyOn(DockerComposeInstallerService.prototype, "install");
upMock = jest.spyOn(DockerComposeService.prototype, "up");
});
it("calls run when imported", async () => {
@ -91,21 +82,15 @@ describe("index", () => {
await new Promise((resolve) => setTimeout(resolve, 0));
expect(infoMock).toHaveBeenNthCalledWith(1, "Setting up docker compose");
expect(infoMock).toHaveBeenNthCalledWith(
2,
"docker compose version: 1.2.3",
);
expect(infoMock).toHaveBeenNthCalledWith(2, "docker compose version: 1.2.3");
// Verify that all of the functions were called correctly
expect(debugMock).toHaveBeenNthCalledWith(
1,
'inputs: {"dockerFlags":[],"composeFiles":["docker-compose.yml"],"services":[],"composeFlags":[],"upFlags":[],"downFlags":[],"cwd":"/current/working/dir","composeVersion":null,"githubToken":null,"serviceLogLevel":"debug"}',
'inputs: {"dockerFlags":[],"composeFiles":["docker-compose.yml"],"services":[],"composeFlags":[],"upFlags":[],"downFlags":[],"cwd":"/current/working/dir","composeVersion":null,"githubToken":null,"serviceLogLevel":"debug"}'
);
expect(infoMock).toHaveBeenNthCalledWith(
3,
"Bringing up docker compose service(s)",
);
expect(infoMock).toHaveBeenNthCalledWith(3, "Bringing up docker compose service(s)");
expect(upMock).toHaveBeenCalledWith({
dockerFlags: [],
@ -119,9 +104,6 @@ describe("index", () => {
expect(setFailedMock).not.toHaveBeenCalled();
expect(infoMock).toHaveBeenNthCalledWith(
4,
"docker compose service(s) are up",
);
expect(infoMock).toHaveBeenNthCalledWith(4, "docker compose service(s) are up");
});
});

View File

@ -1,63 +1,55 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
// Mock @actions/core
const setFailedMock = vi.fn();
const setFailedMock = jest.fn();
vi.doMock("@actions/core", () => ({
jest.unstable_mockModule("@actions/core", () => ({
setFailed: setFailedMock,
getInput: vi.fn().mockReturnValue(""),
getMultilineInput: vi.fn().mockReturnValue([]),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
getInput: jest.fn().mockReturnValue(""),
getMultilineInput: jest.fn().mockReturnValue([]),
debug: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
}));
// Mock docker-compose
const logsMock = vi.fn();
const downMock = vi.fn();
const logsMock = jest.fn();
const downMock = jest.fn();
vi.doMock("docker-compose", () => ({
jest.unstable_mockModule("docker-compose", () => ({
logs: logsMock,
down: downMock,
upAll: vi.fn(),
upMany: vi.fn(),
upAll: jest.fn(),
upMany: jest.fn(),
}));
// Mock node:fs
vi.doMock("node:fs", () => ({
existsSync: vi.fn().mockReturnValue(true),
default: { existsSync: vi.fn().mockReturnValue(true) },
jest.unstable_mockModule("node:fs", () => ({
existsSync: jest.fn().mockReturnValue(true),
default: { existsSync: jest.fn().mockReturnValue(true) },
}));
// Dynamic imports after mock setup
const { run } = await import("./post-runner.js");
const { InputService } = await import("./services/input.service.js");
const { LoggerService, LogLevel } = await import(
"./services/logger.service.js"
);
const { DockerComposeService } = await import(
"./services/docker-compose.service.js"
);
const { LoggerService, LogLevel } = await import("./services/logger.service.js");
const { DockerComposeService } = await import("./services/docker-compose.service.js");
describe("run", () => {
let infoMock: ReturnType<typeof vi.spyOn>;
let debugMock: ReturnType<typeof vi.spyOn>;
let getInputsMock: ReturnType<typeof vi.spyOn>;
let serviceDownMock: ReturnType<typeof vi.spyOn>;
let serviceLogsMock: ReturnType<typeof vi.spyOn>;
let infoMock: jest.SpiedFunction<typeof LoggerService.prototype.info>;
let debugMock: jest.SpiedFunction<typeof LoggerService.prototype.debug>;
let getInputsMock: jest.SpiedFunction<typeof InputService.prototype.getInputs>;
let serviceDownMock: jest.SpiedFunction<typeof DockerComposeService.prototype.down>;
let serviceLogsMock: jest.SpiedFunction<typeof DockerComposeService.prototype.logs>;
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
infoMock = vi
.spyOn(LoggerService.prototype, "info")
.mockImplementation(() => {});
debugMock = vi
.spyOn(LoggerService.prototype, "debug")
.mockImplementation(() => {});
getInputsMock = vi.spyOn(InputService.prototype, "getInputs");
serviceDownMock = vi.spyOn(DockerComposeService.prototype, "down");
serviceLogsMock = vi.spyOn(DockerComposeService.prototype, "logs");
infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(() => {});
debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(() => {});
getInputsMock = jest.spyOn(InputService.prototype, "getInputs");
serviceDownMock = jest.spyOn(DockerComposeService.prototype, "down");
serviceLogsMock = jest.spyOn(DockerComposeService.prototype, "logs");
});
it("should bring down docker compose service(s) and log output", async () => {
@ -131,12 +123,8 @@ describe("run", () => {
await run();
// Assert
expect(debugMock).toHaveBeenCalledWith(
"docker compose error:\ntest logs error",
);
expect(debugMock).toHaveBeenCalledWith(
"docker compose logs:\ntest logs output",
);
expect(debugMock).toHaveBeenCalledWith("docker compose error:\ntest logs error");
expect(debugMock).toHaveBeenCalledWith("docker compose logs:\ntest logs output");
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
});

View File

@ -1,62 +1,54 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
// Mock @actions/core
const setFailedMock = vi.fn();
const setFailedMock = jest.fn();
vi.doMock("@actions/core", () => ({
jest.unstable_mockModule("@actions/core", () => ({
setFailed: setFailedMock,
getInput: vi.fn().mockReturnValue(""),
getMultilineInput: vi.fn().mockReturnValue([]),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
getInput: jest.fn().mockReturnValue(""),
getMultilineInput: jest.fn().mockReturnValue([]),
debug: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
}));
// Mock docker-compose
const logsMock = vi.fn();
const downMock = vi.fn();
const logsMock = jest.fn();
const downMock = jest.fn();
vi.doMock("docker-compose", () => ({
jest.unstable_mockModule("docker-compose", () => ({
logs: logsMock,
down: downMock,
upAll: vi.fn(),
upMany: vi.fn(),
upAll: jest.fn(),
upMany: jest.fn(),
}));
// Mock node:fs
vi.doMock("node:fs", () => ({
existsSync: vi.fn().mockReturnValue(true),
default: { existsSync: vi.fn().mockReturnValue(true) },
jest.unstable_mockModule("node:fs", () => ({
existsSync: jest.fn().mockReturnValue(true),
default: { existsSync: jest.fn().mockReturnValue(true) },
}));
// Dynamic imports after mock setup
const { InputService } = await import("./services/input.service.js");
const { LoggerService, LogLevel } = await import(
"./services/logger.service.js"
);
const { DockerComposeService } = await import(
"./services/docker-compose.service.js"
);
const { LoggerService, LogLevel } = await import("./services/logger.service.js");
const { DockerComposeService } = await import("./services/docker-compose.service.js");
let getInputsMock: ReturnType<typeof vi.spyOn>;
let debugMock: ReturnType<typeof vi.spyOn>;
let infoMock: ReturnType<typeof vi.spyOn>;
let serviceLogsMock: ReturnType<typeof vi.spyOn>;
let serviceDownMock: ReturnType<typeof vi.spyOn>;
let getInputsMock: jest.SpiedFunction<typeof InputService.prototype.getInputs>;
let debugMock: jest.SpiedFunction<typeof LoggerService.prototype.debug>;
let infoMock: jest.SpiedFunction<typeof LoggerService.prototype.info>;
let serviceLogsMock: jest.SpiedFunction<typeof DockerComposeService.prototype.logs>;
let serviceDownMock: jest.SpiedFunction<typeof DockerComposeService.prototype.down>;
describe("post", () => {
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
infoMock = vi
.spyOn(LoggerService.prototype, "info")
.mockImplementation(() => {});
debugMock = vi
.spyOn(LoggerService.prototype, "debug")
.mockImplementation(() => {});
getInputsMock = vi.spyOn(InputService.prototype, "getInputs");
serviceLogsMock = vi.spyOn(DockerComposeService.prototype, "logs");
serviceDownMock = vi.spyOn(DockerComposeService.prototype, "down");
infoMock = jest.spyOn(LoggerService.prototype, "info").mockImplementation(() => {});
debugMock = jest.spyOn(LoggerService.prototype, "debug").mockImplementation(() => {});
getInputsMock = jest.spyOn(InputService.prototype, "getInputs");
serviceLogsMock = jest.spyOn(DockerComposeService.prototype, "logs");
serviceDownMock = jest.spyOn(DockerComposeService.prototype, "down");
});
it("calls run when imported", async () => {
@ -97,10 +89,7 @@ describe("post", () => {
serviceLogger: debugMock,
});
expect(debugMock).toHaveBeenNthCalledWith(
1,
"docker compose logs:\ntest logs",
);
expect(debugMock).toHaveBeenNthCalledWith(1, "docker compose logs:\ntest logs");
expect(infoMock).toHaveBeenNthCalledWith(1, "docker compose is down");
expect(setFailedMock).not.toHaveBeenCalled();

View File

@ -1,24 +1,21 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
import type { IDockerComposeResult } from "docker-compose";
import { MockAgent, setGlobalDispatcher } from "undici";
// Mock docker-compose before importing the module under test
const versionMock =
vi.fn<() => Promise<IDockerComposeResult & { data: { version: string } }>>();
const versionMock = jest.fn<() => Promise<IDockerComposeResult & { data: { version: string } }>>();
vi.doMock("docker-compose", () => ({
jest.unstable_mockModule("docker-compose", () => ({
version: versionMock,
}));
// Create manual installer adapter mock
const manualInstallerAdapterMock = {
install: vi.fn<(version: string) => Promise<void>>(),
install: jest.fn<(version: string) => Promise<void>>(),
};
// Dynamic import after mock setup
const { DockerComposeInstallerService } = await import(
"./docker-compose-installer.service.js"
);
const { DockerComposeInstallerService } = await import("./docker-compose-installer.service.js");
describe("DockerComposeInstallerService", () => {
let mockAgent: MockAgent;
@ -33,10 +30,7 @@ describe("DockerComposeInstallerService", () => {
},
});
const installCompose = (
composeVersion: string | null,
githubToken: string | null,
) =>
const installCompose = (composeVersion: string | null, githubToken: string | null) =>
service.install({
composeVersion,
cwd: "/path/to/cwd",
@ -65,26 +59,24 @@ describe("DockerComposeInstallerService", () => {
headers: {
"content-type": "application/json",
},
},
}
);
setGlobalDispatcher(mockClient);
Object.defineProperty(globalThis, "fetch", {
value: vi.fn(),
value: jest.fn(),
});
};
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
service = new DockerComposeInstallerService(
manualInstallerAdapterMock as never,
);
service = new DockerComposeInstallerService(manualInstallerAdapterMock as never);
});
afterEach(() => {
vi.resetAllMocks();
jest.resetAllMocks();
});
describe("install", () => {
@ -102,9 +94,7 @@ describe("DockerComposeInstallerService", () => {
// Assert
expect(result).toBe(latestVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
latestVersion,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
});
it("should return current version when no version is provided", async () => {
@ -136,9 +126,7 @@ describe("DockerComposeInstallerService", () => {
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
const expectedVersion = "1.3.0";
versionMock.mockResolvedValueOnce(
composeVersionResponse(expectedVersion),
);
versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion));
setPlatform("linux");
// Act
@ -146,9 +134,7 @@ describe("DockerComposeInstallerService", () => {
// Assert
expect(result).toBe(expectedVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
expectedVersion,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(expectedVersion);
});
it("should install the latest version if requested", async () => {
@ -165,9 +151,7 @@ describe("DockerComposeInstallerService", () => {
// Assert
expect(result).toBe(latestVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
latestVersion,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
});
it("should throw an error if the latest version if requested and no Github token is provided", async () => {
@ -176,7 +160,7 @@ describe("DockerComposeInstallerService", () => {
// Act & Assert
await expect(installCompose("latest", null)).rejects.toThrow(
"GitHub token is required to install the latest version",
"GitHub token is required to install the latest version"
);
});
@ -185,14 +169,12 @@ describe("DockerComposeInstallerService", () => {
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
const expectedVersion = "1.3.0";
versionMock.mockResolvedValueOnce(
composeVersionResponse(expectedVersion),
);
versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion));
setPlatform("win32");
// Act & Assert
await expect(installCompose(expectedVersion, null)).rejects.toThrow(
`Unsupported platform: win32`,
`Unsupported platform: win32`
);
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
@ -205,9 +187,7 @@ describe("DockerComposeInstallerService", () => {
const installedVersion = "2.0.0";
// After installation, version() returns the new version
versionMock.mockResolvedValueOnce(
composeVersionResponse(installedVersion),
);
versionMock.mockResolvedValueOnce(composeVersionResponse(installedVersion));
setPlatform("linux");
// Act
@ -215,9 +195,7 @@ describe("DockerComposeInstallerService", () => {
// Assert
expect(result).toBe(installedVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
installedVersion,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(installedVersion);
});
it("should install latest version when missing or unspecified", async () => {
@ -236,9 +214,7 @@ describe("DockerComposeInstallerService", () => {
// Assert
expect(result).toBe(latestVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
latestVersion,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
});
it("should throw if Compose is missing and no GitHub token is provided", async () => {
@ -247,7 +223,7 @@ describe("DockerComposeInstallerService", () => {
setPlatform("linux");
await expect(installCompose("latest", null)).rejects.toThrow(
"GitHub token is required to install the latest version",
"GitHub token is required to install the latest version"
);
});
@ -273,11 +249,9 @@ describe("DockerComposeInstallerService", () => {
// Act & Assert
await expect(installCompose(targetVersion, "token")).rejects.toThrow(
`Failed to install Docker Compose version "${targetVersion}", installed version is "1.3.0"`,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
targetVersion,
`Failed to install Docker Compose version "${targetVersion}", installed version is "1.3.0"`
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(targetVersion);
});
it("should throw with unknown installed version when post-install version check fails", async () => {
@ -285,18 +259,14 @@ describe("DockerComposeInstallerService", () => {
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
const targetVersion = "v1.4.0";
versionMock.mockRejectedValueOnce(
new Error("version check failed after install"),
);
versionMock.mockRejectedValueOnce(new Error("version check failed after install"));
setPlatform("linux");
// Act & Assert
await expect(installCompose(targetVersion, "token")).rejects.toThrow(
`Failed to install Docker Compose version "${targetVersion}", installed version is "unknown"`,
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
targetVersion,
`Failed to install Docker Compose version "${targetVersion}", installed version is "unknown"`
);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(targetVersion);
});
});
});

View File

@ -1,7 +1,7 @@
import * as github from "@actions/github";
import { version } from "docker-compose";
import { COMPOSE_VERSION_LATEST, type Inputs } from "./input.service.js";
import type { ManualInstallerAdapter } from "./installer-adapter/manual-installer-adapter.js";
import { COMPOSE_VERSION_LATEST, Inputs } from "./input.service.js";
import { ManualInstallerAdapter } from "./installer-adapter/manual-installer-adapter.js";
export type InstallInputs = {
composeVersion: Inputs["composeVersion"];
@ -14,28 +14,19 @@ export type VersionInputs = {
};
export class DockerComposeInstallerService {
constructor(
private readonly manualInstallerAdapter: ManualInstallerAdapter,
) {}
constructor(private readonly manualInstallerAdapter: ManualInstallerAdapter) {}
async install({
composeVersion,
cwd,
githubToken,
}: InstallInputs): Promise<string> {
async install({ composeVersion, cwd, githubToken }: InstallInputs): Promise<string> {
const currentVersion = await this.version({ cwd });
const normalizedCurrentVersion = currentVersion
? this.normalizeVersion(currentVersion)
: null;
const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null;
const normalizedRequestedVersion = composeVersion
? this.normalizeVersion(composeVersion)
: null;
const needsInstall =
!currentVersion ||
(composeVersion &&
normalizedRequestedVersion !== normalizedCurrentVersion);
(composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion);
if (!needsInstall) {
return currentVersion;
}
@ -44,9 +35,7 @@ export class DockerComposeInstallerService {
if (targetVersion === COMPOSE_VERSION_LATEST) {
if (!githubToken) {
throw new Error(
"GitHub token is required to install the latest version",
);
throw new Error("GitHub token is required to install the latest version");
}
targetVersion = await this.getLatestVersion(githubToken);
}
@ -57,11 +46,10 @@ export class DockerComposeInstallerService {
if (
!installedVersion ||
this.normalizeVersion(installedVersion) !==
this.normalizeVersion(targetVersion)
this.normalizeVersion(installedVersion) !== this.normalizeVersion(targetVersion)
) {
throw new Error(
`Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"`,
`Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"`
);
}

View File

@ -1,4 +1,4 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
import type {
IDockerComposeLogOptions,
IDockerComposeOptions,
@ -6,26 +6,16 @@ import type {
} from "docker-compose";
// Mock docker-compose before importing the module under test
const upAllMock =
vi.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
const upAllMock = jest.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
const upManyMock =
vi.fn<
(
services: string[],
options: IDockerComposeOptions,
) => Promise<IDockerComposeResult>
>();
const downMock =
vi.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
jest.fn<(services: string[], options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
const downMock = jest.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
const logsMock =
vi.fn<
(
services: string[],
options: IDockerComposeLogOptions,
) => Promise<IDockerComposeResult>
jest.fn<
(services: string[], options: IDockerComposeLogOptions) => Promise<IDockerComposeResult>
>();
vi.doMock("docker-compose", () => ({
jest.unstable_mockModule("docker-compose", () => ({
upAll: upAllMock,
upMany: upManyMock,
down: downMock,
@ -39,7 +29,7 @@ describe("DockerComposeService", () => {
let service: InstanceType<typeof DockerComposeService>;
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
service = new DockerComposeService();
});
@ -52,7 +42,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
upAllMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
@ -72,8 +62,7 @@ describe("DockerComposeService", () => {
});
// Ensure callback is calling the service logger
const callback = (upAllMock.mock.calls[0][0] as IDockerComposeOptions)
?.callback;
const callback = (upAllMock.mock.calls[0][0] as IDockerComposeOptions)?.callback;
expect(callback).toBeDefined();
const message = "test log output";
@ -93,7 +82,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
upAllMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
@ -121,7 +110,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: ["--build"],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
upManyMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
@ -149,7 +138,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const dockerComposeError = {
@ -161,11 +150,9 @@ describe("DockerComposeService", () => {
upAllMock.mockRejectedValue(dockerComposeError);
await expect(service.up(upInputs)).rejects.toThrow(
"Docker Compose command failed with exit code 1",
);
await expect(service.up(upInputs)).rejects.toThrow(
"unable to pull image",
"Docker Compose command failed with exit code 1"
);
await expect(service.up(upInputs)).rejects.toThrow("unable to pull image");
});
it("should throw formatted error when upMany fails with docker-compose result", async () => {
@ -176,7 +163,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const dockerComposeError = {
@ -188,11 +175,9 @@ describe("DockerComposeService", () => {
upManyMock.mockRejectedValue(dockerComposeError);
await expect(service.up(upInputs)).rejects.toThrow(
"Docker Compose command failed with exit code 1",
);
await expect(service.up(upInputs)).rejects.toThrow(
"Service 'web' failed to start",
"Docker Compose command failed with exit code 1"
);
await expect(service.up(upInputs)).rejects.toThrow("Service 'web' failed to start");
await expect(service.up(upInputs)).rejects.toThrow("Starting web...");
});
@ -204,7 +189,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const dockerComposeError = {
@ -215,37 +200,7 @@ describe("DockerComposeService", () => {
upAllMock.mockRejectedValue(dockerComposeError);
await expect(service.up(upInputs)).rejects.toThrow(
"Some error without exit code",
);
});
it("should format docker-compose result when streams are undefined", async () => {
const upInputs = {
dockerFlags: [] as string[],
composeFiles: ["docker-compose.yml"],
services: [] as string[],
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
};
const dockerComposeError = {
exitCode: 1,
err: undefined,
out: undefined,
};
upAllMock.mockRejectedValue(dockerComposeError);
await expect(service.up(upInputs)).rejects.toThrow(
"Docker Compose command failed with exit code 1",
);
await expect(service.up(upInputs)).rejects.not.toThrow("Error output:");
await expect(service.up(upInputs)).rejects.not.toThrow(
"Standard output:",
);
await expect(service.up(upInputs)).rejects.toThrow("Some error without exit code");
});
it("should pass through standard Error objects", async () => {
@ -256,15 +211,13 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const standardError = new Error("Standard error message");
upAllMock.mockRejectedValue(standardError);
await expect(service.up(upInputs)).rejects.toThrow(
"Standard error message",
);
await expect(service.up(upInputs)).rejects.toThrow("Standard error message");
});
it("should pass through error strings", async () => {
@ -275,7 +228,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const unknownError = "Some unknown error";
@ -292,15 +245,13 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
upFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const unknownError = { unexpected: "error format" };
upAllMock.mockRejectedValue(unknownError);
await expect(service.up(upInputs)).rejects.toThrow(
JSON.stringify(unknownError),
);
await expect(service.up(upInputs)).rejects.toThrow(JSON.stringify(unknownError));
});
});
@ -312,7 +263,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
downFlags: ["--volumes", "--remove-orphans"],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
downMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
@ -339,7 +290,7 @@ describe("DockerComposeService", () => {
composeFlags: [] as string[],
downFlags: [] as string[],
cwd: "/current/working/dir",
serviceLogger: vi.fn(),
serviceLogger: jest.fn(),
};
const dockerComposeError = {
@ -351,17 +302,15 @@ describe("DockerComposeService", () => {
downMock.mockRejectedValue(dockerComposeError);
await expect(service.down(downInputs)).rejects.toThrow(
"Docker Compose command failed with exit code 1",
);
await expect(service.down(downInputs)).rejects.toThrow(
"Error stopping containers",
"Docker Compose command failed with exit code 1"
);
await expect(service.down(downInputs)).rejects.toThrow("Error stopping containers");
});
});
describe("logs", () => {
it("should call logs with correct options", async () => {
const debugMock = vi.fn();
const debugMock = jest.fn();
const logsInputs = {
dockerFlags: [] as string[],
composeFiles: ["docker-compose.yml"],

View File

@ -1,13 +1,13 @@
import {
down,
type IDockerComposeLogOptions,
type IDockerComposeOptions,
type IDockerComposeResult,
IDockerComposeLogOptions,
IDockerComposeOptions,
IDockerComposeResult,
logs,
upAll,
upMany,
} from "docker-compose";
import type { Inputs } from "./input.service.js";
import { Inputs } from "./input.service.js";
type OptionsInputs = {
dockerFlags: Inputs["dockerFlags"];
@ -17,10 +17,7 @@ type OptionsInputs = {
serviceLogger: (message: string) => void;
};
export type UpInputs = OptionsInputs & {
upFlags: Inputs["upFlags"];
services: Inputs["services"];
};
export type UpInputs = OptionsInputs & { upFlags: Inputs["upFlags"]; services: Inputs["services"] };
export type DownInputs = OptionsInputs & { downFlags: Inputs["downFlags"] };
export type LogsInputs = OptionsInputs & { services: Inputs["services"] };
@ -107,21 +104,19 @@ export class DockerComposeService {
// Add exit code information
if (error.exitCode !== null) {
parts.push(
`Docker Compose command failed with exit code ${error.exitCode}`,
);
parts.push(`Docker Compose command failed with exit code ${error.exitCode}`);
} else {
parts.push("Docker Compose command failed");
}
// Add error stream output if available
if (error.err?.trim()) {
if (error.err && error.err.trim()) {
parts.push("\nError output:");
parts.push(error.err.trim());
}
// Add standard output if available and different from error output
if (error.out?.trim() && error.out !== error.err) {
if (error.out && error.out.trim() && error.out !== error.err) {
parts.push("\nStandard output:");
parts.push(error.out.trim());
}

View File

@ -1,23 +1,22 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
// Mock @actions/core before importing the module under test
const getInputMock =
vi.fn<(name: string, options?: { required?: boolean }) => string>();
const getInputMock = jest.fn<(name: string, options?: { required?: boolean }) => string>();
const getMultilineInputMock =
vi.fn<(name: string, options?: { required?: boolean }) => string[]>();
jest.fn<(name: string, options?: { required?: boolean }) => string[]>();
vi.doMock("@actions/core", () => ({
jest.unstable_mockModule("@actions/core", () => ({
getInput: getInputMock,
getMultilineInput: getMultilineInputMock,
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
debug: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
}));
// Mock node:fs
const existsSyncMock = vi.fn<(path: string) => boolean>();
const existsSyncMock = jest.fn<(path: string) => boolean>();
vi.doMock("node:fs", () => ({
jest.unstable_mockModule("node:fs", () => ({
existsSync: existsSyncMock,
default: { existsSync: existsSyncMock },
}));
@ -30,7 +29,7 @@ describe("InputService", () => {
let service: InstanceType<typeof InputService>;
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
getMultilineInputMock.mockImplementation((inputName) => {
switch (inputName) {
@ -161,9 +160,7 @@ describe("InputService", () => {
const inputs = service.getInputs();
expect(inputs.composeFiles).toEqual([
"oci://docker.io/hoverkraft/compose-app:latest",
]);
expect(inputs.composeFiles).toEqual(["oci://docker.io/hoverkraft/compose-app:latest"]);
expect(existsSyncMock).not.toHaveBeenCalled();
});
@ -186,12 +183,10 @@ describe("InputService", () => {
}
});
existsSyncMock.mockImplementation(
(file) => file === "/current/working/directory/file1",
);
existsSyncMock.mockImplementation((file) => file === "/current/working/directory/file1");
expect(() => service.getInputs()).toThrow(
'Compose file not found in "/current/working/directory/file2", "file2"',
'Compose file not found in "/current/working/directory/file2", "file2"'
);
});
@ -391,7 +386,7 @@ describe("InputService", () => {
existsSyncMock.mockReturnValue(true);
expect(() => service.getInputs()).toThrow(
'Invalid service log level "invalid-log-level". Valid values are: debug, info',
'Invalid service log level "invalid-log-level". Valid values are: debug, info'
);
});
});

View File

@ -53,8 +53,7 @@ export class InputService {
private getComposeFiles(): string[] {
const cwd = this.getCwd();
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter(
(composeFile: string) => {
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile: string) => {
const trimmedComposeFile = composeFile.trim();
if (!trimmedComposeFile.length) {
@ -73,11 +72,8 @@ export class InputService {
}
}
throw new Error(
`Compose file not found in "${possiblePaths.join('", "')}"`,
);
},
);
throw new Error(`Compose file not found in "${possiblePaths.join('", "')}"`);
});
if (!composeFiles.length) {
throw new Error("No compose files found");
@ -131,15 +127,10 @@ export class InputService {
}
private getServiceLogLevel(): LogLevel {
const configuredLevel = getInput(InputNames.ServiceLogLevel, {
required: false,
});
if (
configuredLevel &&
!Object.values(LogLevel).includes(configuredLevel as LogLevel)
) {
const configuredLevel = getInput(InputNames.ServiceLogLevel, { required: false });
if (configuredLevel && !Object.values(LogLevel).includes(configuredLevel as LogLevel)) {
throw new Error(
`Invalid service log level "${configuredLevel}". Valid values are: ${Object.values(LogLevel).join(", ")}`,
`Invalid service log level "${configuredLevel}". Valid values are: ${Object.values(LogLevel).join(", ")}`
);
}
return (configuredLevel as LogLevel) || LogLevel.Debug;

View File

@ -1,73 +1,52 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
import type { ExecOptions } from "@actions/exec";
import type { OutgoingHttpHeaders } from "node:http";
// Mock @actions/exec
const execMock =
vi.fn<
(command: string, args?: string[], options?: ExecOptions) => Promise<number>
>();
jest.fn<(command: string, args?: string[], options?: ExecOptions) => Promise<number>>();
vi.doMock("@actions/exec", () => ({
jest.unstable_mockModule("@actions/exec", () => ({
exec: execMock,
}));
// Mock @actions/io
const mkdirPMock = vi.fn<(fsPath: string) => Promise<void>>();
const mkdirPMock = jest.fn<(fsPath: string) => Promise<void>>();
vi.doMock("@actions/io", () => ({
jest.unstable_mockModule("@actions/io", () => ({
mkdirP: mkdirPMock,
}));
// Mock @actions/tool-cache
const cacheFileMock =
vi.fn<
jest.fn<
(
sourceFile: string,
targetFile: string,
tool: string,
version: string,
arch?: string,
arch?: string
) => Promise<string>
>();
const downloadToolMock =
vi.fn<
(
url: string,
dest?: string,
auth?: string,
headers?: OutgoingHttpHeaders,
) => Promise<string>
jest.fn<
(url: string, dest?: string, auth?: string, headers?: OutgoingHttpHeaders) => Promise<string>
>();
vi.doMock("@actions/tool-cache", () => ({
jest.unstable_mockModule("@actions/tool-cache", () => ({
cacheFile: cacheFileMock,
downloadTool: downloadToolMock,
}));
// Dynamic import after mock setup
const { ManualInstallerAdapter } = await import(
"./manual-installer-adapter.js"
);
const originalHome = process.env.HOME;
const originalDockerConfig = process.env.DOCKER_CONFIG;
const { ManualInstallerAdapter } = await import("./manual-installer-adapter.js");
describe("ManualInstallerAdapter", () => {
let adapter: InstanceType<typeof ManualInstallerAdapter>;
beforeEach(() => {
vi.resetAllMocks();
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
if (originalDockerConfig === undefined) {
jest.clearAllMocks();
delete process.env.DOCKER_CONFIG;
} else {
process.env.DOCKER_CONFIG = originalDockerConfig;
}
adapter = new ManualInstallerAdapter();
});
@ -76,17 +55,15 @@ describe("ManualInstallerAdapter", () => {
// Arrange
const version = "v2.29.0";
execMock.mockImplementationOnce(async (_command, _args, options) => {
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
return 0;
});
// Uname -s
execMock.mockResolvedValueOnce(0);
execMock.mockImplementationOnce(async (_command, _args, options) => {
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
return 0;
});
// Uname -m
execMock.mockResolvedValueOnce(0);
process.env.HOME = "/home/test";
Object.defineProperty(process.env, "HOME", {
value: "/home/test",
});
// Act
await adapter.install(version);
@ -101,15 +78,15 @@ describe("ManualInstallerAdapter", () => {
});
expect(downloadToolMock).toHaveBeenCalledWith(
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose-Linux-x86_64",
"/home/test/.docker/cli-plugins/docker-compose",
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose--",
"/home/test/.docker/cli-plugins/docker-compose"
);
expect(cacheFileMock).toHaveBeenCalledWith(
"/home/test/.docker/cli-plugins/docker-compose",
"docker-compose",
"docker-compose",
version,
version
);
});
@ -135,7 +112,7 @@ describe("ManualInstallerAdapter", () => {
// Assert
expect(downloadToolMock).toHaveBeenCalledWith(
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose-Linux-x86_64",
"/custom/docker/cli-plugins/docker-compose",
"/custom/docker/cli-plugins/docker-compose"
);
});
@ -143,17 +120,15 @@ describe("ManualInstallerAdapter", () => {
// Arrange
const version = "2.29.0";
execMock.mockImplementationOnce(async (_command, _args, options) => {
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
return 0;
});
// Uname -s
execMock.mockResolvedValueOnce(0);
execMock.mockImplementationOnce(async (_command, _args, options) => {
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
return 0;
});
// Uname -m
execMock.mockResolvedValueOnce(0);
process.env.HOME = "/home/test";
Object.defineProperty(process.env, "HOME", {
value: "/home/test",
});
// Act
await adapter.install(version);
@ -168,8 +143,8 @@ describe("ManualInstallerAdapter", () => {
});
expect(downloadToolMock).toHaveBeenCalledWith(
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose-Linux-x86_64",
"/home/test/.docker/cli-plugins/docker-compose",
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose--",
"/home/test/.docker/cli-plugins/docker-compose"
);
});
@ -188,7 +163,9 @@ describe("ManualInstallerAdapter", () => {
});
delete process.env.DOCKER_CONFIG;
process.env.HOME = "/home/test";
Object.defineProperty(process.env, "HOME", {
value: "/home/test",
});
// Act
await adapter.install(version);
@ -196,7 +173,7 @@ describe("ManualInstallerAdapter", () => {
// Assert
expect(downloadToolMock).toHaveBeenCalledWith(
"https://github.com/docker/compose/releases/download/1.29.0/docker-compose-Linux-x86_64",
"/home/test/.docker/cli-plugins/docker-compose",
"/home/test/.docker/cli-plugins/docker-compose"
);
});
@ -208,9 +185,7 @@ describe("ManualInstallerAdapter", () => {
execMock.mockResolvedValueOnce(1);
// Act
await expect(adapter.install(version)).rejects.toThrow(
"Failed to run command: uname -s",
);
await expect(adapter.install(version)).rejects.toThrow("Failed to run command: uname -s");
// Assert
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {

View File

@ -2,7 +2,7 @@ import { exec } from "@actions/exec";
import { mkdirP } from "@actions/io";
import { basename } from "node:path";
import { cacheFile, downloadTool } from "@actions/tool-cache";
import type { DockerComposeInstallerAdapter } from "./docker-compose-installer-adapter.js";
import { DockerComposeInstallerAdapter } from "./docker-compose-installer-adapter.js";
export class ManualInstallerAdapter implements DockerComposeInstallerAdapter {
async install(version: string): Promise<void> {
@ -13,26 +13,17 @@ export class ManualInstallerAdapter implements DockerComposeInstallerAdapter {
await this.downloadFile(version, dockerComposePluginPath);
await exec(`chmod +x ${dockerComposePluginPath}`);
await cacheFile(
dockerComposePluginPath,
"docker-compose",
"docker-compose",
version,
);
await cacheFile(dockerComposePluginPath, "docker-compose", "docker-compose", version);
}
private async getDockerComposePluginPath(): Promise<string> {
const dockerConfig =
process.env.DOCKER_CONFIG || `${process.env.HOME}/.docker`;
const dockerConfig = process.env.DOCKER_CONFIG || `${process.env.HOME}/.docker`;
const dockerComposePluginPath = `${dockerConfig}/cli-plugins/docker-compose`;
return dockerComposePluginPath;
}
private async downloadFile(
version: string,
installerPath: string,
): Promise<void> {
private async downloadFile(version: string, installerPath: string): Promise<void> {
if (!version.startsWith("v") && parseInt(version.split(".")[0], 10) >= 2) {
version = `v${version}`;
}

View File

@ -1,14 +1,14 @@
import { describe, expect, it, beforeEach, vi } from "vitest";
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
// Import types directly from the module
import type { LogLevel as LogLevelType } from "./logger.service.js";
// Mock @actions/core before importing the module under test
const warningMock = vi.fn();
const infoMock = vi.fn();
const debugMock = vi.fn();
const warningMock = jest.fn();
const infoMock = jest.fn();
const debugMock = jest.fn();
vi.doMock("@actions/core", () => ({
jest.unstable_mockModule("@actions/core", () => ({
warning: warningMock,
info: infoMock,
debug: debugMock,
@ -21,7 +21,7 @@ describe("LoggerService", () => {
let loggerService: InstanceType<typeof LoggerService>;
beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
loggerService = new LoggerService();
});

View File

@ -6,8 +6,6 @@ WORKDIR /app
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
HEALTHCHECK CMD grep -qa "entrypoint.sh" /proc/1/cmdline || exit 1
CMD ["/bin/sh", "entrypoint.sh"]
USER 1000:1000

View File

@ -1,12 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
include: ["src/**/*.{ts,tsx,js,jsx}"],
provider: "v8",
},
environment: "node",
globals: true,
},
});