Skip to content

Commit ac9dc8b

Browse files
committed
compose install
Signed-off-by: CrazyMax <[email protected]>
1 parent 9b3822d commit ac9dc8b

File tree

8 files changed

+618
-0
lines changed

8 files changed

+618
-0
lines changed

__tests__/compose/compose.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright 2025 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {describe, expect, it, jest, test, afterEach} from '@jest/globals';
18+
import fs from 'fs';
19+
import os from 'os';
20+
import path from 'path';
21+
import * as rimraf from 'rimraf';
22+
import * as semver from 'semver';
23+
24+
import {Context} from '../../src/context';
25+
import {Exec} from '../../src/exec';
26+
27+
import {Compose} from '../../src/compose/compose';
28+
29+
const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'compose-compose-'));
30+
const tmpName = path.join(tmpDir, '.tmpname-jest');
31+
32+
jest.spyOn(Context, 'tmpDir').mockImplementation((): string => {
33+
fs.mkdirSync(tmpDir, {recursive: true});
34+
return tmpDir;
35+
});
36+
37+
jest.spyOn(Context, 'tmpName').mockImplementation((): string => {
38+
return tmpName;
39+
});
40+
41+
afterEach(() => {
42+
rimraf.sync(tmpDir);
43+
});
44+
45+
describe('isAvailable', () => {
46+
it('docker cli', async () => {
47+
const execSpy = jest.spyOn(Exec, 'getExecOutput');
48+
const compose = new Compose({
49+
standalone: false
50+
});
51+
await compose.isAvailable();
52+
// eslint-disable-next-line jest/no-standalone-expect
53+
expect(execSpy).toHaveBeenCalledWith(`docker`, ['compose'], {
54+
silent: true,
55+
ignoreReturnCode: true
56+
});
57+
});
58+
it('standalone', async () => {
59+
const execSpy = jest.spyOn(Exec, 'getExecOutput');
60+
const compose = new Compose({
61+
standalone: true
62+
});
63+
await compose.isAvailable();
64+
// eslint-disable-next-line jest/no-standalone-expect
65+
expect(execSpy).toHaveBeenCalledWith(`compose`, [], {
66+
silent: true,
67+
ignoreReturnCode: true
68+
});
69+
});
70+
});
71+
72+
describe('printVersion', () => {
73+
it('docker cli', async () => {
74+
const execSpy = jest.spyOn(Exec, 'exec');
75+
const compose = new Compose({
76+
standalone: false
77+
});
78+
await compose.printVersion();
79+
expect(execSpy).toHaveBeenCalledWith(`docker`, ['compose', 'version'], {
80+
failOnStdErr: false
81+
});
82+
});
83+
it('standalone', async () => {
84+
const execSpy = jest.spyOn(Exec, 'exec');
85+
const compose = new Compose({
86+
standalone: true
87+
});
88+
await compose.printVersion();
89+
expect(execSpy).toHaveBeenCalledWith(`compose`, ['version'], {
90+
failOnStdErr: false
91+
});
92+
});
93+
});
94+
95+
describe('version', () => {
96+
it('valid', async () => {
97+
const compose = new Compose();
98+
expect(semver.valid(await compose.version())).not.toBeUndefined();
99+
});
100+
});
101+
102+
describe('parseVersion', () => {
103+
// prettier-ignore
104+
test.each([
105+
['Docker Compose version v2.31.0', '2.31.0'],
106+
])('given %p', async (stdout, expected) => {
107+
expect(Compose.parseVersion(stdout)).toEqual(expected);
108+
});
109+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright 2025 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {describe, expect, test} from '@jest/globals';
18+
import * as fs from 'fs';
19+
20+
import {Install} from '../../src/compose/install';
21+
22+
const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'true' && process.env.ImageOS && process.env.ImageOS.startsWith('ubuntu')) ? describe : describe.skip;
23+
24+
maybe('download', () => {
25+
// prettier-ignore
26+
test.each(['latest'])(
27+
'install compose %s', async (version) => {
28+
await expect((async () => {
29+
const install = new Install({
30+
standalone: true
31+
});
32+
const toolPath = await install.download(version);
33+
if (!fs.existsSync(toolPath)) {
34+
throw new Error('toolPath does not exist');
35+
}
36+
const binPath = await install.installStandalone(toolPath);
37+
if (!fs.existsSync(binPath)) {
38+
throw new Error('binPath does not exist');
39+
}
40+
})()).resolves.not.toThrow();
41+
}, 60000);
42+
});

__tests__/compose/install.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright 2025 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {describe, expect, it, jest, test, afterEach} from '@jest/globals';
18+
import fs from 'fs';
19+
import os from 'os';
20+
import path from 'path';
21+
import * as rimraf from 'rimraf';
22+
import osm = require('os');
23+
24+
import {Install} from '../../src/compose/install';
25+
26+
const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'compose-install-'));
27+
28+
afterEach(function () {
29+
rimraf.sync(tmpDir);
30+
});
31+
32+
describe('download', () => {
33+
// prettier-ignore
34+
test.each([
35+
['v2.31.0', false],
36+
['v2.32.4', true],
37+
['latest', true]
38+
])(
39+
'acquires %p of compose (standalone: %p)', async (version, standalone) => {
40+
const install = new Install({standalone: standalone});
41+
const toolPath = await install.download(version);
42+
expect(fs.existsSync(toolPath)).toBe(true);
43+
let composeBin: string;
44+
if (standalone) {
45+
composeBin = await install.installStandalone(toolPath, tmpDir);
46+
} else {
47+
composeBin = await install.installPlugin(toolPath, tmpDir);
48+
}
49+
expect(fs.existsSync(composeBin)).toBe(true);
50+
},
51+
100000
52+
);
53+
54+
// prettier-ignore
55+
test.each([
56+
// following versions are already cached to htc from previous test cases
57+
['v2.31.0'],
58+
['v2.32.4'],
59+
])(
60+
'acquires %p of compose with cache', async (version) => {
61+
const install = new Install({standalone: false});
62+
const toolPath = await install.download(version);
63+
expect(fs.existsSync(toolPath)).toBe(true);
64+
});
65+
66+
// prettier-ignore
67+
test.each([
68+
['v2.27.1'],
69+
['v2.28.0'],
70+
])(
71+
'acquires %p of compose without cache', async (version) => {
72+
const install = new Install({standalone: false});
73+
const toolPath = await install.download(version, true);
74+
expect(fs.existsSync(toolPath)).toBe(true);
75+
});
76+
77+
// TODO: add tests for arm
78+
// prettier-ignore
79+
test.each([
80+
['win32', 'x64'],
81+
['win32', 'arm64'],
82+
['darwin', 'x64'],
83+
['darwin', 'arm64'],
84+
['linux', 'x64'],
85+
['linux', 'arm64'],
86+
['linux', 'ppc64'],
87+
['linux', 's390x'],
88+
])(
89+
'acquires compose for %s/%s', async (os, arch) => {
90+
jest.spyOn(osm, 'platform').mockImplementation(() => os as NodeJS.Platform);
91+
jest.spyOn(osm, 'arch').mockImplementation(() => arch);
92+
const install = new Install();
93+
const composeBin = await install.download('latest');
94+
expect(fs.existsSync(composeBin)).toBe(true);
95+
},
96+
100000
97+
);
98+
});
99+
100+
describe('getDownloadVersion', () => {
101+
it('returns latest download version', async () => {
102+
const version = await Install.getDownloadVersion('latest');
103+
expect(version.version).toEqual('latest');
104+
expect(version.downloadURL).toEqual('https://github.com/docker/compose/releases/download/v%s/%s');
105+
expect(version.releasesURL).toEqual('https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/compose-releases.json');
106+
});
107+
it('returns v2.24.3 download version', async () => {
108+
const version = await Install.getDownloadVersion('v2.24.3');
109+
expect(version.version).toEqual('v2.24.3');
110+
expect(version.downloadURL).toEqual('https://github.com/docker/compose/releases/download/v%s/%s');
111+
expect(version.releasesURL).toEqual('https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/compose-releases.json');
112+
});
113+
});
114+
115+
describe('getRelease', () => {
116+
it('returns latest GitHub release', async () => {
117+
const version = await Install.getDownloadVersion('latest');
118+
const release = await Install.getRelease(version);
119+
expect(release).not.toBeNull();
120+
expect(release?.tag_name).not.toEqual('');
121+
});
122+
it('returns v2.24.3 GitHub release', async () => {
123+
const version = await Install.getDownloadVersion('v2.24.3');
124+
const release = await Install.getRelease(version);
125+
expect(release).not.toBeNull();
126+
expect(release?.id).toEqual(138380726);
127+
expect(release?.tag_name).toEqual('v2.24.3');
128+
expect(release?.html_url).toEqual('https://github.com/docker/compose/releases/tag/v2.24.3');
129+
});
130+
it('unknown release', async () => {
131+
const version = await Install.getDownloadVersion('foo');
132+
await expect(Install.getRelease(version)).rejects.toThrow(new Error('Cannot find Compose release foo in https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/compose-releases.json'));
133+
});
134+
});

