Compare commits

..

3 Commits

Author SHA1 Message Date
hoverkraft-bot[bot]
b160464f2e docs: update actions and workflows documentation
[skip ci]

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-03 15:26:54 +00:00
Emilien Escalle
4894d24920 chore: minor refactoring
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
2026-02-03 16:07:35 +01:00
Joao Pargana
c36b7c122a fix: install latest compose as fallback 2026-02-03 11:06:57 +01:00
4 changed files with 219 additions and 173 deletions

View File

@ -49,7 +49,7 @@ Some extra options can be passed to the `docker compose down` command using the
## Usage ## Usage
```yaml ```yaml
- uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3 - uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with: with:
# Additional options to pass to `docker` command. # Additional options to pass to `docker` command.
docker-flags: "" docker-flags: ""
@ -139,7 +139,7 @@ jobs:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2
- name: Run docker compose - name: Run docker compose
uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3 uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with: with:
compose-file: "./docker/docker-compose.yml" compose-file: "./docker/docker-compose.yml"
@ -153,7 +153,7 @@ jobs:
```yaml ```yaml
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2
- uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3 - uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with: with:
compose-file: "./docker/docker-compose.yml" compose-file: "./docker/docker-compose.yml"
env: env:
@ -168,7 +168,7 @@ Perform `docker compose up` to some given service instead of all of them
steps: steps:
# need checkout before using compose-action # need checkout before using compose-action
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3 - uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with: with:
compose-file: "./docker/docker-compose.yml" compose-file: "./docker/docker-compose.yml"
services: | services: |
@ -206,7 +206,7 @@ A full list of flags can be found in the [Docker compose documentation](https://
steps: steps:
# need checkout before using compose-action # need checkout before using compose-action
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3 - uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with: with:
compose-file: "./docker/docker-compose.yml" compose-file: "./docker/docker-compose.yml"
compose-flags: "--profile profile-1" compose-flags: "--profile profile-1"
@ -220,7 +220,7 @@ This is useful when you have a base compose file and additional files for differ
steps: steps:
# need checkout before using compose-action # need checkout before using compose-action
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3 - uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
with: with:
compose-file: | compose-file: |
./docker/docker-compose.yml ./docker/docker-compose.yml

34
dist/index.js generated vendored
View File

@ -47950,27 +47950,42 @@ class DockerComposeInstallerService {
} }
async install({ composeVersion, cwd, githubToken }) { async install({ composeVersion, cwd, githubToken }) {
const currentVersion = await this.version({ cwd }); const currentVersion = await this.version({ cwd });
if (!composeVersion) { const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null;
const normalizedRequestedVersion = composeVersion
? this.normalizeVersion(composeVersion)
: null;
const needsInstall = !currentVersion ||
(composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion);
if (!needsInstall) {
return currentVersion; return currentVersion;
} }
if (currentVersion === composeVersion) { let targetVersion = composeVersion || COMPOSE_VERSION_LATEST;
return currentVersion; if (targetVersion === COMPOSE_VERSION_LATEST) {
}
if (composeVersion === COMPOSE_VERSION_LATEST) {
if (!githubToken) { if (!githubToken) {
throw new Error("GitHub token is required to install the latest version"); throw new Error("GitHub token is required to install the latest version");
} }
composeVersion = await this.getLatestVersion(githubToken); targetVersion = await this.getLatestVersion(githubToken);
} }
await this.installVersion(composeVersion); await this.installVersion(targetVersion);
return 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"}"`);
}
return installedVersion;
} }
async version({ cwd }) { async version({ cwd }) {
try {
const result = await (0,dist.version)({ const result = await (0,dist.version)({
cwd, cwd,
}); });
return result.data.version; return result.data.version;
} }
catch {
// If version check fails (e.g., Docker Compose not installed), return null
return null;
}
}
async getLatestVersion(githubToken) { async getLatestVersion(githubToken) {
const octokit = getOctokit(githubToken); const octokit = getOctokit(githubToken);
const response = await octokit.rest.repos.getLatestRelease({ const response = await octokit.rest.repos.getLatestRelease({
@ -47979,6 +47994,9 @@ class DockerComposeInstallerService {
}); });
return response.data.tag_name; return response.data.tag_name;
} }
normalizeVersion(version) {
return version.replace(/^v/i, "");
}
async installVersion(version) { async installVersion(version) {
switch (process.platform) { switch (process.platform) {
case "linux": case "linux":

View File

@ -21,115 +21,29 @@ describe("DockerComposeInstallerService", () => {
let mockAgent: MockAgent; let mockAgent: MockAgent;
let service: InstanceType<typeof DockerComposeInstallerService>; let service: InstanceType<typeof DockerComposeInstallerService>;
beforeEach(() => { const composeVersionResponse = (version: string) => ({
jest.clearAllMocks();
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
service = new DockerComposeInstallerService(manualInstallerAdapterMock as never);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("install", () => {
it("should return current version when no version is provided", async () => {
// Arrange
versionMock.mockResolvedValue({
exitCode: 0, exitCode: 0,
out: "", out: "",
err: "", err: "",
data: { data: {
version: "2.0.0", version,
}, },
}); });
// Act const installCompose = (composeVersion: string | null, githubToken: string | null) =>
const result = await service.install({ service.install({
composeVersion: null, composeVersion,
cwd: "/path/to/cwd", cwd: "/path/to/cwd",
githubToken: null, githubToken,
});
// 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({
exitCode: 0,
out: "",
err: "",
data: {
version: "1.2.3",
},
});
// Act
const result = await service.install({
composeVersion: "1.2.3",
cwd: "/path/to/cwd",
githubToken: null,
});
// 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({
exitCode: 0,
out: "",
err: "",
data: {
version: "1.2.3",
},
});
const expectedVersion = "1.3.0";
versionMock.mockResolvedValueOnce({
exitCode: 0,
out: "",
err: "",
data: {
version: expectedVersion,
},
}); });
const setPlatform = (platform: NodeJS.Platform) => {
Object.defineProperty(process, "platform", { Object.defineProperty(process, "platform", {
value: "linux", value: platform,
}); });
};
// Act const mockLatestRelease = (version: string) => {
const result = await service.install({
composeVersion: expectedVersion,
cwd: "/path/to/cwd",
githubToken: null,
});
// Assert
expect(result).toBe(expectedVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(expectedVersion);
});
it("should install the latest version if requested", async () => {
// Arrange
versionMock.mockResolvedValueOnce({
exitCode: 0,
out: "",
err: "",
data: {
version: "1.2.3",
},
});
const latestVersion = "v1.4.0";
const mockClient = mockAgent.get("https://api.github.com"); const mockClient = mockAgent.get("https://api.github.com");
mockClient mockClient
.intercept({ .intercept({
@ -139,7 +53,7 @@ describe("DockerComposeInstallerService", () => {
.reply( .reply(
200, 200,
{ {
tag_name: latestVersion, tag_name: version,
}, },
{ {
headers: { headers: {
@ -148,30 +62,76 @@ describe("DockerComposeInstallerService", () => {
} }
); );
setGlobalDispatcher(mockClient); setGlobalDispatcher(mockClient);
versionMock.mockResolvedValueOnce({
exitCode: 0,
out: "",
err: "",
data: {
version: latestVersion,
},
});
Object.defineProperty(process, "platform", {
value: "linux",
});
Object.defineProperty(globalThis, "fetch", { Object.defineProperty(globalThis, "fetch", {
value: jest.fn(), value: jest.fn(),
}); });
};
beforeEach(() => {
jest.clearAllMocks();
mockAgent = new MockAgent();
mockAgent.disableNetConnect();
service = new DockerComposeInstallerService(manualInstallerAdapterMock as never);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("install", () => {
it("should return current version when no version is provided", async () => {
// Arrange
versionMock.mockResolvedValue(composeVersionResponse("2.0.0"));
// Act // Act
const result = await service.install({ const result = await installCompose(null, null);
composeVersion: "latest",
cwd: "/path/to/cwd", // Assert
githubToken: "token", 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"));
// Act
const result = await installCompose("v1.2.3", null);
// 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"));
const expectedVersion = "1.3.0";
versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion));
setPlatform("linux");
// Act
const result = await installCompose(expectedVersion, null);
// 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"));
const latestVersion = "v1.4.0";
mockLatestRelease(latestVersion);
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
setPlatform("linux");
// Act
const result = await installCompose("latest", "token");
// Assert // Assert
expect(result).toBe(latestVersion); expect(result).toBe(latestVersion);
expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion); expect(manualInstallerAdapterMock.install).toHaveBeenCalledWith(latestVersion);
@ -179,60 +139,102 @@ describe("DockerComposeInstallerService", () => {
it("should throw an error if the latest version if requested and no Github token is provided", async () => { it("should throw an error if the latest version if requested and no Github token is provided", async () => {
// Arrange // Arrange
versionMock.mockResolvedValueOnce({ versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
exitCode: 0,
out: "",
err: "",
data: {
version: "1.2.3",
},
});
// Act & Assert // Act & Assert
await expect( await expect(installCompose("latest", null)).rejects.toThrow(
service.install({ "GitHub token is required to install the latest version"
composeVersion: "latest", );
cwd: "/path/to/cwd",
githubToken: null,
})
).rejects.toThrow("GitHub token is required to install the latest version");
}); });
it("should throw an error on unsupported platforms", async () => { it("should throw an error on unsupported platforms", async () => {
// Arrange // Arrange
versionMock.mockResolvedValueOnce({ versionMock.mockResolvedValueOnce(composeVersionResponse("1.2.3"));
exitCode: 0,
out: "",
err: "",
data: {
version: "1.2.3",
},
});
const expectedVersion = "1.3.0"; const expectedVersion = "1.3.0";
versionMock.mockResolvedValueOnce({ versionMock.mockResolvedValueOnce(composeVersionResponse(expectedVersion));
exitCode: 0, setPlatform("win32");
out: "",
err: "",
data: {
version: expectedVersion,
},
});
Object.defineProperty(process, "platform", {
value: "win32",
});
// Act & Assert // Act & Assert
await expect( await expect(installCompose(expectedVersion, null)).rejects.toThrow(
service.install({ `Unsupported platform: win32`
composeVersion: expectedVersion, );
cwd: "/path/to/cwd",
githubToken: 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"));
const installedVersion = "2.0.0";
// After installation, version() returns the new version
versionMock.mockResolvedValueOnce(composeVersionResponse(installedVersion));
setPlatform("linux");
// Act
const result = await installCompose(installedVersion, "token");
// 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"));
const latestVersion = "v1.4.0";
mockLatestRelease(latestVersion);
versionMock.mockResolvedValueOnce(composeVersionResponse(latestVersion));
setPlatform("linux");
// Act
const result = await installCompose("latest", "token");
// 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");
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"));
// Act
const result = await installCompose("", null);
// 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"));
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);
});
}); });
}); });

View File

@ -19,31 +19,53 @@ export class DockerComposeInstallerService {
async install({ composeVersion, cwd, githubToken }: InstallInputs): Promise<string> { async install({ composeVersion, cwd, githubToken }: InstallInputs): Promise<string> {
const currentVersion = await this.version({ cwd }); const currentVersion = await this.version({ cwd });
if (!composeVersion) { const normalizedCurrentVersion = currentVersion ? this.normalizeVersion(currentVersion) : null;
const normalizedRequestedVersion = composeVersion
? this.normalizeVersion(composeVersion)
: null;
const needsInstall =
!currentVersion ||
(composeVersion && normalizedRequestedVersion !== normalizedCurrentVersion);
if (!needsInstall) {
return currentVersion; return currentVersion;
} }
if (currentVersion === composeVersion) { let targetVersion = composeVersion || COMPOSE_VERSION_LATEST;
return currentVersion;
}
if (composeVersion === COMPOSE_VERSION_LATEST) { if (targetVersion === COMPOSE_VERSION_LATEST) {
if (!githubToken) { if (!githubToken) {
throw new Error("GitHub token is required to install the latest version"); throw new Error("GitHub token is required to install the latest version");
} }
composeVersion = await this.getLatestVersion(githubToken); targetVersion = await this.getLatestVersion(githubToken);
} }
await this.installVersion(composeVersion); await this.installVersion(targetVersion);
return 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"}"`
);
} }
private async version({ cwd }: VersionInputs): Promise<string> { return installedVersion;
}
private async version({ cwd }: VersionInputs): Promise<string | null> {
try {
const result = await version({ const result = await version({
cwd, cwd,
}); });
return result.data.version; 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> { private async getLatestVersion(githubToken: string): Promise<string> {
@ -57,6 +79,10 @@ export class DockerComposeInstallerService {
return response.data.tag_name; return response.data.tag_name;
} }
private normalizeVersion(version: string): string {
return version.replace(/^v/i, "");
}
private async installVersion(version: string): Promise<void> { private async installVersion(version: string): Promise<void> {
switch (process.platform) { switch (process.platform) {
case "linux": case "linux":