mirror of
https://github.com/hoverkraft-tech/compose-action.git
synced 2026-07-04 04:32:51 +08:00
Compare commits
2 Commits
9a87d604e1
...
9c11768144
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c11768144 | ||
|
|
98fdf9dfda |
@ -1,29 +1,30 @@
|
||||
{
|
||||
"name": "Debian",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"remoteEnv": {
|
||||
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"eamodio.gitlens",
|
||||
"github.copilot",
|
||||
"github.copilot-chat",
|
||||
"github.vscode-github-actions",
|
||||
"ms-vscode.makefile-tools",
|
||||
"bierner.markdown-preview-github-styles",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "zsh"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "Debian",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:2": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:3": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"remoteEnv": {
|
||||
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"eamodio.gitlens",
|
||||
"github.copilot",
|
||||
"github.copilot-chat",
|
||||
"github.vscode-github-actions",
|
||||
"ms-vscode.makefile-tools",
|
||||
"bierner.markdown-preview-github-styles",
|
||||
"esbenp.prettier-vscode",
|
||||
"biomejs.biome",
|
||||
"vitest.explorer"
|
||||
],
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "zsh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@ -3,6 +3,8 @@ updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
open-pull-requests-limit: 20
|
||||
cooldown:
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: friday
|
||||
@ -17,6 +19,8 @@ updates:
|
||||
- "/test"
|
||||
- "/"
|
||||
open-pull-requests-limit: 20
|
||||
cooldown:
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: friday
|
||||
@ -29,6 +33,8 @@ updates:
|
||||
- package-ecosystem: docker-compose
|
||||
directory: "/test"
|
||||
open-pull-requests-limit: 20
|
||||
cooldown:
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: friday
|
||||
@ -42,6 +48,8 @@ updates:
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 20
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: friday
|
||||
@ -62,6 +70,8 @@ updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
open-pull-requests-limit: 20
|
||||
directory: "/"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: friday
|
||||
|
||||
2
.github/linters/.jscpd.json
vendored
2
.github/linters/.jscpd.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"ignore": ["**/dist/**", "**/node_modules/**", "**/coverage/**"]
|
||||
"ignore": ["**/dist/**", "**/node_modules/**", "**/coverage/**"]
|
||||
}
|
||||
|
||||
5
.github/workflows/__shared-ci.yml
vendored
5
.github/workflows/__shared-ci.yml
vendored
@ -7,7 +7,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
linter:
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/linter.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@ -15,7 +15,7 @@ jobs:
|
||||
statuses: write
|
||||
with:
|
||||
linter-env: |
|
||||
FILTER_REGEX_EXCLUDE=dist/**/*
|
||||
FILTER_REGEX_EXCLUDE=dist/**/*|.github/social-preview.svg|.github/logo.svg
|
||||
VALIDATE_JSCPD=false
|
||||
VALIDATE_TYPESCRIPT_STANDARD=false
|
||||
VALIDATE_TYPESCRIPT_ES=false
|
||||
@ -33,7 +33,6 @@ jobs:
|
||||
packages: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
secrets: inherit
|
||||
|
||||
check-dist:
|
||||
name: Test nodejs
|
||||
|
||||
4
.github/workflows/greetings.yml
vendored
4
.github/workflows/greetings.yml
vendored
@ -3,14 +3,14 @@ name: Greetings
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only reusable workflow with explicit write permissions and no checkout
|
||||
branches: [main]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
greetings:
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/greetings.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
5
.github/workflows/main-ci.yml
vendored
5
.github/workflows/main-ci.yml
vendored
@ -27,7 +27,6 @@ jobs:
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
statuses: write
|
||||
secrets: inherit
|
||||
|
||||
prepare-docs:
|
||||
needs: ci
|
||||
@ -65,9 +64,9 @@ jobs:
|
||||
id: generate-token
|
||||
with:
|
||||
client-id: ${{ vars.CI_BOT_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }}
|
||||
private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }} # zizmor: ignore[secrets-outside-env] repository automation uses a dedicated app secret without untrusted code execution
|
||||
|
||||
- uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
|
||||
- uses: hoverkraft-tech/ci-github-common/actions/create-and-merge-pull-request@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
branch: docs/actions-workflows-documentation-update
|
||||
|
||||
7
.github/workflows/need-fix-to-issue.yml
vendored
7
.github/workflows/need-fix-to-issue.yml
vendored
@ -11,15 +11,16 @@ 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@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/need-fix-to-issue.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
1
.github/workflows/pull-request-ci.yml
vendored
1
.github/workflows/pull-request-ci.yml
vendored
@ -22,4 +22,3 @@ jobs:
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
statuses: write
|
||||
secrets: inherit
|
||||
|
||||
4
.github/workflows/semantic-pull-request.yml
vendored
4
.github/workflows/semantic-pull-request.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: "Pull Request - Semantic Lint"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] validates PR metadata only through a pinned reusable workflow without checkout
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
@ -11,7 +11,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
main:
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/semantic-pull-request.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -8,7 +8,7 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
main:
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@66578f5b9aec4ac5558b5dad750c4c74dfcb65c5 # 0.35.5
|
||||
uses: hoverkraft-tech/ci-github-common/.github/workflows/stale.yml@4bb7594b1bf3696c54b2bbae970376056853f8ea # 0.36.0
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -12,7 +12,6 @@ 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
|
||||
@ -54,9 +53,6 @@ typings/
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
@ -102,3 +98,6 @@ __tests__/runner/*
|
||||
.idea
|
||||
.vscode
|
||||
*.code-workspace
|
||||
lint.sarif
|
||||
biome-report.sarif
|
||||
junit.xml
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
FROM ghcr.io/super-linter/super-linter:slim-v8.0.0
|
||||
|
||||
FROM ghcr.io/hoverkraft-tech/docker-base-images/super-linter:0.6.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"
|
||||
|
||||
14
Makefile
14
Makefile
@ -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,12 +29,8 @@ 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 \
|
||||
|
||||
34
biome.json
Normal file
34
biome.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"$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
23
dist/index.js
generated
vendored
@ -43860,8 +43860,11 @@ 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;
|
||||
@ -43943,12 +43946,12 @@ class DockerComposeService {
|
||||
parts.push("Docker Compose command failed");
|
||||
}
|
||||
// Add error stream output if available
|
||||
if (error.err && error.err.trim()) {
|
||||
if (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 && error.out.trim() && error.out !== error.err) {
|
||||
if (error.out?.trim() && error.out !== error.err) {
|
||||
parts.push("\nStandard output:");
|
||||
parts.push(error.out.trim());
|
||||
}
|
||||
@ -48237,14 +48240,17 @@ 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;
|
||||
}
|
||||
@ -48258,7 +48264,8 @@ 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
11
dist/post.js
generated
vendored
@ -40112,8 +40112,11 @@ 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;
|
||||
@ -40195,12 +40198,12 @@ class DockerComposeService {
|
||||
parts.push("Docker Compose command failed");
|
||||
}
|
||||
// Add error stream output if available
|
||||
if (error.err && error.err.trim()) {
|
||||
if (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 && error.out.trim() && error.out !== error.err) {
|
||||
if (error.out?.trim() && error.out !== error.err) {
|
||||
parts.push("\nStandard output:");
|
||||
parts.push(error.out.trim());
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import tsDevToolsCore from "@ts-dev-tools/core/dist/eslint-plugin-ts-dev-tools/index.js";
|
||||
|
||||
export default tsDevToolsCore.default;
|
||||
13673
package-lock.json
generated
13673
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
186
package.json
186
package.json
@ -1,126 +1,64 @@
|
||||
{
|
||||
"name": "compose-action",
|
||||
"description": "Docker Compose Action",
|
||||
"version": "0.0.0",
|
||||
"author": "hoverkraft",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/hoverkraft-tech/compose-action",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hoverkraft-tech/compose-action.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/hoverkraft-tech/compose-action/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"docker-compose"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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.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": "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": "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": "20250623095600-remove-prettier-oxc"
|
||||
}
|
||||
"name": "compose-action",
|
||||
"description": "Docker Compose Action",
|
||||
"version": "0.0.0",
|
||||
"author": "hoverkraft",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/hoverkraft-tech/compose-action",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hoverkraft-tech/compose-action.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/hoverkraft-tech/compose-action/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"docker-compose"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/github": "^9.1.1",
|
||||
"@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"
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"tsDevTools": {
|
||||
"version": "20260604100000-migrate-to-vitest"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,193 +1,204 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
// Mock @actions/core
|
||||
const setFailedMock = jest.fn();
|
||||
const setFailedMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: jest.fn().mockReturnValue(""),
|
||||
getMultilineInput: jest.fn().mockReturnValue([]),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
vi.doMock("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: vi.fn().mockReturnValue(""),
|
||||
getMultilineInput: vi.fn().mockReturnValue([]),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock docker-compose
|
||||
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" } }),
|
||||
vi.doMock("docker-compose", () => ({
|
||||
upAll: vi.fn(),
|
||||
upMany: vi.fn(),
|
||||
down: vi.fn(),
|
||||
logs: vi.fn(),
|
||||
version: vi
|
||||
.fn<() => Promise<{ data: { version: string } }>>()
|
||||
.mockResolvedValue({ data: { version: "1.2.3" } }),
|
||||
}));
|
||||
|
||||
// Mock node:fs
|
||||
jest.unstable_mockModule("node:fs", async () => {
|
||||
const actualFs = await jest.requireActual<typeof import("node:fs")>("node:fs");
|
||||
vi.doMock("node:fs", async () => {
|
||||
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
|
||||
return {
|
||||
...actualFs,
|
||||
existsSync: jest.fn().mockReturnValue(true),
|
||||
default: {
|
||||
...actualFs,
|
||||
existsSync: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
return {
|
||||
...actualFs,
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
default: {
|
||||
...actualFs,
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 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: 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>;
|
||||
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>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
it("should install docker compose with specified version", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: "1.29.2",
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
it("should install docker compose with specified version", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: "1.29.2",
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
installMock.mockResolvedValue("1.29.2");
|
||||
installMock.mockResolvedValue("1.29.2");
|
||||
|
||||
upMock.mockResolvedValue();
|
||||
upMock.mockResolvedValue();
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(infoMock).toHaveBeenCalledWith("Setting up docker compose version 1.29.2");
|
||||
// Assert
|
||||
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"}'
|
||||
);
|
||||
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"}',
|
||||
);
|
||||
|
||||
expect(installMock).toHaveBeenCalledWith({
|
||||
composeVersion: "1.29.2",
|
||||
cwd: "/current/working/dir",
|
||||
githubToken: null,
|
||||
});
|
||||
expect(installMock).toHaveBeenCalledWith({
|
||||
composeVersion: "1.29.2",
|
||||
cwd: "/current/working/dir",
|
||||
githubToken: null,
|
||||
});
|
||||
|
||||
expect(upMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
upFlags: [],
|
||||
services: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(upMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
upFlags: [],
|
||||
services: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should bring up docker compose services", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
it("should bring up docker compose services", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(upMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
upFlags: [],
|
||||
services: ["web"],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
// Assert
|
||||
expect(upMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
upFlags: [],
|
||||
services: ["web"],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Test error");
|
||||
upMock.mockRejectedValue(error);
|
||||
it("should handle errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Test error");
|
||||
upMock.mockRejectedValue(error);
|
||||
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith("Error: Test error");
|
||||
});
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith("Error: Test error");
|
||||
});
|
||||
|
||||
it("should handle unknown errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = "Test error";
|
||||
upMock.mockRejectedValue(error);
|
||||
it("should handle unknown errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = "Test error";
|
||||
upMock.mockRejectedValue(error);
|
||||
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith('"Test error"');
|
||||
});
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith('"Test error"');
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,42 +10,42 @@ import { ManualInstallerAdapter } from "./services/installer-adapter/manual-inst
|
||||
* @returns {Promise<void>} Resolves when the action is complete.
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const loggerService = new LoggerService();
|
||||
const inputService = new InputService();
|
||||
const dockerComposeInstallerService = new DockerComposeInstallerService(
|
||||
new ManualInstallerAdapter()
|
||||
);
|
||||
const dockerComposeService = new DockerComposeService();
|
||||
try {
|
||||
const loggerService = new LoggerService();
|
||||
const inputService = new InputService();
|
||||
const dockerComposeInstallerService = new DockerComposeInstallerService(
|
||||
new ManualInstallerAdapter(),
|
||||
);
|
||||
const dockerComposeService = new DockerComposeService();
|
||||
|
||||
const inputs = inputService.getInputs();
|
||||
loggerService.debug(`inputs: ${JSON.stringify(inputs)}`);
|
||||
const inputs = inputService.getInputs();
|
||||
loggerService.debug(`inputs: ${JSON.stringify(inputs)}`);
|
||||
|
||||
loggerService.info(
|
||||
"Setting up docker compose" +
|
||||
(inputs.composeVersion ? ` version ${inputs.composeVersion}` : "")
|
||||
);
|
||||
loggerService.info(
|
||||
"Setting up docker compose" +
|
||||
(inputs.composeVersion ? ` version ${inputs.composeVersion}` : ""),
|
||||
);
|
||||
|
||||
const installedVersion = await dockerComposeInstallerService.install({
|
||||
composeVersion: inputs.composeVersion,
|
||||
cwd: inputs.cwd,
|
||||
githubToken: inputs.githubToken,
|
||||
});
|
||||
const installedVersion = await dockerComposeInstallerService.install({
|
||||
composeVersion: inputs.composeVersion,
|
||||
cwd: inputs.cwd,
|
||||
githubToken: inputs.githubToken,
|
||||
});
|
||||
|
||||
loggerService.info(`docker compose version: ${installedVersion}`);
|
||||
loggerService.info(`docker compose version: ${installedVersion}`);
|
||||
|
||||
loggerService.info("Bringing up docker compose service(s)");
|
||||
await dockerComposeService.up({
|
||||
dockerFlags: inputs.dockerFlags,
|
||||
composeFiles: inputs.composeFiles,
|
||||
composeFlags: inputs.composeFlags,
|
||||
cwd: inputs.cwd,
|
||||
upFlags: inputs.upFlags,
|
||||
services: inputs.services,
|
||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||
});
|
||||
loggerService.info("docker compose service(s) are up");
|
||||
} catch (error) {
|
||||
setFailed(`${error instanceof Error ? error : JSON.stringify(error)}`);
|
||||
}
|
||||
loggerService.info("Bringing up docker compose service(s)");
|
||||
await dockerComposeService.up({
|
||||
dockerFlags: inputs.dockerFlags,
|
||||
composeFiles: inputs.composeFiles,
|
||||
composeFlags: inputs.composeFlags,
|
||||
cwd: inputs.cwd,
|
||||
upFlags: inputs.upFlags,
|
||||
services: inputs.services,
|
||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||
});
|
||||
loggerService.info("docker compose service(s) are up");
|
||||
} catch (error) {
|
||||
setFailed(`${error instanceof Error ? error : JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,109 +1,127 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
// Mock @actions/core
|
||||
const setFailedMock = jest.fn();
|
||||
const setFailedMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: jest.fn().mockReturnValue(""),
|
||||
getMultilineInput: jest.fn().mockReturnValue([]),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
vi.doMock("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: vi.fn().mockReturnValue(""),
|
||||
getMultilineInput: vi.fn().mockReturnValue([]),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock docker-compose
|
||||
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" } }),
|
||||
vi.doMock("docker-compose", () => ({
|
||||
upAll: vi.fn(),
|
||||
upMany: vi.fn(),
|
||||
down: vi.fn(),
|
||||
logs: vi.fn(),
|
||||
version: vi
|
||||
.fn<() => Promise<{ data: { version: string } }>>()
|
||||
.mockResolvedValue({ data: { version: "1.2.3" } }),
|
||||
}));
|
||||
|
||||
// Mock node:fs
|
||||
jest.unstable_mockModule("node:fs", async () => {
|
||||
const actualFs = await jest.requireActual<typeof import("node:fs")>("node:fs");
|
||||
vi.doMock("node:fs", async () => {
|
||||
const actualFs = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
|
||||
return {
|
||||
...actualFs,
|
||||
existsSync: jest.fn().mockReturnValue(true),
|
||||
default: {
|
||||
...actualFs,
|
||||
existsSync: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
};
|
||||
return {
|
||||
...actualFs,
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
default: {
|
||||
...actualFs,
|
||||
existsSync: vi.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: 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>;
|
||||
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>;
|
||||
|
||||
describe("index", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
it("calls run when imported", async () => {
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
it("calls run when imported", async () => {
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
installMock.mockResolvedValue("1.2.3");
|
||||
upMock.mockResolvedValueOnce();
|
||||
installMock.mockResolvedValue("1.2.3");
|
||||
upMock.mockResolvedValueOnce();
|
||||
|
||||
await import("./index.js");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await import("./index.js");
|
||||
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(1, "Setting up docker compose");
|
||||
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"}'
|
||||
);
|
||||
// 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"}',
|
||||
);
|
||||
|
||||
expect(infoMock).toHaveBeenNthCalledWith(3, "Bringing up docker compose service(s)");
|
||||
expect(infoMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"Bringing up docker compose service(s)",
|
||||
);
|
||||
|
||||
expect(upMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(upMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(infoMock).toHaveBeenNthCalledWith(4, "docker compose service(s) are up");
|
||||
});
|
||||
expect(infoMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
"docker compose service(s) are up",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,193 +1,205 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
// Mock @actions/core
|
||||
const setFailedMock = jest.fn();
|
||||
const setFailedMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: jest.fn().mockReturnValue(""),
|
||||
getMultilineInput: jest.fn().mockReturnValue([]),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
vi.doMock("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: vi.fn().mockReturnValue(""),
|
||||
getMultilineInput: vi.fn().mockReturnValue([]),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock docker-compose
|
||||
const logsMock = jest.fn();
|
||||
const downMock = jest.fn();
|
||||
const logsMock = vi.fn();
|
||||
const downMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("docker-compose", () => ({
|
||||
logs: logsMock,
|
||||
down: downMock,
|
||||
upAll: jest.fn(),
|
||||
upMany: jest.fn(),
|
||||
vi.doMock("docker-compose", () => ({
|
||||
logs: logsMock,
|
||||
down: downMock,
|
||||
upAll: vi.fn(),
|
||||
upMany: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock node:fs
|
||||
jest.unstable_mockModule("node:fs", () => ({
|
||||
existsSync: jest.fn().mockReturnValue(true),
|
||||
default: { existsSync: jest.fn().mockReturnValue(true) },
|
||||
vi.doMock("node:fs", () => ({
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
default: { existsSync: vi.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: 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>;
|
||||
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>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
it("should bring down docker compose service(s) and log output", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
it("should bring down docker compose service(s) and log output", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
serviceLogsMock.mockResolvedValue({ error: "", output: "test logs" });
|
||||
serviceDownMock.mockResolvedValue();
|
||||
serviceLogsMock.mockResolvedValue({ error: "", output: "test logs" });
|
||||
serviceDownMock.mockResolvedValue();
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(serviceLogsMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
services: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
// Assert
|
||||
expect(serviceLogsMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
services: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
|
||||
expect(serviceDownMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
downFlags: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(serviceDownMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
downFlags: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
|
||||
expect(debugMock).toHaveBeenCalledWith("docker compose logs:\ntest logs");
|
||||
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(debugMock).toHaveBeenCalledWith("docker compose logs:\ntest logs");
|
||||
expect(infoMock).toHaveBeenCalledWith("docker compose is down");
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log docker composer errors if any", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
it("should log docker composer errors if any", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
serviceLogsMock.mockResolvedValue({
|
||||
error: "test logs error",
|
||||
output: "test logs output",
|
||||
});
|
||||
serviceLogsMock.mockResolvedValue({
|
||||
error: "test logs error",
|
||||
output: "test logs output",
|
||||
});
|
||||
|
||||
serviceDownMock.mockResolvedValue();
|
||||
serviceDownMock.mockResolvedValue();
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
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");
|
||||
});
|
||||
// Assert
|
||||
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");
|
||||
});
|
||||
|
||||
it("should set failed when an error occurs", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => {
|
||||
throw new Error("An error occurred");
|
||||
});
|
||||
it("should set failed when an error occurs", async () => {
|
||||
// Arrange
|
||||
getInputsMock.mockImplementation(() => {
|
||||
throw new Error("An error occurred");
|
||||
});
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith("Error: An error occurred");
|
||||
});
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith("Error: An error occurred");
|
||||
});
|
||||
|
||||
it("should handle errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Test error");
|
||||
serviceDownMock.mockRejectedValue(error);
|
||||
it("should handle errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Test error");
|
||||
serviceDownMock.mockRejectedValue(error);
|
||||
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith("Error: Test error");
|
||||
});
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith("Error: Test error");
|
||||
});
|
||||
|
||||
it("should handle unknown errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = "Test error";
|
||||
serviceDownMock.mockRejectedValue(error);
|
||||
it("should handle unknown errors and call setFailed", async () => {
|
||||
// Arrange
|
||||
const error = "Test error";
|
||||
serviceDownMock.mockRejectedValue(error);
|
||||
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
// Act
|
||||
await run();
|
||||
// Act
|
||||
await run();
|
||||
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith('"Test error"');
|
||||
});
|
||||
// Assert
|
||||
expect(setFailedMock).toHaveBeenCalledWith('"Test error"');
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,39 +8,39 @@ import { DockerComposeService } from "./services/docker-compose.service.js";
|
||||
* @returns {Promise<void>} Resolves when the action is complete.
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
const loggerService = new LoggerService();
|
||||
const inputService = new InputService();
|
||||
const dockerComposeService = new DockerComposeService();
|
||||
try {
|
||||
const loggerService = new LoggerService();
|
||||
const inputService = new InputService();
|
||||
const dockerComposeService = new DockerComposeService();
|
||||
|
||||
const inputs = inputService.getInputs();
|
||||
const inputs = inputService.getInputs();
|
||||
|
||||
const { error, output } = await dockerComposeService.logs({
|
||||
dockerFlags: inputs.dockerFlags,
|
||||
composeFiles: inputs.composeFiles,
|
||||
composeFlags: inputs.composeFlags,
|
||||
cwd: inputs.cwd,
|
||||
services: inputs.services,
|
||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||
});
|
||||
const { error, output } = await dockerComposeService.logs({
|
||||
dockerFlags: inputs.dockerFlags,
|
||||
composeFiles: inputs.composeFiles,
|
||||
composeFlags: inputs.composeFlags,
|
||||
cwd: inputs.cwd,
|
||||
services: inputs.services,
|
||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
loggerService.debug("docker compose error:\n" + error);
|
||||
}
|
||||
if (error) {
|
||||
loggerService.debug("docker compose error:\n" + error);
|
||||
}
|
||||
|
||||
loggerService.debug("docker compose logs:\n" + output);
|
||||
loggerService.debug("docker compose logs:\n" + output);
|
||||
|
||||
await dockerComposeService.down({
|
||||
dockerFlags: inputs.dockerFlags,
|
||||
composeFiles: inputs.composeFiles,
|
||||
composeFlags: inputs.composeFlags,
|
||||
cwd: inputs.cwd,
|
||||
downFlags: inputs.downFlags,
|
||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||
});
|
||||
await dockerComposeService.down({
|
||||
dockerFlags: inputs.dockerFlags,
|
||||
composeFiles: inputs.composeFiles,
|
||||
composeFlags: inputs.composeFlags,
|
||||
cwd: inputs.cwd,
|
||||
downFlags: inputs.downFlags,
|
||||
serviceLogger: loggerService.getServiceLogger(inputs.serviceLogLevel),
|
||||
});
|
||||
|
||||
loggerService.info("docker compose is down");
|
||||
} catch (error) {
|
||||
setFailed(`${error instanceof Error ? error : JSON.stringify(error)}`);
|
||||
}
|
||||
loggerService.info("docker compose is down");
|
||||
} catch (error) {
|
||||
setFailed(`${error instanceof Error ? error : JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
153
src/post.test.ts
153
src/post.test.ts
@ -1,97 +1,108 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
// Mock @actions/core
|
||||
const setFailedMock = jest.fn();
|
||||
const setFailedMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: jest.fn().mockReturnValue(""),
|
||||
getMultilineInput: jest.fn().mockReturnValue([]),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
vi.doMock("@actions/core", () => ({
|
||||
setFailed: setFailedMock,
|
||||
getInput: vi.fn().mockReturnValue(""),
|
||||
getMultilineInput: vi.fn().mockReturnValue([]),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock docker-compose
|
||||
const logsMock = jest.fn();
|
||||
const downMock = jest.fn();
|
||||
const logsMock = vi.fn();
|
||||
const downMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("docker-compose", () => ({
|
||||
logs: logsMock,
|
||||
down: downMock,
|
||||
upAll: jest.fn(),
|
||||
upMany: jest.fn(),
|
||||
vi.doMock("docker-compose", () => ({
|
||||
logs: logsMock,
|
||||
down: downMock,
|
||||
upAll: vi.fn(),
|
||||
upMany: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock node:fs
|
||||
jest.unstable_mockModule("node:fs", () => ({
|
||||
existsSync: jest.fn().mockReturnValue(true),
|
||||
default: { existsSync: jest.fn().mockReturnValue(true) },
|
||||
vi.doMock("node:fs", () => ({
|
||||
existsSync: vi.fn().mockReturnValue(true),
|
||||
default: { existsSync: vi.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: 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>;
|
||||
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>;
|
||||
|
||||
describe("post", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
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");
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
it("calls run when imported", async () => {
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
it("calls run when imported", async () => {
|
||||
getInputsMock.mockImplementation(() => ({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [],
|
||||
composeFlags: [],
|
||||
upFlags: [],
|
||||
downFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
composeVersion: null,
|
||||
githubToken: null,
|
||||
serviceLogLevel: LogLevel.Debug,
|
||||
}));
|
||||
|
||||
serviceLogsMock.mockResolvedValue({ error: "", output: "test logs" });
|
||||
serviceDownMock.mockResolvedValueOnce();
|
||||
serviceLogsMock.mockResolvedValue({ error: "", output: "test logs" });
|
||||
serviceDownMock.mockResolvedValueOnce();
|
||||
|
||||
await import("./post.js");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await import("./post.js");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(serviceLogsMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
services: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(serviceLogsMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
services: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
|
||||
expect(serviceDownMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
downFlags: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
expect(serviceDownMock).toHaveBeenCalledWith({
|
||||
dockerFlags: [],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
composeFlags: [],
|
||||
cwd: "/current/working/dir",
|
||||
downFlags: [],
|
||||
serviceLogger: debugMock,
|
||||
});
|
||||
|
||||
expect(debugMock).toHaveBeenNthCalledWith(1, "docker compose logs:\ntest logs");
|
||||
expect(infoMock).toHaveBeenNthCalledWith(1, "docker compose is down");
|
||||
expect(debugMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"docker compose logs:\ntest logs",
|
||||
);
|
||||
expect(infoMock).toHaveBeenNthCalledWith(1, "docker compose is down");
|
||||
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(setFailedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,272 +1,302 @@
|
||||
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import type { IDockerComposeResult } from "docker-compose";
|
||||
import { MockAgent, setGlobalDispatcher } from "undici";
|
||||
|
||||
// Mock docker-compose before importing the module under test
|
||||
const versionMock = jest.fn<() => Promise<IDockerComposeResult & { data: { version: string } }>>();
|
||||
const versionMock =
|
||||
vi.fn<() => Promise<IDockerComposeResult & { data: { version: string } }>>();
|
||||
|
||||
jest.unstable_mockModule("docker-compose", () => ({
|
||||
version: versionMock,
|
||||
vi.doMock("docker-compose", () => ({
|
||||
version: versionMock,
|
||||
}));
|
||||
|
||||
// Create manual installer adapter mock
|
||||
const manualInstallerAdapterMock = {
|
||||
install: jest.fn<(version: string) => Promise<void>>(),
|
||||
install: vi.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;
|
||||
let service: InstanceType<typeof DockerComposeInstallerService>;
|
||||
let mockAgent: MockAgent;
|
||||
let service: InstanceType<typeof DockerComposeInstallerService>;
|
||||
|
||||
const composeVersionResponse = (version: string) => ({
|
||||
exitCode: 0,
|
||||
out: "",
|
||||
err: "",
|
||||
data: {
|
||||
version,
|
||||
},
|
||||
});
|
||||
const composeVersionResponse = (version: string) => ({
|
||||
exitCode: 0,
|
||||
out: "",
|
||||
err: "",
|
||||
data: {
|
||||
version,
|
||||
},
|
||||
});
|
||||
|
||||
const installCompose = (composeVersion: string | null, githubToken: string | null) =>
|
||||
service.install({
|
||||
composeVersion,
|
||||
cwd: "/path/to/cwd",
|
||||
githubToken,
|
||||
});
|
||||
const installCompose = (
|
||||
composeVersion: string | null,
|
||||
githubToken: string | null,
|
||||
) =>
|
||||
service.install({
|
||||
composeVersion,
|
||||
cwd: "/path/to/cwd",
|
||||
githubToken,
|
||||
});
|
||||
|
||||
const setPlatform = (platform: NodeJS.Platform) => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: platform,
|
||||
});
|
||||
};
|
||||
const setPlatform = (platform: NodeJS.Platform) => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: platform,
|
||||
});
|
||||
};
|
||||
|
||||
const mockLatestRelease = (version: string) => {
|
||||
const mockClient = mockAgent.get("https://api.github.com");
|
||||
mockClient
|
||||
.intercept({
|
||||
path: "/repos/docker/compose/releases/latest",
|
||||
method: "GET",
|
||||
})
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
tag_name: version,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
setGlobalDispatcher(mockClient);
|
||||
Object.defineProperty(globalThis, "fetch", {
|
||||
value: jest.fn(),
|
||||
});
|
||||
};
|
||||
const mockLatestRelease = (version: string) => {
|
||||
const mockClient = mockAgent.get("https://api.github.com");
|
||||
mockClient
|
||||
.intercept({
|
||||
path: "/repos/docker/compose/releases/latest",
|
||||
method: "GET",
|
||||
})
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
tag_name: version,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
setGlobalDispatcher(mockClient);
|
||||
Object.defineProperty(globalThis, "fetch", {
|
||||
value: vi.fn(),
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAgent = new MockAgent();
|
||||
mockAgent.disableNetConnect();
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockAgent = new MockAgent();
|
||||
mockAgent.disableNetConnect();
|
||||
|
||||
service = new DockerComposeInstallerService(manualInstallerAdapterMock as never);
|
||||
});
|
||||
service = new DockerComposeInstallerService(
|
||||
manualInstallerAdapterMock as never,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("install", () => {
|
||||
it("should install latest when compose version is not specified and Compose is missing", async () => {
|
||||
// Arrange: first call to version() fails (Compose missing)
|
||||
versionMock.mockRejectedValueOnce(new Error("version not installed"));
|
||||
describe("install", () => {
|
||||
it("should install latest when compose version is not specified and Compose is missing", async () => {
|
||||
// Arrange: first call to version() fails (Compose missing)
|
||||
versionMock.mockRejectedValueOnce(new Error("version not installed"));
|
||||
|
||||
const latestVersion = "v2.0.0";
|
||||
mockLatestRelease(latestVersion);
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
|
||||
setPlatform("linux");
|
||||
const latestVersion = "v2.0.0";
|
||||
mockLatestRelease(latestVersion);
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
|
||||
setPlatform("linux");
|
||||
|
||||
// Act
|
||||
const result = await installCompose(null, "token");
|
||||
// Act
|
||||
const result = await installCompose(null, "token");
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(latestVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe(latestVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
|
||||
latestVersion,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return current version when no version is provided", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValue(composeVersionResponse("2.0.0"));
|
||||
it("should return current version when no version is provided", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValue(composeVersionResponse("2.0.0"));
|
||||
|
||||
// Act
|
||||
const result = await installCompose(null, null);
|
||||
// Act
|
||||
const result = await installCompose(null, null);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe("2.0.0");
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe("2.0.0");
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not install anything when expected version is already installed", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValue(composeVersionResponse("1.2.3"));
|
||||
it("should not install anything when expected version is already installed", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValue(composeVersionResponse("1.2.3"));
|
||||
|
||||
// Act
|
||||
const result = await installCompose("v1.2.3", null);
|
||||
// Act
|
||||
const result = await installCompose("v1.2.3", null);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe("1.2.3");
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe("1.2.3");
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should install the requested version if it is not already installed", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
it("should install the requested version if it is not already installed", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
|
||||
const expectedVersion = "1.3.0";
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion));
|
||||
setPlatform("linux");
|
||||
const expectedVersion = "1.3.0";
|
||||
versionMock.mockResolvedValueOnce(
|
||||
composeVersionResponse(expectedVersion),
|
||||
);
|
||||
setPlatform("linux");
|
||||
|
||||
// Act
|
||||
const result = await installCompose(expectedVersion, null);
|
||||
// Act
|
||||
const result = await installCompose(expectedVersion, null);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(expectedVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(expectedVersion);
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe(expectedVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
|
||||
expectedVersion,
|
||||
);
|
||||
});
|
||||
|
||||
it("should install the latest version if requested", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
it("should install the latest version if requested", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
|
||||
const latestVersion = "v1.4.0";
|
||||
mockLatestRelease(latestVersion);
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
|
||||
setPlatform("linux");
|
||||
const latestVersion = "v1.4.0";
|
||||
mockLatestRelease(latestVersion);
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
|
||||
setPlatform("linux");
|
||||
|
||||
// Act
|
||||
const result = await installCompose("latest", "token");
|
||||
// Act
|
||||
const result = await installCompose("latest", "token");
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(latestVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe(latestVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
|
||||
latestVersion,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if the latest version if requested and no Github token is provided", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
it("should throw an error if the latest version if requested and no Github token is provided", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
|
||||
// Act & Assert
|
||||
await expect(installCompose("latest", null)).rejects.toThrow(
|
||||
"GitHub token is required to install the latest version"
|
||||
);
|
||||
});
|
||||
// Act & Assert
|
||||
await expect(installCompose("latest", null)).rejects.toThrow(
|
||||
"GitHub token is required to install the latest version",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error on unsupported platforms", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
it("should throw an error on unsupported platforms", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
|
||||
const expectedVersion = "1.3.0";
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion));
|
||||
setPlatform("win32");
|
||||
const expectedVersion = "1.3.0";
|
||||
versionMock.mockResolvedValueOnce(
|
||||
composeVersionResponse(expectedVersion),
|
||||
);
|
||||
setPlatform("win32");
|
||||
|
||||
// Act & Assert
|
||||
await expect(installCompose(expectedVersion, null)).rejects.toThrow(
|
||||
`Unsupported platform: win32`
|
||||
);
|
||||
// Act & Assert
|
||||
await expect(installCompose(expectedVersion, null)).rejects.toThrow(
|
||||
`Unsupported platform: win32`,
|
||||
);
|
||||
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should install when version check fails", async () => {
|
||||
// Arrange: first call to version() doesn't find
|
||||
versionMock.mockRejectedValueOnce(new Error("version not installed"));
|
||||
it("should install when version check fails", async () => {
|
||||
// Arrange: first call to version() doesn't find
|
||||
versionMock.mockRejectedValueOnce(new Error("version not installed"));
|
||||
|
||||
const installedVersion = "2.0.0";
|
||||
const installedVersion = "2.0.0";
|
||||
|
||||
// After installation, version() returns the new version
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(installedVersion));
|
||||
setPlatform("linux");
|
||||
// After installation, version() returns the new version
|
||||
versionMock.mockResolvedValueOnce(
|
||||
composeVersionResponse(installedVersion),
|
||||
);
|
||||
setPlatform("linux");
|
||||
|
||||
// Act
|
||||
const result = await installCompose(installedVersion, "token");
|
||||
// Act
|
||||
const result = await installCompose(installedVersion, "token");
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(installedVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(installedVersion);
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe(installedVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
|
||||
installedVersion,
|
||||
);
|
||||
});
|
||||
|
||||
it("should install latest version when missing or unspecified", async () => {
|
||||
// Arrange: first call to version() doesn't find
|
||||
versionMock.mockRejectedValueOnce(new Error("version check failed"));
|
||||
// second call finds newly installed version
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("v1.4.0"));
|
||||
it("should install latest version when missing or unspecified", async () => {
|
||||
// Arrange: first call to version() doesn't find
|
||||
versionMock.mockRejectedValueOnce(new Error("version check failed"));
|
||||
// second call finds newly installed version
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("v1.4.0"));
|
||||
|
||||
const latestVersion = "v1.4.0";
|
||||
mockLatestRelease(latestVersion);
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
|
||||
setPlatform("linux");
|
||||
const latestVersion = "v1.4.0";
|
||||
mockLatestRelease(latestVersion);
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
|
||||
setPlatform("linux");
|
||||
|
||||
// Act
|
||||
const result = await installCompose("latest", "token");
|
||||
// Act
|
||||
const result = await installCompose("latest", "token");
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(latestVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe(latestVersion);
|
||||
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(
|
||||
latestVersion,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if Compose is missing and no GitHub token is provided", async () => {
|
||||
// Arrange: first call to version() doesn't find
|
||||
versionMock.mockRejectedValueOnce(new Error("version check failed"));
|
||||
setPlatform("linux");
|
||||
it("should throw if Compose is missing and no GitHub token is provided", async () => {
|
||||
// Arrange: first call to version() doesn't find
|
||||
versionMock.mockRejectedValueOnce(new Error("version check failed"));
|
||||
setPlatform("linux");
|
||||
|
||||
await expect(installCompose("latest", null)).rejects.toThrow(
|
||||
"GitHub token is required to install the latest version"
|
||||
);
|
||||
});
|
||||
await expect(installCompose("latest", null)).rejects.toThrow(
|
||||
"GitHub token is required to install the latest version",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not install when the version is already installed and no version is specified", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValue(composeVersionResponse("1.2.3"));
|
||||
it("should not install when the version is already installed and no version is specified", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValue(composeVersionResponse("1.2.3"));
|
||||
|
||||
// Act
|
||||
const result = await installCompose("", null);
|
||||
// Act
|
||||
const result = await installCompose("", null);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe("1.2.3");
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
// Assert
|
||||
expect(result).toBe("1.2.3");
|
||||
expect(manualInstallerAdapterMock.install).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw when installed version does not match target", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
it("should throw when installed version does not match target", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
|
||||
const targetVersion = "v1.4.0";
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.3.0"));
|
||||
setPlatform("linux");
|
||||
const targetVersion = "v1.4.0";
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.3.0"));
|
||||
setPlatform("linux");
|
||||
|
||||
// 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);
|
||||
});
|
||||
// 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,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw with unknown installed version when post-install version check fails", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
it("should throw with unknown installed version when post-install version check fails", async () => {
|
||||
// Arrange
|
||||
versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
|
||||
|
||||
const targetVersion = "v1.4.0";
|
||||
versionMock.mockRejectedValueOnce(new Error("version check failed after install"));
|
||||
setPlatform("linux");
|
||||
const targetVersion = "v1.4.0";
|
||||
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);
|
||||
});
|
||||
});
|
||||
// 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,96 +1,108 @@
|
||||
import * as github from "@actions/github";
|
||||
import { version } from "docker-compose";
|
||||
import { COMPOSE_VERSION_LATEST, Inputs } from "./input.service.js";
|
||||
import { ManualInstallerAdapter } from "./installer-adapter/manual-installer-adapter.js";
|
||||
import { COMPOSE_VERSION_LATEST, type Inputs } from "./input.service.js";
|
||||
import type { ManualInstallerAdapter } from "./installer-adapter/manual-installer-adapter.js";
|
||||
|
||||
export type InstallInputs = {
|
||||
composeVersion: Inputs["composeVersion"];
|
||||
cwd: Inputs["cwd"];
|
||||
githubToken: Inputs["githubToken"];
|
||||
composeVersion: Inputs["composeVersion"];
|
||||
cwd: Inputs["cwd"];
|
||||
githubToken: Inputs["githubToken"];
|
||||
};
|
||||
|
||||
export type VersionInputs = {
|
||||
cwd: Inputs["cwd"];
|
||||
cwd: Inputs["cwd"];
|
||||
};
|
||||
|
||||
export class DockerComposeInstallerService {
|
||||
constructor(private readonly manualInstallerAdapter: ManualInstallerAdapter) {}
|
||||
constructor(
|
||||
private readonly manualInstallerAdapter: ManualInstallerAdapter,
|
||||
) {}
|
||||
|
||||
async install({ composeVersion, cwd, githubToken }: InstallInputs): Promise<string> {
|
||||
const currentVersion = await this.version({ cwd });
|
||||
async install({
|
||||
composeVersion,
|
||||
cwd,
|
||||
githubToken,
|
||||
}: InstallInputs): Promise<string> {
|
||||
const currentVersion = await this.version({ cwd });
|
||||
|
||||
const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null;
|
||||
const normalizedRequestedVersion = composeVersion
|
||||
? this.normalizeVersion(composeVersion)
|
||||
: null;
|
||||
const normalizedCurrentVersion = currentVersion
|
||||
? this.normalizeVersion(currentVersion)
|
||||
: null;
|
||||
const normalizedRequestedVersion = composeVersion
|
||||
? this.normalizeVersion(composeVersion)
|
||||
: null;
|
||||
|
||||
const needsInstall =
|
||||
!currentVersion ||
|
||||
(composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion);
|
||||
if (!needsInstall) {
|
||||
return currentVersion;
|
||||
}
|
||||
const needsInstall =
|
||||
!currentVersion ||
|
||||
(composeVersion &&
|
||||
normalizedRequestedVersion !== normalizedCurrentVersion);
|
||||
if (!needsInstall) {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
let targetVersion = composeVersion || COMPOSE_VERSION_LATEST;
|
||||
let targetVersion = composeVersion || COMPOSE_VERSION_LATEST;
|
||||
|
||||
if (targetVersion === COMPOSE_VERSION_LATEST) {
|
||||
if (!githubToken) {
|
||||
throw new Error("GitHub token is required to install the latest version");
|
||||
}
|
||||
targetVersion = await this.getLatestVersion(githubToken);
|
||||
}
|
||||
if (targetVersion === COMPOSE_VERSION_LATEST) {
|
||||
if (!githubToken) {
|
||||
throw new Error(
|
||||
"GitHub token is required to install the latest version",
|
||||
);
|
||||
}
|
||||
targetVersion = await this.getLatestVersion(githubToken);
|
||||
}
|
||||
|
||||
await this.installVersion(targetVersion);
|
||||
await this.installVersion(targetVersion);
|
||||
|
||||
const installedVersion = await this.version({ cwd });
|
||||
const installedVersion = await this.version({ cwd });
|
||||
|
||||
if (
|
||||
!installedVersion ||
|
||||
this.normalizeVersion(installedVersion) !== this.normalizeVersion(targetVersion)
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!installedVersion ||
|
||||
this.normalizeVersion(installedVersion) !==
|
||||
this.normalizeVersion(targetVersion)
|
||||
) {
|
||||
throw new Error(
|
||||
`Failed to install Docker Compose version "${targetVersion}", installed version is "${installedVersion ?? "unknown"}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return installedVersion;
|
||||
}
|
||||
return installedVersion;
|
||||
}
|
||||
|
||||
private async version({ cwd }: VersionInputs): Promise<string | null> {
|
||||
try {
|
||||
const result = await version({
|
||||
cwd,
|
||||
});
|
||||
return result.data.version;
|
||||
} catch {
|
||||
// If version check fails (e.g., Docker Compose not installed), return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private async version({ cwd }: VersionInputs): Promise<string | null> {
|
||||
try {
|
||||
const result = await version({
|
||||
cwd,
|
||||
});
|
||||
return result.data.version;
|
||||
} catch {
|
||||
// If version check fails (e.g., Docker Compose not installed), return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getLatestVersion(githubToken: string): Promise<string> {
|
||||
const octokit = github.getOctokit(githubToken);
|
||||
private async getLatestVersion(githubToken: string): Promise<string> {
|
||||
const octokit = github.getOctokit(githubToken);
|
||||
|
||||
const response = await octokit.rest.repos.getLatestRelease({
|
||||
owner: "docker",
|
||||
repo: "compose",
|
||||
});
|
||||
const response = await octokit.rest.repos.getLatestRelease({
|
||||
owner: "docker",
|
||||
repo: "compose",
|
||||
});
|
||||
|
||||
return response.data.tag_name;
|
||||
}
|
||||
return response.data.tag_name;
|
||||
}
|
||||
|
||||
private normalizeVersion(version: string): string {
|
||||
return version.replace(/^v/i, "");
|
||||
}
|
||||
private normalizeVersion(version: string): string {
|
||||
return version.replace(/^v/i, "");
|
||||
}
|
||||
|
||||
private async installVersion(version: string): Promise<void> {
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
case "darwin":
|
||||
await this.manualInstallerAdapter.install(version);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
}
|
||||
private async installVersion(version: string): Promise<void> {
|
||||
switch (process.platform) {
|
||||
case "linux":
|
||||
case "darwin":
|
||||
await this.manualInstallerAdapter.install(version);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,340 +1,391 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
import type {
|
||||
IDockerComposeLogOptions,
|
||||
IDockerComposeOptions,
|
||||
IDockerComposeResult,
|
||||
IDockerComposeLogOptions,
|
||||
IDockerComposeOptions,
|
||||
IDockerComposeResult,
|
||||
} from "docker-compose";
|
||||
|
||||
// Mock docker-compose before importing the module under test
|
||||
const upAllMock = jest.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
||||
const upAllMock =
|
||||
vi.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
||||
const upManyMock =
|
||||
jest.fn<(services: string[], options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
||||
const downMock = jest.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
||||
vi.fn<
|
||||
(
|
||||
services: string[],
|
||||
options: IDockerComposeOptions,
|
||||
) => Promise<IDockerComposeResult>
|
||||
>();
|
||||
const downMock =
|
||||
vi.fn<(options: IDockerComposeOptions) => Promise<IDockerComposeResult>>();
|
||||
const logsMock =
|
||||
jest.fn<
|
||||
(services: string[], options: IDockerComposeLogOptions) => Promise<IDockerComposeResult>
|
||||
>();
|
||||
vi.fn<
|
||||
(
|
||||
services: string[],
|
||||
options: IDockerComposeLogOptions,
|
||||
) => Promise<IDockerComposeResult>
|
||||
>();
|
||||
|
||||
jest.unstable_mockModule("docker-compose", () => ({
|
||||
upAll: upAllMock,
|
||||
upMany: upManyMock,
|
||||
down: downMock,
|
||||
logs: logsMock,
|
||||
vi.doMock("docker-compose", () => ({
|
||||
upAll: upAllMock,
|
||||
upMany: upManyMock,
|
||||
down: downMock,
|
||||
logs: logsMock,
|
||||
}));
|
||||
|
||||
// Dynamic import after mock setup
|
||||
const { DockerComposeService } = await import("./docker-compose.service.js");
|
||||
|
||||
describe("DockerComposeService", () => {
|
||||
let service: InstanceType<typeof DockerComposeService>;
|
||||
let service: InstanceType<typeof DockerComposeService>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new DockerComposeService();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
service = new DockerComposeService();
|
||||
});
|
||||
|
||||
describe("up", () => {
|
||||
it("should call up with correct options", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
describe("up", () => {
|
||||
it("should call up with correct options", 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(),
|
||||
};
|
||||
|
||||
upAllMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
upAllMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
|
||||
await service.up(upInputs);
|
||||
await service.up(upInputs);
|
||||
|
||||
expect(upAllMock).toHaveBeenCalledWith({
|
||||
composeOptions: [],
|
||||
commandOptions: [],
|
||||
config: ["docker-compose.yml"],
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
expect(upAllMock).toHaveBeenCalledWith({
|
||||
composeOptions: [],
|
||||
commandOptions: [],
|
||||
config: ["docker-compose.yml"],
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
|
||||
// Ensure callback is calling the service logger
|
||||
const callback = (upAllMock.mock.calls[0][0] as IDockerComposeOptions)?.callback;
|
||||
expect(callback).toBeDefined();
|
||||
// Ensure callback is calling the service logger
|
||||
const callback = (upAllMock.mock.calls[0][0] as IDockerComposeOptions)
|
||||
?.callback;
|
||||
expect(callback).toBeDefined();
|
||||
|
||||
const message = "test log output";
|
||||
const message = "test log output";
|
||||
|
||||
if (callback) {
|
||||
callback(Buffer.from(message));
|
||||
}
|
||||
if (callback) {
|
||||
callback(Buffer.from(message));
|
||||
}
|
||||
|
||||
expect(upInputs.serviceLogger).toHaveBeenCalledWith("test log output");
|
||||
});
|
||||
expect(upInputs.serviceLogger).toHaveBeenCalledWith("test log output");
|
||||
});
|
||||
|
||||
it("should call up with specific docker flags", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: ["--context", "dev"],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
it("should call up with specific docker flags", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: ["--context", "dev"],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: vi.fn(),
|
||||
};
|
||||
|
||||
upAllMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
upAllMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
|
||||
await service.up(upInputs);
|
||||
await service.up(upInputs);
|
||||
|
||||
expect(upAllMock).toHaveBeenCalledWith({
|
||||
composeOptions: [],
|
||||
commandOptions: [],
|
||||
config: ["docker-compose.yml"],
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: ["--context", "dev"],
|
||||
},
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
});
|
||||
expect(upAllMock).toHaveBeenCalledWith({
|
||||
composeOptions: [],
|
||||
commandOptions: [],
|
||||
config: ["docker-compose.yml"],
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: ["--context", "dev"],
|
||||
},
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("should call up with specific services", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["helloworld2", "helloworld3"],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: ["--build"],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
it("should call up with specific services", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["helloworld2", "helloworld3"],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: ["--build"],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: vi.fn(),
|
||||
};
|
||||
|
||||
upManyMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
upManyMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
|
||||
await service.up(upInputs);
|
||||
await service.up(upInputs);
|
||||
|
||||
expect(upManyMock).toHaveBeenCalledWith(["helloworld2", "helloworld3"], {
|
||||
composeOptions: [],
|
||||
commandOptions: ["--build"],
|
||||
config: ["docker-compose.yml"],
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(upManyMock).toHaveBeenCalledWith(["helloworld2", "helloworld3"], {
|
||||
composeOptions: [],
|
||||
commandOptions: ["--build"],
|
||||
config: ["docker-compose.yml"],
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw formatted error when upAll fails with docker-compose result", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
it("should throw formatted error when upAll fails with docker-compose result", 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: "Error: unable to pull image\nfailed to resolve reference",
|
||||
out: "",
|
||||
};
|
||||
const dockerComposeError = {
|
||||
exitCode: 1,
|
||||
err: "Error: unable to pull image\nfailed to resolve reference",
|
||||
out: "",
|
||||
};
|
||||
|
||||
upAllMock.mockRejectedValue(dockerComposeError);
|
||||
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");
|
||||
});
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw formatted error when upMany fails with docker-compose result", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
it("should throw formatted error when upMany fails with docker-compose result", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["web"],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: vi.fn(),
|
||||
};
|
||||
|
||||
const dockerComposeError = {
|
||||
exitCode: 1,
|
||||
err: "Service 'web' failed to start",
|
||||
out: "Starting web...",
|
||||
};
|
||||
const dockerComposeError = {
|
||||
exitCode: 1,
|
||||
err: "Service 'web' failed to start",
|
||||
out: "Starting web...",
|
||||
};
|
||||
|
||||
upManyMock.mockRejectedValue(dockerComposeError);
|
||||
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");
|
||||
await expect(service.up(upInputs)).rejects.toThrow("Starting web...");
|
||||
});
|
||||
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",
|
||||
);
|
||||
await expect(service.up(upInputs)).rejects.toThrow("Starting web...");
|
||||
});
|
||||
|
||||
it("should pass through docker-compose result without exit code", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
it("should pass through docker-compose result without exit code", 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: null,
|
||||
err: "Some error without exit code",
|
||||
out: "",
|
||||
};
|
||||
const dockerComposeError = {
|
||||
exitCode: null,
|
||||
err: "Some error without exit code",
|
||||
out: "",
|
||||
};
|
||||
|
||||
upAllMock.mockRejectedValue(dockerComposeError);
|
||||
upAllMock.mockRejectedValue(dockerComposeError);
|
||||
|
||||
await expect(service.up(upInputs)).rejects.toThrow("Some error without exit code");
|
||||
});
|
||||
await expect(service.up(upInputs)).rejects.toThrow(
|
||||
"Some error without exit code",
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass through standard Error objects", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
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 standardError = new Error("Standard error message");
|
||||
upAllMock.mockRejectedValue(standardError);
|
||||
const dockerComposeError = {
|
||||
exitCode: 1,
|
||||
err: undefined,
|
||||
out: undefined,
|
||||
};
|
||||
|
||||
await expect(service.up(upInputs)).rejects.toThrow("Standard error message");
|
||||
});
|
||||
upAllMock.mockRejectedValue(dockerComposeError);
|
||||
|
||||
it("should pass through error strings", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
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:",
|
||||
);
|
||||
});
|
||||
|
||||
const unknownError = "Some unknown error";
|
||||
upAllMock.mockRejectedValue(unknownError);
|
||||
it("should pass through standard Error objects", 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(),
|
||||
};
|
||||
|
||||
await expect(service.up(upInputs)).rejects.toThrow("Some unknown error");
|
||||
});
|
||||
const standardError = new Error("Standard error message");
|
||||
upAllMock.mockRejectedValue(standardError);
|
||||
|
||||
it("should handle unknown error types gracefully", async () => {
|
||||
const upInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
upFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
await expect(service.up(upInputs)).rejects.toThrow(
|
||||
"Standard error message",
|
||||
);
|
||||
});
|
||||
|
||||
const unknownError = { unexpected: "error format" };
|
||||
upAllMock.mockRejectedValue(unknownError);
|
||||
it("should pass through error strings", 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(),
|
||||
};
|
||||
|
||||
await expect(service.up(upInputs)).rejects.toThrow(JSON.stringify(unknownError));
|
||||
});
|
||||
});
|
||||
const unknownError = "Some unknown error";
|
||||
upAllMock.mockRejectedValue(unknownError);
|
||||
|
||||
describe("down", () => {
|
||||
it("should call down with correct options", async () => {
|
||||
const downInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
downFlags: ["--volumes", "--remove-orphans"],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
await expect(service.up(upInputs)).rejects.toThrow("Some unknown error");
|
||||
});
|
||||
|
||||
downMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
it("should handle unknown error types gracefully", 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(),
|
||||
};
|
||||
|
||||
await service.down(downInputs);
|
||||
const unknownError = { unexpected: "error format" };
|
||||
upAllMock.mockRejectedValue(unknownError);
|
||||
|
||||
expect(downMock).toHaveBeenCalledWith({
|
||||
composeOptions: [],
|
||||
commandOptions: ["--volumes", "--remove-orphans"],
|
||||
config: [],
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
});
|
||||
await expect(service.up(upInputs)).rejects.toThrow(
|
||||
JSON.stringify(unknownError),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw formatted error when down fails with docker-compose result", async () => {
|
||||
const downInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
downFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: jest.fn(),
|
||||
};
|
||||
describe("down", () => {
|
||||
it("should call down with correct options", async () => {
|
||||
const downInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
downFlags: ["--volumes", "--remove-orphans"],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: vi.fn(),
|
||||
};
|
||||
|
||||
const dockerComposeError = {
|
||||
exitCode: 1,
|
||||
err: "Error stopping containers",
|
||||
out: "",
|
||||
};
|
||||
downMock.mockResolvedValue({ exitCode: 0, err: "", out: "" });
|
||||
|
||||
downMock.mockRejectedValue(dockerComposeError);
|
||||
await service.down(downInputs);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
expect(downMock).toHaveBeenCalledWith({
|
||||
composeOptions: [],
|
||||
commandOptions: ["--volumes", "--remove-orphans"],
|
||||
config: [],
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
cwd: "/current/working/dir",
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
describe("logs", () => {
|
||||
it("should call logs with correct options", async () => {
|
||||
const debugMock = jest.fn();
|
||||
const logsInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["helloworld2", "helloworld3"],
|
||||
composeFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: debugMock,
|
||||
};
|
||||
it("should throw formatted error when down fails with docker-compose result", async () => {
|
||||
const downInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: [] as string[],
|
||||
composeFlags: [] as string[],
|
||||
downFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: vi.fn(),
|
||||
};
|
||||
|
||||
logsMock.mockResolvedValue({ exitCode: 0, err: "", out: "logs" });
|
||||
const dockerComposeError = {
|
||||
exitCode: 1,
|
||||
err: "Error stopping containers",
|
||||
out: "",
|
||||
};
|
||||
|
||||
await service.logs(logsInputs);
|
||||
downMock.mockRejectedValue(dockerComposeError);
|
||||
|
||||
expect(logsMock).toHaveBeenCalledWith(["helloworld2", "helloworld3"], {
|
||||
composeOptions: [],
|
||||
config: ["docker-compose.yml"],
|
||||
cwd: "/current/working/dir",
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
follow: false,
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logs", () => {
|
||||
it("should call logs with correct options", async () => {
|
||||
const debugMock = vi.fn();
|
||||
const logsInputs = {
|
||||
dockerFlags: [] as string[],
|
||||
composeFiles: ["docker-compose.yml"],
|
||||
services: ["helloworld2", "helloworld3"],
|
||||
composeFlags: [] as string[],
|
||||
cwd: "/current/working/dir",
|
||||
serviceLogger: debugMock,
|
||||
};
|
||||
|
||||
logsMock.mockResolvedValue({ exitCode: 0, err: "", out: "logs" });
|
||||
|
||||
await service.logs(logsInputs);
|
||||
|
||||
expect(logsMock).toHaveBeenCalledWith(["helloworld2", "helloworld3"], {
|
||||
composeOptions: [],
|
||||
config: ["docker-compose.yml"],
|
||||
cwd: "/current/working/dir",
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: [],
|
||||
},
|
||||
follow: false,
|
||||
callback: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,148 +1,153 @@
|
||||
import {
|
||||
down,
|
||||
IDockerComposeLogOptions,
|
||||
IDockerComposeOptions,
|
||||
IDockerComposeResult,
|
||||
logs,
|
||||
upAll,
|
||||
upMany,
|
||||
down,
|
||||
type IDockerComposeLogOptions,
|
||||
type IDockerComposeOptions,
|
||||
type IDockerComposeResult,
|
||||
logs,
|
||||
upAll,
|
||||
upMany,
|
||||
} from "docker-compose";
|
||||
import { Inputs } from "./input.service.js";
|
||||
import type { Inputs } from "./input.service.js";
|
||||
|
||||
type OptionsInputs = {
|
||||
dockerFlags: Inputs["dockerFlags"];
|
||||
composeFiles: Inputs["composeFiles"];
|
||||
composeFlags: Inputs["composeFlags"];
|
||||
cwd: Inputs["cwd"];
|
||||
serviceLogger: (message: string) => void;
|
||||
dockerFlags: Inputs["dockerFlags"];
|
||||
composeFiles: Inputs["composeFiles"];
|
||||
composeFlags: Inputs["composeFlags"];
|
||||
cwd: Inputs["cwd"];
|
||||
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"] };
|
||||
|
||||
export class DockerComposeService {
|
||||
async up({ upFlags, services, ...optionsInputs }: UpInputs): Promise<void> {
|
||||
const options: IDockerComposeOptions = {
|
||||
...this.getCommonOptions(optionsInputs),
|
||||
commandOptions: upFlags,
|
||||
};
|
||||
async up({ upFlags, services, ...optionsInputs }: UpInputs): Promise<void> {
|
||||
const options: IDockerComposeOptions = {
|
||||
...this.getCommonOptions(optionsInputs),
|
||||
commandOptions: upFlags,
|
||||
};
|
||||
|
||||
try {
|
||||
if (services.length > 0) {
|
||||
await upMany(services, options);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (services.length > 0) {
|
||||
await upMany(services, options);
|
||||
return;
|
||||
}
|
||||
|
||||
await upAll(options);
|
||||
} catch (error) {
|
||||
throw this.formatDockerComposeError(error);
|
||||
}
|
||||
}
|
||||
await upAll(options);
|
||||
} catch (error) {
|
||||
throw this.formatDockerComposeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async down({ downFlags, ...optionsInputs }: DownInputs): Promise<void> {
|
||||
const options: IDockerComposeOptions = {
|
||||
...this.getCommonOptions(optionsInputs),
|
||||
commandOptions: downFlags,
|
||||
};
|
||||
async down({ downFlags, ...optionsInputs }: DownInputs): Promise<void> {
|
||||
const options: IDockerComposeOptions = {
|
||||
...this.getCommonOptions(optionsInputs),
|
||||
commandOptions: downFlags,
|
||||
};
|
||||
|
||||
try {
|
||||
await down(options);
|
||||
} catch (error) {
|
||||
throw this.formatDockerComposeError(error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await down(options);
|
||||
} catch (error) {
|
||||
throw this.formatDockerComposeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async logs({ services, ...optionsInputs }: LogsInputs): Promise<{
|
||||
error: string;
|
||||
output: string;
|
||||
}> {
|
||||
const options: IDockerComposeLogOptions = {
|
||||
...this.getCommonOptions(optionsInputs),
|
||||
follow: false,
|
||||
};
|
||||
async logs({ services, ...optionsInputs }: LogsInputs): Promise<{
|
||||
error: string;
|
||||
output: string;
|
||||
}> {
|
||||
const options: IDockerComposeLogOptions = {
|
||||
...this.getCommonOptions(optionsInputs),
|
||||
follow: false,
|
||||
};
|
||||
|
||||
const { err, out } = await logs(services, options);
|
||||
const { err, out } = await logs(services, options);
|
||||
|
||||
return {
|
||||
error: err,
|
||||
output: out,
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: err,
|
||||
output: out,
|
||||
};
|
||||
}
|
||||
|
||||
private getCommonOptions({
|
||||
dockerFlags,
|
||||
composeFiles,
|
||||
composeFlags,
|
||||
cwd,
|
||||
serviceLogger,
|
||||
}: OptionsInputs): IDockerComposeOptions {
|
||||
return {
|
||||
config: composeFiles,
|
||||
composeOptions: composeFlags,
|
||||
cwd: cwd,
|
||||
callback: (chunk) => serviceLogger(chunk.toString()),
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: dockerFlags,
|
||||
},
|
||||
};
|
||||
}
|
||||
private getCommonOptions({
|
||||
dockerFlags,
|
||||
composeFiles,
|
||||
composeFlags,
|
||||
cwd,
|
||||
serviceLogger,
|
||||
}: OptionsInputs): IDockerComposeOptions {
|
||||
return {
|
||||
config: composeFiles,
|
||||
composeOptions: composeFlags,
|
||||
cwd: cwd,
|
||||
callback: (chunk) => serviceLogger(chunk.toString()),
|
||||
executable: {
|
||||
executablePath: "docker",
|
||||
options: dockerFlags,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats docker-compose errors into proper Error objects with readable messages
|
||||
*/
|
||||
private formatDockerComposeError(error: unknown): Error {
|
||||
// If it's already an Error, return it
|
||||
if (error instanceof Error) {
|
||||
return error;
|
||||
}
|
||||
/**
|
||||
* Formats docker-compose errors into proper Error objects with readable messages
|
||||
*/
|
||||
private formatDockerComposeError(error: unknown): Error {
|
||||
// If it's already an Error, return it
|
||||
if (error instanceof Error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Handle docker-compose result objects
|
||||
if (this.isDockerComposeResult(error)) {
|
||||
const parts: string[] = [];
|
||||
// Handle docker-compose result objects
|
||||
if (this.isDockerComposeResult(error)) {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Add exit code information
|
||||
if (error.exitCode !== null) {
|
||||
parts.push(`Docker Compose command failed with exit code ${error.exitCode}`);
|
||||
} else {
|
||||
parts.push("Docker Compose command failed");
|
||||
}
|
||||
// Add exit code information
|
||||
if (error.exitCode !== null) {
|
||||
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 && error.err.trim()) {
|
||||
parts.push("\nError output:");
|
||||
parts.push(error.err.trim());
|
||||
}
|
||||
// Add error stream output if available
|
||||
if (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 && error.out.trim() && error.out !== error.err) {
|
||||
parts.push("\nStandard output:");
|
||||
parts.push(error.out.trim());
|
||||
}
|
||||
// Add standard output if available and different from error output
|
||||
if (error.out?.trim() && error.out !== error.err) {
|
||||
parts.push("\nStandard output:");
|
||||
parts.push(error.out.trim());
|
||||
}
|
||||
|
||||
return new Error(parts.join("\n"));
|
||||
}
|
||||
return new Error(parts.join("\n"));
|
||||
}
|
||||
|
||||
// Handle string errors
|
||||
if (typeof error === "string") {
|
||||
return new Error(error);
|
||||
}
|
||||
// Handle string errors
|
||||
if (typeof error === "string") {
|
||||
return new Error(error);
|
||||
}
|
||||
|
||||
// Fallback for unknown error types
|
||||
return new Error(JSON.stringify(error));
|
||||
}
|
||||
// Fallback for unknown error types
|
||||
return new Error(JSON.stringify(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a docker-compose result
|
||||
*/
|
||||
private isDockerComposeResult(error: unknown): error is IDockerComposeResult {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"exitCode" in error &&
|
||||
"err" in error &&
|
||||
"out" in error
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Type guard to check if an object is a docker-compose result
|
||||
*/
|
||||
private isDockerComposeResult(error: unknown): error is IDockerComposeResult {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"exitCode" in error &&
|
||||
"err" in error &&
|
||||
"out" in error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,25 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
// Mock @actions/core before importing the module under test
|
||||
const getInputMock = jest.fn<(name: string, options?: { required?: boolean }) => string>();
|
||||
const getInputMock =
|
||||
vi.fn<(name: string, options?: { required?: boolean }) => string>();
|
||||
const getMultilineInputMock =
|
||||
jest.fn<(name: string, options?: { required?: boolean }) => string[]>();
|
||||
vi.fn<(name: string, options?: { required?: boolean }) => string[]>();
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
getInput: getInputMock,
|
||||
getMultilineInput: getMultilineInputMock,
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
vi.doMock("@actions/core", () => ({
|
||||
getInput: getInputMock,
|
||||
getMultilineInput: getMultilineInputMock,
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock node:fs
|
||||
const existsSyncMock = jest.fn<(path: string) => boolean>();
|
||||
const existsSyncMock = vi.fn<(path: string) => boolean>();
|
||||
|
||||
jest.unstable_mockModule("node:fs", () => ({
|
||||
existsSync: existsSyncMock,
|
||||
default: { existsSync: existsSyncMock },
|
||||
vi.doMock("node:fs", () => ({
|
||||
existsSync: existsSyncMock,
|
||||
default: { existsSync: existsSyncMock },
|
||||
}));
|
||||
|
||||
// Dynamic imports after mock setup
|
||||
@ -26,369 +27,373 @@ const { InputService, InputNames } = await import("./input.service.js");
|
||||
const { LogLevel } = await import("./logger.service.js");
|
||||
|
||||
describe("InputService", () => {
|
||||
let service: InstanceType<typeof InputService>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
service = new InputService();
|
||||
});
|
||||
|
||||
describe("getInputs", () => {
|
||||
describe("docker-flags", () => {
|
||||
it("should return given docker-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.DockerFlags:
|
||||
return "docker-flag1 docker-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.dockerFlags).toEqual(["docker-flag1", "docker-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no docker-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.dockerFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("composeFiles", () => {
|
||||
it("should return given composeFiles input", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1", "file2"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["file1", "file2"]);
|
||||
});
|
||||
|
||||
it("should ignore empty compose file entries", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return [" ", "file1"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["file1"]);
|
||||
});
|
||||
|
||||
it("should accept compose file when it exists at the original path", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["./compose.yml"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "/current/working/directory";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockImplementation((file) => file === "./compose.yml");
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["./compose.yml"]);
|
||||
});
|
||||
|
||||
it("should accept OCI compose files without checking the file system", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["oci://docker.io/hoverkraft/compose-app:latest"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "/current/working/directory";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["oci://docker.io/hoverkraft/compose-app:latest"]);
|
||||
expect(existsSyncMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throws an error when a compose file does not exist", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1", "file2"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "/current/working/directory";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockImplementation((file) => file === "/current/working/directory/file1");
|
||||
|
||||
expect(() => service.getInputs()).toThrow(
|
||||
'Compose file not found in "/current/working/directory/file2", "file2"'
|
||||
);
|
||||
});
|
||||
|
||||
it("should throws an error when no composeFiles input", () => {
|
||||
getMultilineInputMock.mockReturnValue([]);
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
expect(() => service.getInputs()).toThrow("No compose files found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("services", () => {
|
||||
it("should return given services input", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Services:
|
||||
return ["service1", "service2"];
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.services).toEqual(["service1", "service2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose-flags", () => {
|
||||
it("should return given compose-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFlags:
|
||||
return "compose-flag1 compose-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFlags).toEqual(["compose-flag1", "compose-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no compose-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("up-flags", () => {
|
||||
it("should return given up-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.UpFlags:
|
||||
return "up-flag1 up-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.upFlags).toEqual(["up-flag1", "up-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no up-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.upFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("down-flags", () => {
|
||||
it("should return given down-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.DownFlags:
|
||||
return "down-flag1 down-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.downFlags).toEqual(["down-flag1", "down-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no down-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.downFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cwd", () => {
|
||||
it("should return given cwd input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "cwd";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.cwd).toEqual("cwd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose-version", () => {
|
||||
it("should return given compose-version input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeVersion:
|
||||
return "compose-version";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeVersion).toEqual("compose-version");
|
||||
});
|
||||
});
|
||||
|
||||
describe("services-log-level", () => {
|
||||
it("should return given services-log-level input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ServiceLogLevel:
|
||||
return "info";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
expect(inputs.serviceLogLevel).toEqual(LogLevel.Info);
|
||||
});
|
||||
|
||||
it("should return default services-log-level input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ServiceLogLevel:
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
expect(inputs.serviceLogLevel).toEqual(LogLevel.Debug);
|
||||
});
|
||||
|
||||
it("should throw an error when services-log-level input is invalid", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ServiceLogLevel:
|
||||
return "invalid-log-level";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
expect(() => service.getInputs()).toThrow(
|
||||
'Invalid service log level "invalid-log-level". Valid values are: debug, info'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
let service: InstanceType<typeof InputService>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
service = new InputService();
|
||||
});
|
||||
|
||||
describe("getInputs", () => {
|
||||
describe("docker-flags", () => {
|
||||
it("should return given docker-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.DockerFlags:
|
||||
return "docker-flag1 docker-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.dockerFlags).toEqual(["docker-flag1", "docker-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no docker-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.dockerFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("composeFiles", () => {
|
||||
it("should return given composeFiles input", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1", "file2"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["file1", "file2"]);
|
||||
});
|
||||
|
||||
it("should ignore empty compose file entries", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return [" ", "file1"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["file1"]);
|
||||
});
|
||||
|
||||
it("should accept compose file when it exists at the original path", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["./compose.yml"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "/current/working/directory";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockImplementation((file) => file === "./compose.yml");
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual(["./compose.yml"]);
|
||||
});
|
||||
|
||||
it("should accept OCI compose files without checking the file system", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["oci://docker.io/hoverkraft/compose-app:latest"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "/current/working/directory";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFiles).toEqual([
|
||||
"oci://docker.io/hoverkraft/compose-app:latest",
|
||||
]);
|
||||
expect(existsSyncMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throws an error when a compose file does not exist", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1", "file2"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "/current/working/directory";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockImplementation(
|
||||
(file) => file === "/current/working/directory/file1",
|
||||
);
|
||||
|
||||
expect(() => service.getInputs()).toThrow(
|
||||
'Compose file not found in "/current/working/directory/file2", "file2"',
|
||||
);
|
||||
});
|
||||
|
||||
it("should throws an error when no composeFiles input", () => {
|
||||
getMultilineInputMock.mockReturnValue([]);
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
expect(() => service.getInputs()).toThrow("No compose files found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("services", () => {
|
||||
it("should return given services input", () => {
|
||||
getMultilineInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Services:
|
||||
return ["service1", "service2"];
|
||||
case InputNames.ComposeFile:
|
||||
return ["file1"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
getInputMock.mockReturnValue("");
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.services).toEqual(["service1", "service2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose-flags", () => {
|
||||
it("should return given compose-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeFlags:
|
||||
return "compose-flag1 compose-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFlags).toEqual(["compose-flag1", "compose-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no compose-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("up-flags", () => {
|
||||
it("should return given up-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.UpFlags:
|
||||
return "up-flag1 up-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.upFlags).toEqual(["up-flag1", "up-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no up-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.upFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("down-flags", () => {
|
||||
it("should return given down-flags input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.DownFlags:
|
||||
return "down-flag1 down-flag2";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.downFlags).toEqual(["down-flag1", "down-flag2"]);
|
||||
});
|
||||
|
||||
it("should return empty array when no down-flags input", () => {
|
||||
getInputMock.mockReturnValue("");
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.downFlags).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cwd", () => {
|
||||
it("should return given cwd input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.Cwd:
|
||||
return "cwd";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.cwd).toEqual("cwd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("compose-version", () => {
|
||||
it("should return given compose-version input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ComposeVersion:
|
||||
return "compose-version";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
|
||||
expect(inputs.composeVersion).toEqual("compose-version");
|
||||
});
|
||||
});
|
||||
|
||||
describe("services-log-level", () => {
|
||||
it("should return given services-log-level input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ServiceLogLevel:
|
||||
return "info";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
expect(inputs.serviceLogLevel).toEqual(LogLevel.Info);
|
||||
});
|
||||
|
||||
it("should return default services-log-level input", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ServiceLogLevel:
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
const inputs = service.getInputs();
|
||||
expect(inputs.serviceLogLevel).toEqual(LogLevel.Debug);
|
||||
});
|
||||
|
||||
it("should throw an error when services-log-level input is invalid", () => {
|
||||
getInputMock.mockImplementation((inputName) => {
|
||||
switch (inputName) {
|
||||
case InputNames.ServiceLogLevel:
|
||||
return "invalid-log-level";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
existsSyncMock.mockReturnValue(true);
|
||||
|
||||
expect(() => service.getInputs()).toThrow(
|
||||
'Invalid service log level "invalid-log-level". Valid values are: debug, info',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,135 +4,144 @@ import { join } from "node:path";
|
||||
import { LogLevel } from "./logger.service.js";
|
||||
|
||||
export type Inputs = {
|
||||
dockerFlags: string[];
|
||||
composeFiles: string[];
|
||||
services: string[];
|
||||
composeFlags: string[];
|
||||
upFlags: string[];
|
||||
downFlags: string[];
|
||||
cwd: string;
|
||||
composeVersion: string | null;
|
||||
githubToken: string | null;
|
||||
serviceLogLevel: LogLevel;
|
||||
dockerFlags: string[];
|
||||
composeFiles: string[];
|
||||
services: string[];
|
||||
composeFlags: string[];
|
||||
upFlags: string[];
|
||||
downFlags: string[];
|
||||
cwd: string;
|
||||
composeVersion: string | null;
|
||||
githubToken: string | null;
|
||||
serviceLogLevel: LogLevel;
|
||||
};
|
||||
|
||||
export enum InputNames {
|
||||
DockerFlags = "docker-flags",
|
||||
ComposeFile = "compose-file",
|
||||
Services = "services",
|
||||
ComposeFlags = "compose-flags",
|
||||
UpFlags = "up-flags",
|
||||
DownFlags = "down-flags",
|
||||
Cwd = "cwd",
|
||||
ComposeVersion = "compose-version",
|
||||
GithubToken = "github-token",
|
||||
ServiceLogLevel = "services-log-level",
|
||||
DockerFlags = "docker-flags",
|
||||
ComposeFile = "compose-file",
|
||||
Services = "services",
|
||||
ComposeFlags = "compose-flags",
|
||||
UpFlags = "up-flags",
|
||||
DownFlags = "down-flags",
|
||||
Cwd = "cwd",
|
||||
ComposeVersion = "compose-version",
|
||||
GithubToken = "github-token",
|
||||
ServiceLogLevel = "services-log-level",
|
||||
}
|
||||
|
||||
export const COMPOSE_VERSION_LATEST = "latest";
|
||||
|
||||
export class InputService {
|
||||
getInputs(): Inputs {
|
||||
return {
|
||||
dockerFlags: this.getDockerFlags(),
|
||||
composeFiles: this.getComposeFiles(),
|
||||
services: this.getServices(),
|
||||
composeFlags: this.getComposeFlags(),
|
||||
upFlags: this.getUpFlags(),
|
||||
downFlags: this.getDownFlags(),
|
||||
cwd: this.getCwd(),
|
||||
composeVersion: this.getComposeVersion(),
|
||||
githubToken: this.getGithubToken(),
|
||||
serviceLogLevel: this.getServiceLogLevel(),
|
||||
};
|
||||
}
|
||||
getInputs(): Inputs {
|
||||
return {
|
||||
dockerFlags: this.getDockerFlags(),
|
||||
composeFiles: this.getComposeFiles(),
|
||||
services: this.getServices(),
|
||||
composeFlags: this.getComposeFlags(),
|
||||
upFlags: this.getUpFlags(),
|
||||
downFlags: this.getDownFlags(),
|
||||
cwd: this.getCwd(),
|
||||
composeVersion: this.getComposeVersion(),
|
||||
githubToken: this.getGithubToken(),
|
||||
serviceLogLevel: this.getServiceLogLevel(),
|
||||
};
|
||||
}
|
||||
|
||||
private getDockerFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.DockerFlags));
|
||||
}
|
||||
private getDockerFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.DockerFlags));
|
||||
}
|
||||
|
||||
private getComposeFiles(): string[] {
|
||||
const cwd = this.getCwd();
|
||||
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter((composeFile: string) => {
|
||||
const trimmedComposeFile = composeFile.trim();
|
||||
private getComposeFiles(): string[] {
|
||||
const cwd = this.getCwd();
|
||||
const composeFiles = getMultilineInput(InputNames.ComposeFile).filter(
|
||||
(composeFile: string) => {
|
||||
const trimmedComposeFile = composeFile.trim();
|
||||
|
||||
if (!trimmedComposeFile.length) {
|
||||
return false;
|
||||
}
|
||||
if (!trimmedComposeFile.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trimmedComposeFile.startsWith("oci://")) {
|
||||
return true;
|
||||
}
|
||||
if (trimmedComposeFile.startsWith("oci://")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const possiblePaths = [join(cwd, composeFile), composeFile];
|
||||
const possiblePaths = [join(cwd, composeFile), composeFile];
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
if (existsSync(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const path of possiblePaths) {
|
||||
if (existsSync(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
if (!composeFiles.length) {
|
||||
throw new Error("No compose files found");
|
||||
}
|
||||
|
||||
return composeFiles;
|
||||
}
|
||||
return composeFiles;
|
||||
}
|
||||
|
||||
private getServices(): string[] {
|
||||
return getMultilineInput(InputNames.Services, { required: false });
|
||||
}
|
||||
private getServices(): string[] {
|
||||
return getMultilineInput(InputNames.Services, { required: false });
|
||||
}
|
||||
|
||||
private getComposeFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.ComposeFlags));
|
||||
}
|
||||
private getComposeFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.ComposeFlags));
|
||||
}
|
||||
|
||||
private getUpFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.UpFlags));
|
||||
}
|
||||
private getUpFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.UpFlags));
|
||||
}
|
||||
|
||||
private getDownFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.DownFlags));
|
||||
}
|
||||
private getDownFlags(): string[] {
|
||||
return this.parseFlags(getInput(InputNames.DownFlags));
|
||||
}
|
||||
|
||||
private parseFlags(flags: string | null): string[] {
|
||||
if (!flags) {
|
||||
return [];
|
||||
}
|
||||
private parseFlags(flags: string | null): string[] {
|
||||
if (!flags) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flags.trim().split(" ");
|
||||
}
|
||||
return flags.trim().split(" ");
|
||||
}
|
||||
|
||||
private getCwd(): string {
|
||||
return getInput(InputNames.Cwd);
|
||||
}
|
||||
private getCwd(): string {
|
||||
return getInput(InputNames.Cwd);
|
||||
}
|
||||
|
||||
private getComposeVersion(): string | null {
|
||||
return (
|
||||
getInput(InputNames.ComposeVersion, {
|
||||
required: false,
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
private getComposeVersion(): string | null {
|
||||
return (
|
||||
getInput(InputNames.ComposeVersion, {
|
||||
required: false,
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
|
||||
private getGithubToken(): string | null {
|
||||
return (
|
||||
getInput(InputNames.GithubToken, {
|
||||
required: false,
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
private getGithubToken(): string | null {
|
||||
return (
|
||||
getInput(InputNames.GithubToken, {
|
||||
required: false,
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
|
||||
private getServiceLogLevel(): 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(", ")}`
|
||||
);
|
||||
}
|
||||
return (configuredLevel as LogLevel) || LogLevel.Debug;
|
||||
}
|
||||
private getServiceLogLevel(): 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(", ")}`,
|
||||
);
|
||||
}
|
||||
return (configuredLevel as LogLevel) || LogLevel.Debug;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export interface DockerComposeInstallerAdapter {
|
||||
install(version: string): Promise<void>;
|
||||
install(version: string): Promise<void>;
|
||||
}
|
||||
|
||||
@ -1,196 +1,221 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
import type { ExecOptions } from "@actions/exec";
|
||||
import type { OutgoingHttpHeaders } from "node:http";
|
||||
|
||||
// Mock @actions/exec
|
||||
const execMock =
|
||||
jest.fn<(command: string, args?: string[], options?: ExecOptions) => Promise<number>>();
|
||||
vi.fn<
|
||||
(command: string, args?: string[], options?: ExecOptions) => Promise<number>
|
||||
>();
|
||||
|
||||
jest.unstable_mockModule("@actions/exec", () => ({
|
||||
exec: execMock,
|
||||
vi.doMock("@actions/exec", () => ({
|
||||
exec: execMock,
|
||||
}));
|
||||
|
||||
// Mock @actions/io
|
||||
const mkdirPMock = jest.fn<(fsPath: string) => Promise<void>>();
|
||||
const mkdirPMock = vi.fn<(fsPath: string) => Promise<void>>();
|
||||
|
||||
jest.unstable_mockModule("@actions/io", () => ({
|
||||
mkdirP: mkdirPMock,
|
||||
vi.doMock("@actions/io", () => ({
|
||||
mkdirP: mkdirPMock,
|
||||
}));
|
||||
|
||||
// Mock @actions/tool-cache
|
||||
const cacheFileMock =
|
||||
jest.fn<
|
||||
(
|
||||
sourceFile: string,
|
||||
targetFile: string,
|
||||
tool: string,
|
||||
version: string,
|
||||
arch?: string
|
||||
) => Promise<string>
|
||||
>();
|
||||
vi.fn<
|
||||
(
|
||||
sourceFile: string,
|
||||
targetFile: string,
|
||||
tool: string,
|
||||
version: string,
|
||||
arch?: string,
|
||||
) => Promise<string>
|
||||
>();
|
||||
const downloadToolMock =
|
||||
jest.fn<
|
||||
(url: string, dest?: string, auth?: string, headers?: OutgoingHttpHeaders) => Promise<string>
|
||||
>();
|
||||
vi.fn<
|
||||
(
|
||||
url: string,
|
||||
dest?: string,
|
||||
auth?: string,
|
||||
headers?: OutgoingHttpHeaders,
|
||||
) => Promise<string>
|
||||
>();
|
||||
|
||||
jest.unstable_mockModule("@actions/tool-cache", () => ({
|
||||
cacheFile: cacheFileMock,
|
||||
downloadTool: downloadToolMock,
|
||||
vi.doMock("@actions/tool-cache", () => ({
|
||||
cacheFile: cacheFileMock,
|
||||
downloadTool: downloadToolMock,
|
||||
}));
|
||||
|
||||
// Dynamic import after mock setup
|
||||
const { ManualInstallerAdapter } = await import("./manual-installer-adapter.js");
|
||||
const { ManualInstallerAdapter } = await import(
|
||||
"./manual-installer-adapter.js"
|
||||
);
|
||||
|
||||
const originalHome = process.env.HOME;
|
||||
const originalDockerConfig = process.env.DOCKER_CONFIG;
|
||||
|
||||
describe("ManualInstallerAdapter", () => {
|
||||
let adapter: InstanceType<typeof ManualInstallerAdapter>;
|
||||
let adapter: InstanceType<typeof ManualInstallerAdapter>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete process.env.DOCKER_CONFIG;
|
||||
adapter = new ManualInstallerAdapter();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
if (originalHome === undefined) {
|
||||
delete process.env.HOME;
|
||||
} else {
|
||||
process.env.HOME = originalHome;
|
||||
}
|
||||
if (originalDockerConfig === undefined) {
|
||||
delete process.env.DOCKER_CONFIG;
|
||||
} else {
|
||||
process.env.DOCKER_CONFIG = originalDockerConfig;
|
||||
}
|
||||
adapter = new ManualInstallerAdapter();
|
||||
});
|
||||
|
||||
describe("install", () => {
|
||||
it("should install docker compose correctly", async () => {
|
||||
// Arrange
|
||||
const version = "v2.29.0";
|
||||
describe("install", () => {
|
||||
it("should install docker compose correctly", async () => {
|
||||
// Arrange
|
||||
const version = "v2.29.0";
|
||||
|
||||
// Uname -s
|
||||
execMock.mockResolvedValueOnce(0);
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Uname -m
|
||||
execMock.mockResolvedValueOnce(0);
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
Object.defineProperty(process.env, "HOME", {
|
||||
value: "/home/test",
|
||||
});
|
||||
process.env.HOME = "/home/test";
|
||||
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
|
||||
// Assert
|
||||
expect(mkdirPMock).toHaveBeenCalledWith("docker-compose");
|
||||
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
expect(execMock).toHaveBeenNthCalledWith(2, "uname -m", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
// Assert
|
||||
expect(mkdirPMock).toHaveBeenCalledWith("docker-compose");
|
||||
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
expect(execMock).toHaveBeenNthCalledWith(2, "uname -m", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
|
||||
expect(downloadToolMock).toHaveBeenCalledWith(
|
||||
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose--",
|
||||
"/home/test/.docker/cli-plugins/docker-compose"
|
||||
);
|
||||
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",
|
||||
);
|
||||
|
||||
expect(cacheFileMock).toHaveBeenCalledWith(
|
||||
"/home/test/.docker/cli-plugins/docker-compose",
|
||||
"docker-compose",
|
||||
"docker-compose",
|
||||
version
|
||||
);
|
||||
});
|
||||
expect(cacheFileMock).toHaveBeenCalledWith(
|
||||
"/home/test/.docker/cli-plugins/docker-compose",
|
||||
"docker-compose",
|
||||
"docker-compose",
|
||||
version,
|
||||
);
|
||||
});
|
||||
|
||||
it("should use DOCKER_CONFIG when set", async () => {
|
||||
// Arrange
|
||||
const version = "v2.29.0";
|
||||
it("should use DOCKER_CONFIG when set", async () => {
|
||||
// Arrange
|
||||
const version = "v2.29.0";
|
||||
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
|
||||
return 0;
|
||||
});
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
|
||||
return 0;
|
||||
});
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
process.env.DOCKER_CONFIG = "/custom/docker";
|
||||
process.env.DOCKER_CONFIG = "/custom/docker";
|
||||
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
|
||||
// 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"
|
||||
);
|
||||
});
|
||||
// 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",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle version without 'v' prefix", async () => {
|
||||
// Arrange
|
||||
const version = "2.29.0";
|
||||
it("should handle version without 'v' prefix", async () => {
|
||||
// Arrange
|
||||
const version = "2.29.0";
|
||||
|
||||
// Uname -s
|
||||
execMock.mockResolvedValueOnce(0);
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Uname -m
|
||||
execMock.mockResolvedValueOnce(0);
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
Object.defineProperty(process.env, "HOME", {
|
||||
value: "/home/test",
|
||||
});
|
||||
process.env.HOME = "/home/test";
|
||||
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
|
||||
// Assert
|
||||
expect(mkdirPMock).toHaveBeenCalledWith("docker-compose");
|
||||
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
expect(execMock).toHaveBeenNthCalledWith(2, "uname -m", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
// Assert
|
||||
expect(mkdirPMock).toHaveBeenCalledWith("docker-compose");
|
||||
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
expect(execMock).toHaveBeenNthCalledWith(2, "uname -m", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
|
||||
expect(downloadToolMock).toHaveBeenCalledWith(
|
||||
"https://github.com/docker/compose/releases/download/v2.29.0/docker-compose--",
|
||||
"/home/test/.docker/cli-plugins/docker-compose"
|
||||
);
|
||||
});
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add 'v' prefix for 1.x versions", async () => {
|
||||
// Arrange
|
||||
const version = "1.29.0";
|
||||
it("should not add 'v' prefix for 1.x versions", async () => {
|
||||
// Arrange
|
||||
const version = "1.29.0";
|
||||
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
|
||||
return 0;
|
||||
});
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("Linux\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
|
||||
return 0;
|
||||
});
|
||||
execMock.mockImplementationOnce(async (_command, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from("x86_64\n"));
|
||||
return 0;
|
||||
});
|
||||
|
||||
delete process.env.DOCKER_CONFIG;
|
||||
Object.defineProperty(process.env, "HOME", {
|
||||
value: "/home/test",
|
||||
});
|
||||
delete process.env.DOCKER_CONFIG;
|
||||
process.env.HOME = "/home/test";
|
||||
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
// Act
|
||||
await adapter.install(version);
|
||||
|
||||
// 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"
|
||||
);
|
||||
});
|
||||
// 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",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if a command fails", async () => {
|
||||
// Arrange
|
||||
const version = "v2.29.0";
|
||||
it("should throw an error if a command fails", async () => {
|
||||
// Arrange
|
||||
const version = "v2.29.0";
|
||||
|
||||
// Uname -s
|
||||
execMock.mockResolvedValueOnce(1);
|
||||
// Uname -s
|
||||
execMock.mockResolvedValueOnce(1);
|
||||
|
||||
// Act
|
||||
await expect(adapter.install(version)).rejects.toThrow("Failed to run command: uname -s");
|
||||
// Act
|
||||
await expect(adapter.install(version)).rejects.toThrow(
|
||||
"Failed to run command: uname -s",
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
});
|
||||
});
|
||||
// Assert
|
||||
expect(execMock).toHaveBeenNthCalledWith(1, "uname -s", [], {
|
||||
listeners: { stdout: expect.any(Function) },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,59 +2,68 @@ import { exec } from "@actions/exec";
|
||||
import { mkdirP } from "@actions/io";
|
||||
import { basename } from "node:path";
|
||||
import { cacheFile, downloadTool } from "@actions/tool-cache";
|
||||
import { DockerComposeInstallerAdapter } from "./docker-compose-installer-adapter.js";
|
||||
import type { DockerComposeInstallerAdapter } from "./docker-compose-installer-adapter.js";
|
||||
|
||||
export class ManualInstallerAdapter implements DockerComposeInstallerAdapter {
|
||||
async install(version: string): Promise<void> {
|
||||
const dockerComposePluginPath = await this.getDockerComposePluginPath();
|
||||
async install(version: string): Promise<void> {
|
||||
const dockerComposePluginPath = await this.getDockerComposePluginPath();
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
await mkdirP(basename(dockerComposePluginPath));
|
||||
// Create the directory if it doesn't exist
|
||||
await mkdirP(basename(dockerComposePluginPath));
|
||||
|
||||
await this.downloadFile(version, dockerComposePluginPath);
|
||||
await exec(`chmod +x ${dockerComposePluginPath}`);
|
||||
await cacheFile(dockerComposePluginPath, "docker-compose", "docker-compose", version);
|
||||
}
|
||||
await this.downloadFile(version, dockerComposePluginPath);
|
||||
await exec(`chmod +x ${dockerComposePluginPath}`);
|
||||
await cacheFile(
|
||||
dockerComposePluginPath,
|
||||
"docker-compose",
|
||||
"docker-compose",
|
||||
version,
|
||||
);
|
||||
}
|
||||
|
||||
private async getDockerComposePluginPath(): Promise<string> {
|
||||
const dockerConfig = process.env.DOCKER_CONFIG || `${process.env.HOME}/.docker`;
|
||||
private async getDockerComposePluginPath(): Promise<string> {
|
||||
const dockerConfig =
|
||||
process.env.DOCKER_CONFIG || `${process.env.HOME}/.docker`;
|
||||
|
||||
const dockerComposePluginPath = `${dockerConfig}/cli-plugins/docker-compose`;
|
||||
return dockerComposePluginPath;
|
||||
}
|
||||
const dockerComposePluginPath = `${dockerConfig}/cli-plugins/docker-compose`;
|
||||
return dockerComposePluginPath;
|
||||
}
|
||||
|
||||
private async downloadFile(version: string, installerPath: string): Promise<void> {
|
||||
if (!version.startsWith("v") && parseInt(version.split(".")[0], 10) >= 2) {
|
||||
version = `v${version}`;
|
||||
}
|
||||
private async downloadFile(
|
||||
version: string,
|
||||
installerPath: string,
|
||||
): Promise<void> {
|
||||
if (!version.startsWith("v") && parseInt(version.split(".")[0], 10) >= 2) {
|
||||
version = `v${version}`;
|
||||
}
|
||||
|
||||
const system = await this.getSystem();
|
||||
const hardware = await this.getHardware();
|
||||
const system = await this.getSystem();
|
||||
const hardware = await this.getHardware();
|
||||
|
||||
const url = `https://github.com/docker/compose/releases/download/${version}/docker-compose-${system}-${hardware}`;
|
||||
await downloadTool(url, installerPath);
|
||||
}
|
||||
const url = `https://github.com/docker/compose/releases/download/${version}/docker-compose-${system}-${hardware}`;
|
||||
await downloadTool(url, installerPath);
|
||||
}
|
||||
|
||||
private async getSystem(): Promise<string> {
|
||||
return this.runCommand("uname -s");
|
||||
}
|
||||
private async getSystem(): Promise<string> {
|
||||
return this.runCommand("uname -s");
|
||||
}
|
||||
|
||||
private async getHardware(): Promise<string> {
|
||||
return this.runCommand("uname -m");
|
||||
}
|
||||
private async getHardware(): Promise<string> {
|
||||
return this.runCommand("uname -m");
|
||||
}
|
||||
|
||||
private async runCommand(command: string): Promise<string> {
|
||||
let output = "";
|
||||
const result = await exec(command, [], {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result !== 0) {
|
||||
throw new Error(`Failed to run command: ${command}`);
|
||||
}
|
||||
return output.trim();
|
||||
}
|
||||
private async runCommand(command: string): Promise<string> {
|
||||
let output = "";
|
||||
const result = await exec(command, [], {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result !== 0) {
|
||||
throw new Error(`Failed to run command: ${command}`);
|
||||
}
|
||||
return output.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,68 +1,68 @@
|
||||
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
// 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 = jest.fn();
|
||||
const infoMock = jest.fn();
|
||||
const debugMock = jest.fn();
|
||||
const warningMock = vi.fn();
|
||||
const infoMock = vi.fn();
|
||||
const debugMock = vi.fn();
|
||||
|
||||
jest.unstable_mockModule("@actions/core", () => ({
|
||||
warning: warningMock,
|
||||
info: infoMock,
|
||||
debug: debugMock,
|
||||
vi.doMock("@actions/core", () => ({
|
||||
warning: warningMock,
|
||||
info: infoMock,
|
||||
debug: debugMock,
|
||||
}));
|
||||
|
||||
// Dynamic import after mock setup
|
||||
const { LoggerService, LogLevel } = await import("./logger.service.js");
|
||||
|
||||
describe("LoggerService", () => {
|
||||
let loggerService: InstanceType<typeof LoggerService>;
|
||||
let loggerService: InstanceType<typeof LoggerService>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
loggerService = new LoggerService();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
loggerService = new LoggerService();
|
||||
});
|
||||
|
||||
describe("warn", () => {
|
||||
it("should call warning with the correct message", () => {
|
||||
const message = "This is a warning message";
|
||||
loggerService.warn(message);
|
||||
expect(warningMock).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
describe("warn", () => {
|
||||
it("should call warning with the correct message", () => {
|
||||
const message = "This is a warning message";
|
||||
loggerService.warn(message);
|
||||
expect(warningMock).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe("info", () => {
|
||||
it("should call info with the correct message", () => {
|
||||
const message = "This is an info message";
|
||||
loggerService.info(message);
|
||||
expect(infoMock).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
describe("info", () => {
|
||||
it("should call info with the correct message", () => {
|
||||
const message = "This is an info message";
|
||||
loggerService.info(message);
|
||||
expect(infoMock).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe("debug", () => {
|
||||
it("should call debug with the correct message", () => {
|
||||
const message = "This is a debug message";
|
||||
loggerService.debug(message);
|
||||
expect(debugMock).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
describe("debug", () => {
|
||||
it("should call debug with the correct message", () => {
|
||||
const message = "This is a debug message";
|
||||
loggerService.debug(message);
|
||||
expect(debugMock).toHaveBeenCalledWith(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getServiceLogger", () => {
|
||||
it("should return the correct logger function for debug level", () => {
|
||||
const logger = loggerService.getServiceLogger(LogLevel.Debug);
|
||||
expect(logger).toBe(loggerService.debug);
|
||||
});
|
||||
describe("getServiceLogger", () => {
|
||||
it("should return the correct logger function for debug level", () => {
|
||||
const logger = loggerService.getServiceLogger(LogLevel.Debug);
|
||||
expect(logger).toBe(loggerService.debug);
|
||||
});
|
||||
|
||||
it("should return the correct logger function for info level", () => {
|
||||
const logger = loggerService.getServiceLogger(LogLevel.Info);
|
||||
expect(logger).toBe(loggerService.info);
|
||||
});
|
||||
it("should return the correct logger function for info level", () => {
|
||||
const logger = loggerService.getServiceLogger(LogLevel.Info);
|
||||
expect(logger).toBe(loggerService.info);
|
||||
});
|
||||
|
||||
it("should default to info level if an unknown level is provided", () => {
|
||||
const logger = loggerService.getServiceLogger("unknown" as LogLevelType);
|
||||
expect(logger).toBe(loggerService.info);
|
||||
});
|
||||
});
|
||||
it("should default to info level if an unknown level is provided", () => {
|
||||
const logger = loggerService.getServiceLogger("unknown" as LogLevelType);
|
||||
expect(logger).toBe(loggerService.info);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
import { debug, info, warning } from "@actions/core";
|
||||
|
||||
export class LoggerService {
|
||||
warn(message: string): void {
|
||||
warning(message);
|
||||
}
|
||||
warn(message: string): void {
|
||||
warning(message);
|
||||
}
|
||||
|
||||
info(message: string): void {
|
||||
info(message);
|
||||
}
|
||||
info(message: string): void {
|
||||
info(message);
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
debug(message);
|
||||
}
|
||||
debug(message: string) {
|
||||
debug(message);
|
||||
}
|
||||
|
||||
getServiceLogger(level: LogLevel): (message: string) => void {
|
||||
switch (level) {
|
||||
case LogLevel.Debug:
|
||||
return this.debug;
|
||||
case LogLevel.Info:
|
||||
return this.info;
|
||||
default:
|
||||
return this.info;
|
||||
}
|
||||
}
|
||||
getServiceLogger(level: LogLevel): (message: string) => void {
|
||||
switch (level) {
|
||||
case LogLevel.Debug:
|
||||
return this.debug;
|
||||
case LogLevel.Info:
|
||||
return this.info;
|
||||
default:
|
||||
return this.info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
Debug = "debug",
|
||||
Info = "info",
|
||||
Debug = "debug",
|
||||
Info = "info",
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ 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
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "NodeNext",
|
||||
"baseUrl": "./",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"newLine": "lf",
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["./dist", "./node_modules", "./src/**/*.test.ts", "./coverage"],
|
||||
"include": ["./src/**/*"]
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"rootDir": "./src",
|
||||
"moduleResolution": "NodeNext",
|
||||
"baseUrl": "./",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"newLine": "lf",
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["./dist", "./node_modules", "./src/**/*.test.ts", "./coverage"],
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
|
||||
12
vitest.config.ts
Normal file
12
vitest.config.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
include: ["src/**/*.{ts,tsx,js,jsx}"],
|
||||
provider: "v8",
|
||||
},
|
||||
environment: "node",
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user