dev.Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ARG NODE_VERSION=20
1818
ARG DOCKER_VERSION=27.2.1
1919
ARG BUILDX_VERSION=0.19.3
20+
ARG COMPOSE_VERSION=2.32.4
2021
ARG UNDOCK_VERSION=0.8.0
2122

2223
FROM node:${NODE_VERSION}-alpine AS base
@@ -76,6 +77,7 @@ RUN --mount=type=bind,target=.,rw \
7677

7778
FROM docker:${DOCKER_VERSION} AS docker
7879
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
80+
FROM docker/compose-bin:v${COMPOSE_VERSION} AS compose
7981
FROM crazymax/undock:${UNDOCK_VERSION} AS undock
8082

8183
FROM deps AS test
@@ -85,6 +87,8 @@ RUN --mount=type=bind,target=.,rw \
8587
--mount=type=bind,from=docker,source=/usr/local/bin/docker,target=/usr/bin/docker \
8688
--mount=type=bind,from=buildx,source=/buildx,target=/usr/libexec/docker/cli-plugins/docker-buildx \
8789
--mount=type=bind,from=buildx,source=/buildx,target=/usr/bin/buildx \
90+
--mount=type=bind,from=compose,source=/docker-compose,target=/usr/libexec/docker/cli-plugins/docker-compose \
91+
--mount=type=bind,from=compose,source=/docker-compose,target=/usr/bin/compose \
8892
--mount=type=bind,from=undock,source=/usr/local/bin/undock,target=/usr/bin/undock \
8993
--mount=type=secret,id=GITHUB_TOKEN \
9094
GITHUB_TOKEN=$(cat /run/secrets/GITHUB_TOKEN) yarn run test:coverage --coverageDirectory=/tmp/coverage

0 commit comments

Comments
 (0)