mirror of
https://github.com/docker/actions-toolkit.git
synced 2024-11-23 11:36:10 +08:00
buildx: install
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
parent
e47d166c4f
commit
257dd09431
@ -45,34 +45,6 @@ afterEach(() => {
|
|||||||
rimraf.sync(tmpDir);
|
rimraf.sync(tmpDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getRelease', () => {
|
|
||||||
it('returns latest buildx GitHub release', async () => {
|
|
||||||
const release = await Buildx.getRelease('latest');
|
|
||||||
expect(release).not.toBeNull();
|
|
||||||
expect(release?.tag_name).not.toEqual('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns v0.10.1 buildx GitHub release', async () => {
|
|
||||||
const release = await Buildx.getRelease('v0.10.1');
|
|
||||||
expect(release).not.toBeNull();
|
|
||||||
expect(release?.id).toEqual(90346950);
|
|
||||||
expect(release?.tag_name).toEqual('v0.10.1');
|
|
||||||
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.10.1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns v0.2.2 buildx GitHub release', async () => {
|
|
||||||
const release = await Buildx.getRelease('v0.2.2');
|
|
||||||
expect(release).not.toBeNull();
|
|
||||||
expect(release?.id).toEqual(17671545);
|
|
||||||
expect(release?.tag_name).toEqual('v0.2.2');
|
|
||||||
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.2.2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unknown release', async () => {
|
|
||||||
await expect(Buildx.getRelease('foo')).rejects.toThrowError(new Error('Cannot find Buildx release foo in https://raw.githubusercontent.com/docker/buildx/master/.github/releases.json'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isAvailable', () => {
|
describe('isAvailable', () => {
|
||||||
it('docker cli', async () => {
|
it('docker cli', async () => {
|
||||||
const execSpy = jest.spyOn(exec, 'getExecOutput');
|
const execSpy = jest.spyOn(exec, 'getExecOutput');
|
||||||
|
96
__tests__/buildx/install.test.ts
Normal file
96
__tests__/buildx/install.test.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2023 actions-toolkit authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {describe, expect, it, jest, test, beforeEach} from '@jest/globals';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import osm = require('os');
|
||||||
|
|
||||||
|
import {Install} from '../../src/buildx/install';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('install', () => {
|
||||||
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'actions-toolkit-'));
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
test.each([
|
||||||
|
['v0.4.1', false],
|
||||||
|
['latest', false],
|
||||||
|
['v0.4.1', true],
|
||||||
|
['latest', true]
|
||||||
|
])(
|
||||||
|
'acquires %p of buildx (standalone: %p)', async (version, standalone) => {
|
||||||
|
const install = new Install({standalone: standalone});
|
||||||
|
const buildxBin = await install.install(version, tmpDir);
|
||||||
|
expect(fs.existsSync(buildxBin)).toBe(true);
|
||||||
|
},
|
||||||
|
100000
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: add tests for arm
|
||||||
|
// prettier-ignore
|
||||||
|
test.each([
|
||||||
|
['win32', 'x64'],
|
||||||
|
['win32', 'arm64'],
|
||||||
|
['darwin', 'x64'],
|
||||||
|
['darwin', 'arm64'],
|
||||||
|
['linux', 'x64'],
|
||||||
|
['linux', 'arm64'],
|
||||||
|
['linux', 'ppc64'],
|
||||||
|
['linux', 's390x'],
|
||||||
|
])(
|
||||||
|
'acquires buildx for %s/%s', async (os, arch) => {
|
||||||
|
jest.spyOn(osm, 'platform').mockImplementation(() => os);
|
||||||
|
jest.spyOn(osm, 'arch').mockImplementation(() => arch);
|
||||||
|
const install = new Install();
|
||||||
|
const buildxBin = await install.install('latest', tmpDir);
|
||||||
|
expect(fs.existsSync(buildxBin)).toBe(true);
|
||||||
|
},
|
||||||
|
100000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRelease', () => {
|
||||||
|
it('returns latest buildx GitHub release', async () => {
|
||||||
|
const release = await Install.getRelease('latest');
|
||||||
|
expect(release).not.toBeNull();
|
||||||
|
expect(release?.tag_name).not.toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns v0.10.1 buildx GitHub release', async () => {
|
||||||
|
const release = await Install.getRelease('v0.10.1');
|
||||||
|
expect(release).not.toBeNull();
|
||||||
|
expect(release?.id).toEqual(90346950);
|
||||||
|
expect(release?.tag_name).toEqual('v0.10.1');
|
||||||
|
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.10.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns v0.2.2 buildx GitHub release', async () => {
|
||||||
|
const release = await Install.getRelease('v0.2.2');
|
||||||
|
expect(release).not.toBeNull();
|
||||||
|
expect(release?.id).toEqual(17671545);
|
||||||
|
expect(release?.tag_name).toEqual('v0.2.2');
|
||||||
|
expect(release?.html_url).toEqual('https://github.com/docker/buildx/releases/tag/v0.2.2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unknown release', async () => {
|
||||||
|
await expect(Install.getRelease('foo')).rejects.toThrowError(new Error('Cannot find Buildx release foo in https://raw.githubusercontent.com/docker/buildx/master/.github/releases.json'));
|
||||||
|
});
|
||||||
|
});
|
@ -45,6 +45,7 @@
|
|||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.1.1",
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^2.0.1",
|
||||||
|
"@actions/tool-cache": "^2.0.1",
|
||||||
"csv-parse": "^5.3.4",
|
"csv-parse": "^5.3.4",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
|
@ -15,14 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as exec from '@actions/exec';
|
import * as exec from '@actions/exec';
|
||||||
import * as httpm from '@actions/http-client';
|
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
|
|
||||||
import {Docker} from '../docker';
|
import {Docker} from '../docker';
|
||||||
import {Context} from '../context';
|
import {Context} from '../context';
|
||||||
import {Inputs} from './inputs';
|
import {Inputs} from './inputs';
|
||||||
|
import {Install} from './install';
|
||||||
import {GitHubRelease} from '../types/github';
|
|
||||||
|
|
||||||
export interface BuildxOpts {
|
export interface BuildxOpts {
|
||||||
context: Context;
|
context: Context;
|
||||||
@ -34,31 +32,16 @@ export class Buildx {
|
|||||||
private _version: string | undefined;
|
private _version: string | undefined;
|
||||||
|
|
||||||
public readonly inputs: Inputs;
|
public readonly inputs: Inputs;
|
||||||
|
public readonly install: Install;
|
||||||
public readonly standalone: boolean;
|
public readonly standalone: boolean;
|
||||||
|
|
||||||
constructor(opts: BuildxOpts) {
|
constructor(opts: BuildxOpts) {
|
||||||
this.context = opts.context;
|
this.context = opts.context;
|
||||||
this.inputs = new Inputs(this.context);
|
this.inputs = new Inputs(this.context);
|
||||||
|
this.install = new Install({standalone: opts.standalone});
|
||||||
this.standalone = opts?.standalone ?? !Docker.isAvailable();
|
this.standalone = opts?.standalone ?? !Docker.isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getRelease(version: string): Promise<GitHubRelease> {
|
|
||||||
// FIXME: Use https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/buildx-releases.json when repo public
|
|
||||||
const url = `https://raw.githubusercontent.com/docker/buildx/master/.github/releases.json`;
|
|
||||||
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit');
|
|
||||||
const resp: httpm.HttpClientResponse = await http.get(url);
|
|
||||||
const body = await resp.readBody();
|
|
||||||
const statusCode = resp.message.statusCode || 500;
|
|
||||||
if (statusCode >= 400) {
|
|
||||||
throw new Error(`Failed to get Buildx release ${version} from ${url} with status code ${statusCode}: ${body}`);
|
|
||||||
}
|
|
||||||
const releases = <Record<string, GitHubRelease>>JSON.parse(body);
|
|
||||||
if (!releases[version]) {
|
|
||||||
throw new Error(`Cannot find Buildx release ${version} in ${url}`);
|
|
||||||
}
|
|
||||||
return releases[version];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCommand(args: Array<string>) {
|
public getCommand(args: Array<string>) {
|
||||||
return {
|
return {
|
||||||
command: this.standalone ? 'buildx' : 'docker',
|
command: this.standalone ? 'buildx' : 'docker',
|
||||||
|
142
src/buildx/install.ts
Normal file
142
src/buildx/install.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2023 actions-toolkit authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import * as httpm from '@actions/http-client';
|
||||||
|
import * as tc from '@actions/tool-cache';
|
||||||
|
import * as semver from 'semver';
|
||||||
|
import * as util from 'util';
|
||||||
|
|
||||||
|
import {GitHubRelease} from '../types/github';
|
||||||
|
|
||||||
|
export interface InstallOpts {
|
||||||
|
standalone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Install {
|
||||||
|
private readonly opts: InstallOpts;
|
||||||
|
|
||||||
|
constructor(opts?: InstallOpts) {
|
||||||
|
this.opts = opts || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async install(version: string, dest: string): Promise<string> {
|
||||||
|
const release: GitHubRelease = await Install.getRelease(version);
|
||||||
|
const fversion = release.tag_name.replace(/^v+|v+$/g, '');
|
||||||
|
let toolPath: string;
|
||||||
|
toolPath = tc.find('buildx', fversion, this.platform());
|
||||||
|
if (!toolPath) {
|
||||||
|
const c = semver.clean(fversion) || '';
|
||||||
|
if (!semver.valid(c)) {
|
||||||
|
throw new Error(`Invalid Buildx version "${fversion}".`);
|
||||||
|
}
|
||||||
|
toolPath = await this.download(fversion);
|
||||||
|
}
|
||||||
|
if (this.opts.standalone) {
|
||||||
|
return this.setStandalone(toolPath, dest);
|
||||||
|
}
|
||||||
|
return this.setPlugin(toolPath, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setStandalone(toolPath: string, dest: string): Promise<string> {
|
||||||
|
const toolBinPath = path.join(toolPath, os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
|
||||||
|
const binDir = path.join(dest, 'bin');
|
||||||
|
if (!fs.existsSync(binDir)) {
|
||||||
|
fs.mkdirSync(binDir, {recursive: true});
|
||||||
|
}
|
||||||
|
const filename: string = os.platform() == 'win32' ? 'buildx.exe' : 'buildx';
|
||||||
|
const buildxPath: string = path.join(binDir, filename);
|
||||||
|
fs.copyFileSync(toolBinPath, buildxPath);
|
||||||
|
fs.chmodSync(buildxPath, '0755');
|
||||||
|
core.addPath(binDir);
|
||||||
|
return buildxPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setPlugin(toolPath: string, dest: string): Promise<string> {
|
||||||
|
const toolBinPath = path.join(toolPath, os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
|
||||||
|
const pluginsDir: string = path.join(dest, 'cli-plugins');
|
||||||
|
if (!fs.existsSync(pluginsDir)) {
|
||||||
|
fs.mkdirSync(pluginsDir, {recursive: true});
|
||||||
|
}
|
||||||
|
const filename: string = os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
|
||||||
|
const pluginPath: string = path.join(pluginsDir, filename);
|
||||||
|
fs.copyFileSync(toolBinPath, pluginPath);
|
||||||
|
fs.chmodSync(pluginPath, '0755');
|
||||||
|
return pluginPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async download(version: string): Promise<string> {
|
||||||
|
const targetFile: string = os.platform() == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
|
||||||
|
const downloadUrl = util.format('https://github.com/docker/buildx/releases/download/v%s/%s', version, this.filename(version));
|
||||||
|
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||||
|
core.debug(`downloadUrl=${downloadUrl}`);
|
||||||
|
core.debug(`downloadPath=${downloadPath}`);
|
||||||
|
return await tc.cacheFile(downloadPath, targetFile, 'buildx', version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private platform(): string {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const arm_version = (process.config.variables as any).arm_version;
|
||||||
|
return `${os.platform()}-${os.arch()}${arm_version ? 'v' + arm_version : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private filename(version: string): string {
|
||||||
|
let arch: string;
|
||||||
|
switch (os.arch()) {
|
||||||
|
case 'x64': {
|
||||||
|
arch = 'amd64';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ppc64': {
|
||||||
|
arch = 'ppc64le';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'arm': {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const arm_version = (process.config.variables as any).arm_version;
|
||||||
|
arch = arm_version ? 'arm-v' + arm_version : 'arm';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
arch = os.arch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const platform: string = os.platform() == 'win32' ? 'windows' : os.platform();
|
||||||
|
const ext: string = os.platform() == 'win32' ? '.exe' : '';
|
||||||
|
return util.format('buildx-v%s.%s-%s%s', version, platform, arch, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getRelease(version: string): Promise<GitHubRelease> {
|
||||||
|
// FIXME: Use https://raw.githubusercontent.com/docker/actions-toolkit/main/.github/buildx-releases.json when repo public
|
||||||
|
const url = `https://raw.githubusercontent.com/docker/buildx/master/.github/releases.json`;
|
||||||
|
const http: httpm.HttpClient = new httpm.HttpClient('docker-actions-toolkit');
|
||||||
|
const resp: httpm.HttpClientResponse = await http.get(url);
|
||||||
|
const body = await resp.readBody();
|
||||||
|
const statusCode = resp.message.statusCode || 500;
|
||||||
|
if (statusCode >= 400) {
|
||||||
|
throw new Error(`Failed to get Buildx release ${version} from ${url} with status code ${statusCode}: ${body}`);
|
||||||
|
}
|
||||||
|
const releases = <Record<string, GitHubRelease>>JSON.parse(body);
|
||||||
|
if (!releases[version]) {
|
||||||
|
throw new Error(`Cannot find Buildx release ${version} in ${url}`);
|
||||||
|
}
|
||||||
|
return releases[version];
|
||||||
|
}
|
||||||
|
}
|
37
yarn.lock
37
yarn.lock
@ -5,7 +5,7 @@ __metadata:
|
|||||||
version: 6
|
version: 6
|
||||||
cacheKey: 8
|
cacheKey: 8
|
||||||
|
|
||||||
"@actions/core@npm:^1.10.0":
|
"@actions/core@npm:^1.10.0, @actions/core@npm:^1.2.6":
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
resolution: "@actions/core@npm:1.10.0"
|
resolution: "@actions/core@npm:1.10.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -15,7 +15,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@actions/exec@npm:^1.1.1":
|
"@actions/exec@npm:^1.0.0, @actions/exec@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "@actions/exec@npm:1.1.1"
|
resolution: "@actions/exec@npm:1.1.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -52,6 +52,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@actions/io@npm:^1.1.1":
|
||||||
|
version: 1.1.2
|
||||||
|
resolution: "@actions/io@npm:1.1.2"
|
||||||
|
checksum: 3c6583c4557abf6c95e9cfc9b6377045e65ba2c5dd4863f4feedd6be9daf4f6b60e588ab0151d5626b5f8320a37f05b8d44ab5c329b8c19f65be31b0616e1464
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@actions/tool-cache@npm:^2.0.1":
|
||||||
|
version: 2.0.1
|
||||||
|
resolution: "@actions/tool-cache@npm:2.0.1"
|
||||||
|
dependencies:
|
||||||
|
"@actions/core": ^1.2.6
|
||||||
|
"@actions/exec": ^1.0.0
|
||||||
|
"@actions/http-client": ^2.0.1
|
||||||
|
"@actions/io": ^1.1.1
|
||||||
|
semver: ^6.1.0
|
||||||
|
uuid: ^3.3.2
|
||||||
|
checksum: 33f6393b9b163e4af2b9759e8d37cda4f018f10ddda3643355bb8a9f92d732e5bdff089cf8036b46d181e1ef2b3210b895b2f746fdf54487afe88f1d340aa9e1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ampproject/remapping@npm:^2.1.0":
|
"@ampproject/remapping@npm:^2.1.0":
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
resolution: "@ampproject/remapping@npm:2.1.2"
|
resolution: "@ampproject/remapping@npm:2.1.2"
|
||||||
@ -745,6 +766,7 @@ __metadata:
|
|||||||
"@actions/exec": ^1.1.1
|
"@actions/exec": ^1.1.1
|
||||||
"@actions/github": ^5.1.1
|
"@actions/github": ^5.1.1
|
||||||
"@actions/http-client": ^2.0.1
|
"@actions/http-client": ^2.0.1
|
||||||
|
"@actions/tool-cache": ^2.0.1
|
||||||
"@types/csv-parse": ^1.2.2
|
"@types/csv-parse": ^1.2.2
|
||||||
"@types/node": ^16.18.11
|
"@types/node": ^16.18.11
|
||||||
"@types/semver": ^7.3.13
|
"@types/semver": ^7.3.13
|
||||||
@ -5986,7 +6008,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"semver@npm:^6.0.0, semver@npm:^6.3.0":
|
"semver@npm:^6.0.0, semver@npm:^6.1.0, semver@npm:^6.3.0":
|
||||||
version: 6.3.0
|
version: 6.3.0
|
||||||
resolution: "semver@npm:6.3.0"
|
resolution: "semver@npm:6.3.0"
|
||||||
bin:
|
bin:
|
||||||
@ -6731,6 +6753,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"uuid@npm:^3.3.2":
|
||||||
|
version: 3.4.0
|
||||||
|
resolution: "uuid@npm:3.4.0"
|
||||||
|
bin:
|
||||||
|
uuid: ./bin/uuid
|
||||||
|
checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"uuid@npm:^8.3.2":
|
"uuid@npm:^8.3.2":
|
||||||
version: 8.3.2
|
version: 8.3.2
|
||||||
resolution: "uuid@npm:8.3.2"
|
resolution: "uuid@npm:8.3.2"
|
||||||
|
Loading…
Reference in New Issue
Block a user