Skip to content

Commit faef3be

Browse files
committed
undock install
Signed-off-by: CrazyMax <[email protected]>
1 parent 517a5c5 commit faef3be

File tree

5 files changed

+355
-1
lines changed

5 files changed

+355
-1
lines changed

__tests__/buildx/install.test.itg.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'tr
2424
maybe('download', () => {
2525
// prettier-ignore
2626
test.each(['latest'])(
27-
'install docker %s', async (version) => {
27+
'install buildx %s', async (version) => {
2828
await expect((async () => {
2929
const install = new Install({
3030
standalone: true
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright 2024 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/undock/install';
21+
22+
describe('download', () => {
23+
// prettier-ignore
24+
test.each(['latest'])(
25+
'install undock %s', async (version) => {
26+
await expect((async () => {
27+
const install = new Install();
28+
const toolPath = await install.download(version);
29+
if (!fs.existsSync(toolPath)) {
30+
throw new Error('toolPath does not exist');
31+
}
32+
const binPath = await install.install(toolPath);
33+
if (!fs.existsSync(binPath)) {
34+
throw new Error('binPath does not exist');
35+
}
36+
})()).resolves.not.toThrow();
37+
}, 60000);
38+
});

__tests__/undock/install.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright 2024 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/undock/install';
25+
26+
const tmpDir = fs.mkdtempSync(path.join(process.env.TEMP || os.tmpdir(), 'undock-install-'));
27+
28+
afterEach(function () {
29+
rimraf.sync(tmpDir);
30+
});
31+
32+
describe('download', () => {
33+
// prettier-ignore
34+
test.each([
35+
['v0.4.0'],
36+
['v0.7.0'],
37+
['latest']
38+
])(
39+
'acquires %p of undock (standalone: %p)', async (version) => {
40+
const install = new Install();
41+
const toolPath = await install.download(version);
42+
expect(fs.existsSync(toolPath)).toBe(true);
43+
const undockBin = await install.install(toolPath, tmpDir);
44+
expect(fs.existsSync(undockBin)).toBe(true);
45+
},
46+
100000
47+
);
48+
49+
// prettier-ignore
50+
test.each([
51+
// following versions are already cached to htc from previous test cases
52+
['v0.4.0'],
53+
['v0.7.0'],
54+
])(
55+
'acquires %p of undock with cache', async (version) => {
56+
const install = new Install();
57+
const toolPath = await install.download(version);
58+
expect(fs.existsSync(toolPath)).toBe(true);
59+
});
60+
61+
// prettier-ignore
62+
test.each([
63+
['v0.5.0'],
64+
['v0.6.0'],
65+
])(
66+
'acquires %p of undock without cache', async (version) => {
67+
const install = new Install();
68+
const toolPath = await install.download(version, true);
69+
expect(fs.existsSync(toolPath)).toBe(true);
70+
});
71+
72+
// TODO: add tests for arm
73+
// prettier-ignore
74+
test.each([
75+
['win32', 'x64'],
76+
['win32', 'arm64'],
77+
['darwin', 'x64'],
78+
['darwin', 'arm64'],
79+
['linux', 'x64'],
80+
['linux', 'arm64'],
81+
['linux', 'ppc64'],
82+
['linux', 's390x'],
83+
])(
84+
'acquires undock for %s/%s', async (os, arch) => {
85+
jest.spyOn(osm, 'platform').mockImplementation(() => os as NodeJS.Platform);
86+
jest.spyOn(osm, 'arch').mockImplementation(() => arch);
87+
const install = new Install();
88+
const undockBin = await install.download('latest');
89+
expect(fs.existsSync(undockBin)).toBe(true);
90+
},
91+
100000
92+
);
93+
});
94+
95+
describe('getDownloadVersion', () => {
96+
it('returns latest download version', async () => {
97+
const version = await Install.getDownloadVersion('latest');
98+
expect(version.version).toEqual('latest');
99+
expect(version.downloadURL).toEqual('https://github.com/crazy-max/undock/releases/download/v%s/%s');
100+
expect(version.releasesURL).toEqual('https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/undock-releases.json');
101+
});
102+
it('returns v0.6.0 download version', async () => {
103+
const version = await Install.getDownloadVersion('v0.6.0');
104+
expect(version.version).toEqual('v0.6.0');
105+
expect(version.downloadURL).toEqual('https://github.com/crazy-max/undock/releases/download/v%s/%s');
106+
expect(version.releasesURL).toEqual('https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/undock-releases.json');
107+
});
108+
});
109+
110+
describe('getRelease', () => {
111+
it('returns latest GitHub release', async () => {
112+
const version = await Install.getDownloadVersion('latest');
113+
const release = await Install.getRelease(version);
114+
expect(release).not.toBeNull();
115+
expect(release?.tag_name).not.toEqual('');
116+
});
117+
it('returns v0.6.0 GitHub release', async () => {
118+
const version = await Install.getDownloadVersion('v0.6.0');
119+
const release = await Install.getRelease(version);
120+
expect(release).not.toBeNull();
121+
expect(release?.id).toEqual(121362767);
122+
expect(release?.tag_name).toEqual('v0.6.0');
123+
expect(release?.html_url).toEqual('https://github.com/crazy-max/undock/releases/tag/v0.6.0');
124+
});
125+
it('unknown release', async () => {
126+
const version = await Install.getDownloadVersion('foo');
127+
await expect(Install.getRelease(version)).rejects.toThrow(new Error('Cannot find Undock release foo in https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/undock-releases.json'));
128+
});
129+
});

src/types/undock/undock.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright 2024 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+
export interface DownloadVersion {
18+
version: string;
19+
downloadURL: string;
20+
releasesURL: string;
21+
}

src/undock/install.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* Copyright 2024 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 fs from 'fs';
18+
import os from 'os';
19+
import path from 'path';
20+
import * as core from '@actions/core';
21+
import * as httpm from '@actions/http-client';
22+
import * as tc from '@actions/tool-cache';
23+
import * as semver from 'semver';
24+
import * as util from 'util';
25+
26+
import {Cache} from '../cache';
27+
import {Context} from '../context';
28+
29+
import {GitHubRelease} from '../types/github';
30+
import {DownloadVersion} from '../types/undock/undock';
31+
32+
export class Install {
33+
/*
34+
* Download undock binary from GitHub release
35+
* @param v: version semver version or latest
36+
* @param ghaNoCache: disable binary caching in GitHub Actions cache backend
37+
* @returns path to the undock binary
38+
*/
39+
public async download(v: string, ghaNoCache?: boolean): Promise<string> {
40+
const version: DownloadVersion = await Install.getDownloadVersion(v);
41+
core.debug(`Install.download version: ${version.version}`);
42+
43+
const release: GitHubRelease = await Install.getRelease(version);
44+
core.debug(`Install.download release tag name: ${release.tag_name}`);
45+
46+
const vspec = await this.vspec(release.tag_name);
47+
core.debug(`Install.download vspec: ${vspec}`);
48+
49+
const c = semver.clean(vspec) || '';
50+
if (!semver.valid(c)) {
51+
throw new Error(`Invalid Undock version "${vspec}".`);
52+
}
53+
54+
const installCache = new Cache({
55+
htcName: 'undock-dl-bin',
56+
htcVersion: vspec,
57+
baseCacheDir: path.join(os.homedir(), '.bin'),
58+
cacheFile: os.platform() == 'win32' ? 'undock.exe' : 'undock',
59+
ghaNoCache: ghaNoCache
60+
});
61+
62+
const cacheFoundPath = await installCache.find();
63+
if (cacheFoundPath) {
64+
core.info(`Unodck binary found in ${cacheFoundPath}`);
65+
return cacheFoundPath;
66+
}
67+
68+
const downloadURL = util.format(version.downloadURL, vspec, this.filename(vspec));
69+
core.info(`Downloading ${downloadURL}`);
70+
71+
const htcDownloadPath = await tc.downloadTool(downloadURL);
72+
core.debug(`Install.download htcDownloadPath: ${htcDownloadPath}`);
73+
74+
let htcExtPath: string;
75+
if (os.platform() == 'win32') {
76+
htcExtPath = await tc.extractZip(htcDownloadPath);
77+
} else {
78+
htcExtPath = await tc.extractTar(htcDownloadPath);
79+
}
80+
core.info(`Extracted to ${htcExtPath}`);
81+
82+
const exePath: string = path.join(htcExtPath, os.platform() == 'win32' ? 'undock.exe' : 'undock');
83+
core.debug(`Install.download exePath: ${exePath}`);
84+
85+
const cacheSavePath = await installCache.save(exePath);
86+
core.info(`Cached to ${cacheSavePath}`);
87+
return cacheSavePath;
88+
}
89+
90+
public async install(binPath: string, dest?: string): Promise<string> {
91+
dest = dest || Context.tmpDir();
92+
93+
const binDir = path.join(dest, 'undock-bin');
94+
if (!fs.existsSync(binDir)) {
95+
fs.mkdirSync(binDir, {recursive: true});
96+
}
97+
const binName: string = os.platform() == 'win32' ? 'undock.exe' : 'undock';
98+
const undockPath: string = path.join(binDir, binName);
99+
fs.copyFileSync(binPath, undockPath);
100+
101+
core.info('Fixing perms');
102+
fs.chmodSync(undockPath, '0755');
103+
104+
core.addPath(binDir);
105+
core.info('Added Unodck to PATH');
106+
107+
core.info(`Binary path: ${undockPath}`);
108+
return undockPath;
109+
}
110+
111+
private filename(version: string): string {
112+
let arch: string;
113+
switch (os.arch()) {
114+
case 'x64': {
115+
arch = 'amd64';
116+
break;
117+
}
118+
case 'ppc64': {
119+
arch = 'ppc64le';
120+
break;
121+
}
122+
case 'arm': {
123+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
124+
const arm_version = (process.config.variables as any).arm_version;
125+
arch = arm_version ? 'armv' + arm_version : 'arm';
126+
break;
127+
}
128+
default: {
129+
arch = os.arch();
130+
break;
131+
}
132+
}
133+
const platform: string = os.platform() == 'win32' ? 'windows' : os.platform();
134+
const ext: string = os.platform() == 'win32' ? '.zip' : '.tar.gz';
135+
return util.format('undock_%s_%s_%s%s', version, platform, arch, ext);
136+
}
137+
138+
private async vspec(version: string): Promise<string> {
139+
const v = version.replace(/^v+|v+$/g, '');
140+
core.info(`Use ${v} version spec cache key for ${version}`);
141+
return v;
142+
}
143+
144+
public static async getDownloadVersion(v: string): Promise<DownloadVersion> {
145+
return {
146+
version: v,
147+
downloadURL: 'https://github.com/crazy-max/undock/releases/download/v%s/%s',
148+
releasesURL: 'https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/undock-releases.json'
149+
};
150+
}
151+
152+
public static async getRelease(version: DownloadVersion): Promise<GitHubRelease> {
153+
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit');
154+
const resp: httpm.HttpClientResponse = await http.get(version.releasesURL);
155+
const body = await resp.readBody();
156+
const statusCode = resp.message.statusCode || 500;
157+
if (statusCode >= 400) {
158+
throw new Error(`Failed to get Undock releases from ${version.releasesURL} with status code ${statusCode}: ${body}`);
159+
}
160+
const releases = <Record<string, GitHubRelease>>JSON.parse(body);
161+
if (!releases[version.version]) {
162+
throw new Error(`Cannot find Undock release ${version.version} in ${version.releasesURL}`);
163+
}
164+
return releases[version.version];
165+
}
166+
}

0 commit comments

Comments
 (0)