Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: add new update features #368

Merged
merged 17 commits into from
Feb 11, 2022
Merged
25 changes: 19 additions & 6 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,31 @@ export default class UpdateCommand extends Command {

static flags = {
autoupdate: Flags.boolean({hidden: true}),
'from-local': Flags.boolean({description: 'interactively choose an already installed version'}),
'from-local': Flags.boolean({description: 'Interactively choose an already installed version.'}),
version: Flags.string({
description: 'Install a specific version.',
exclusive: ['from-local'],
}),
}

private channel!: string

private readonly clientRoot = this.config.scopedEnvVar('OCLIF_CLIENT_HOME') || path.join(this.config.dataDir, 'client')

private readonly clientBin = path.join(this.clientRoot, 'bin', this.config.windows ? `${this.config.bin}.cmd` : this.config.bin)

async run(): Promise<void> {
const {args, flags} = await this.parse(UpdateCommand)
const updateCli = new UpdateCli({channel: args.channel, autoUpdate: flags.autoupdate, fromLocal: flags['from-local'], config: this.config as Config, exit: this.exit, getPinToVersion: getPinToVersion})

if (args.channel && flags.version) {
this.error('You cannot specifiy both a version and a channel.')
}

const updateCli = new UpdateCli({
channel: args.channel,
autoUpdate: flags.autoupdate,
fromLocal: flags['from-local'],
version: flags.version,
config: this.config as Config,
exit: this.exit,
getPinToVersion: getPinToVersion,
})
return updateCli.runUpdate()
}
}
98 changes: 87 additions & 11 deletions src/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ export interface UpdateCliOptions {
channel?: string;
autoUpdate: boolean;
fromLocal: boolean;
version: string | undefined;
config: Config;
exit: any;
getPinToVersion: () => Promise<string>;
}

export type VersionIndex = Record<string, string>

export default class UpdateCli {
private channel!: string

Expand Down Expand Up @@ -65,12 +68,32 @@ export default class UpdateCli {

CliUx.ux.log()
CliUx.ux.log(`Updating to an already installed version will not update the channel. If autoupdate is enabled, the CLI will eventually be updated back to ${this.channel}.`)
} else if (this.options.version) {
CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`)
await this.options.config.runHook('preupdate', {channel: this.channel})

const index = await this.fetchVersionIndex()
const url = index[this.options.version]
if (!url) {
throw new Error(`${this.options.version} not found in index:\n${Object.keys(index).join(', ')}`)
}

const manifest = await this.fetchVersionManifest(this.options.version, url)
this.currentVersion = await this.determineCurrentVersion()
this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version
const reason = await this.skipUpdate()
if (reason) CliUx.ux.action.stop(reason || 'done')
else await this.update(manifest)

CliUx.ux.debug('tidy')
await this.tidy()
await this.options.config.runHook('update', {channel: this.channel})
} else {
CliUx.ux.action.start(`${this.options.config.name}: Updating CLI`)
await this.options.config.runHook('preupdate', {channel: this.channel})
const manifest = await this.fetchManifest()
const manifest = await this.fetchChannelManifest()
this.currentVersion = await this.determineCurrentVersion()
this.updatedVersion = (manifest as any).sha ? `${manifest.version}-${(manifest as any).sha}` : manifest.version
this.updatedVersion = manifest.sha ? `${manifest.version}-${manifest.sha}` : manifest.version
const reason = await this.skipUpdate()
if (reason) CliUx.ux.action.stop(reason || 'done')
else await this.update(manifest)
Expand All @@ -83,21 +106,39 @@ export default class UpdateCli {
CliUx.ux.action.stop()
}

private async fetchManifest(): Promise<IManifest> {
private async fetchChannelManifest(): Promise<IManifest> {
const s3Key = this.s3ChannelManifestKey(
this.options.config.bin,
this.options.config.platform,
this.options.config.arch,
(this.options.config.pjson.oclif.update.s3 as any).folder,
)
return this.fetchManifest(s3Key)
}

private async fetchVersionManifest(version: string, url: string): Promise<IManifest> {
const parts = url.split('/')
const hashIndex = parts.indexOf(version) + 1
const hash = parts[hashIndex]
const s3Key = this.s3VersionManifestKey(
this.options.config.bin,
version,
hash,
this.options.config.platform,
this.options.config.arch,
(this.options.config.pjson.oclif.update.s3 as any).folder,
)
return this.fetchManifest(s3Key)
}

private async fetchManifest(s3Key: string): Promise<IManifest> {
const http: typeof HTTP = require('http-call').HTTP

CliUx.ux.action.status = 'fetching manifest'

try {
const url = this.options.config.s3Url(this.options.config.s3Key('manifest', {
channel: this.channel,
platform: this.options.config.platform,
arch: this.options.config.arch,
}))
const url = this.options.config.s3Url(s3Key)
const {body} = await http.get<IManifest | string>(url)

// in case the content-type is not set, parse as a string
// this will happen if uploading without `oclif-dev publish`
if (typeof body === 'string') {
return JSON.parse(body)
}
Expand All @@ -109,6 +150,28 @@ export default class UpdateCli {
}
}

private async fetchVersionIndex(): Promise<VersionIndex> {
const http: typeof HTTP = require('http-call').HTTP

CliUx.ux.action.status = 'fetching version index'

const newIndexUrl = this.options.config.s3Url(
this.s3VersionIndexKey(
this.options.config.bin,
this.options.config.platform,
this.options.config.arch,
(this.options.config.pjson.oclif.update.s3 as any).folder,
),
)

const {body} = await http.get<VersionIndex>(newIndexUrl)
if (typeof body === 'string') {
return JSON.parse(body)
}

return body
}

private async downloadAndExtract(output: string, manifest: IManifest, channel: string) {
const {version, gz, sha256gz} = manifest

Expand Down Expand Up @@ -224,6 +287,19 @@ export default class UpdateCli {
return path.join(s3SubDir, 'channels', this.channel, `${bin}-${platform}-${arch}-buildmanifest`)
}

// eslint-disable-next-line max-params
private s3VersionManifestKey(bin: string, version: string, hash: string, platform: string, arch: string, folder = ''): string {
let s3SubDir = folder || ''
if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/`
return path.join(s3SubDir, 'versions', version, hash, `${bin}-v${version}-${hash}-${platform}-${arch}-buildmanifest`)
}

private s3VersionIndexKey(bin: string, platform: string, arch: string, folder = ''): string {
let s3SubDir = folder || ''
if (s3SubDir !== '' && s3SubDir.slice(-1) !== '/') s3SubDir = `${s3SubDir}/`
return path.join(s3SubDir, 'versions', `${bin}-${platform}-${arch}-tar-gz.json`)
}

private async setChannel() {
const channelPath = path.join(this.options.config.dataDir, 'channel')
fs.writeFile(channelPath, this.channel, 'utf8')
Expand Down
1 change: 1 addition & 0 deletions test/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function initUpdateCli(options: Partial<UpdateCliOptions>): UpdateCli {
fromLocal: options.fromLocal || false,
autoUpdate: options.autoUpdate || false,
config: options.config!,
version: undefined,
exit: undefined,
getPinToVersion: async () => '2.0.0',
})
Expand Down