Skip to content

Commit

Permalink
Update release commands to work with eligibility attribute. (#3081)
Browse files Browse the repository at this point in the history
* Update release commands to work with eligibility attribute.

* Add eligibility info to release info.

* fix test
  • Loading branch information
eablack authored Nov 12, 2024
1 parent ec2587a commit f7db68e
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 54 deletions.
6 changes: 4 additions & 2 deletions packages/cli/src/commands/releases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ export default class Index extends Command {
const url = `/apps/${app}/releases${extended ? '?extended=true' : ''}`

const {body: releases} = await this.heroku.request<Heroku.Release[]>(url, {
partial: true, headers: {
partial: true,
headers: {
Range: `version ..; max=${num || 15}, order=desc`,
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})

Expand Down Expand Up @@ -136,7 +138,7 @@ export default class Index extends Command {
let header = `${app} Releases`
const currentRelease = releases.find(r => r.current === true)
if (currentRelease) {
header += ' - ' + color.blue(`Current: v${currentRelease.version}`)
header += ' - ' + color.cyan(`Current: v${currentRelease.version}`)
}

ux.styledHeader(header)
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/releases/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export default class Info extends Command {

ux.styledHeader(`Release ${color.cyan('v' + release.version)}`)
ux.styledObject({
'Add-ons': release.addon_plan_names, Change: releaseChange, By: userEmail, When: release.created_at,
'Add-ons': release.addon_plan_names,
Change: releaseChange,
By: userEmail,
'Eligible for Rollback?': release.eligible_for_rollback ? 'Yes' : 'No',
When: release.created_at,
})
ux.log()
ux.styledHeader(`${color.cyan('v' + release.version)} Config vars`)
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/commands/releases/rollback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,35 @@ export default class Rollback extends Command {
static hiddenAliases = ['rollback']
static description = `Roll back to a previous release.
If RELEASE is not specified, it will roll back one release.
If RELEASE is not specified, it will roll back to the last eligible release.
`
static flags = {
remote: flags.remote(),
app: flags.app({required: true}),
}

static args = {
release: Args.string({description: 'ID of the release. If omitted, we use the last release ID.'}),
release: Args.string({description: 'ID of the release. If omitted, we use the last eligible release.'}),
}

public async run(): Promise<void> {
const {flags, args} = await this.parse(Rollback)
const {app} = flags
const release = await findByPreviousOrId(this.heroku, app, args.release)

ux.action.start(`Rolling back ${color.magenta(app)} to ${color.green('v' + release.version)}`)
const {body: latest} = await this.heroku.post<Heroku.Release>(`/apps/${app}/releases`, {body: {release: release.id}})
if (!release) {
ux.error(`No eligible release found for ${color.app(app)} to roll back to.`)
}

ux.action.start(`Rolling back ${color.app(app)} to ${color.green('v' + release.version)}`)
const {body: latest} = await this.heroku.post<Heroku.Release>(`/apps/${app}/releases`, {
body: {
release: release.id,
},
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
const streamUrl = latest.output_stream_url
ux.action.stop(`done, ${color.green('v' + latest.version)}`)
ux.warn("Rollback affects code and config vars; it doesn't add or remove addons.")
Expand Down
13 changes: 10 additions & 3 deletions packages/cli/src/lib/releases/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import * as Heroku from '@heroku-cli/schema'
export const findRelease = async function (heroku: APIClient, app: string, search: (releases: Heroku.Release[]) => Heroku.Release) {
const {body: releases} = await heroku.request<Heroku.Release[]>(`/apps/${app}/releases`, {
partial: true,
headers: {Range: 'version ..; max=10, order=desc'},
headers: {
Range: 'version ..; max=10, order=desc',
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})

return search(releases)
Expand All @@ -14,7 +17,11 @@ export const getRelease = async function (heroku: APIClient, app: string, releas
let id = release.toLowerCase()
id = id.startsWith('v') ? id.slice(1) : id

const {body: releaseResponse} = await heroku.get<Heroku.Release>(`/apps/${app}/releases/${id}`)
const {body: releaseResponse} = await heroku.get<Heroku.Release>(`/apps/${app}/releases/${id}`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})

return releaseResponse
}
Expand All @@ -29,7 +36,7 @@ export const findByLatestOrId = async function (heroku: APIClient, app: string,

export const findByPreviousOrId = async function (heroku: APIClient, app: string, release = 'previous') {
if (release === 'previous') {
return findRelease(heroku, app, releases => releases.filter(r => r.status === 'succeeded')[1])
return findRelease(heroku, app, releases => releases.filter(r => r.eligible_for_rollback)[1])
}

return getRelease(heroku, app, release)
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/lib/releases/status_helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const description = function (release: {status?: string, [k: string]: any
return 'release command executing'
case 'failed':
return 'release command failed'
case 'expired':
return 'release expired'
default:
return ''
}
Expand All @@ -15,7 +17,9 @@ export const color = function (s?: string) {
return 'yellow'
case 'failed':
return 'red'
case 'expired':
return 'gray'
default:
return 'white'
return 'cyan'
}
}
7 changes: 1 addition & 6 deletions packages/cli/test/unit/commands/releases/index.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,6 @@ describe('releases', function () {
},
},
]
const slug = {
process_types: {
release: 'bundle exec rake db:migrate',
},
}

it('shows releases', async function () {
process.stdout.isTTY = true
Expand All @@ -134,7 +129,7 @@ describe('releases', function () {
v37 first commit [email protected] 2015/11/18 01:36:38 +0000
`)
assertLineWidths(stdout.output, 80)
// expect(stderr.output).to.equal('')
expect(stderr.output).to.equal('')
api.done()
})

Expand Down
172 changes: 138 additions & 34 deletions packages/cli/test/unit/commands/releases/info.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {stdout, stderr} from 'stdout-stderr'
import {stdout} from 'stdout-stderr'
import Cmd from '../../../../src/commands/releases/info'
import runCommand from '../../../helpers/runCommand'
import * as nock from 'nock'
import {expect} from 'chai'
import expectOutput from '../../../helpers/utils/expectOutput'
import heredoc from 'tsheredoc'

const d = new Date(2000, 1, 1)
describe('releases:info', function () {
Expand All @@ -14,98 +16,200 @@ describe('releases:info', function () {
description: 'something changed',
user: {
email: '[email protected]',
}, created_at: d, version: 10, addon_plan_names: ['addon1', 'addon2'],
}, created_at: d,
version: 10,
eligible_for_rollback: true,
addon_plan_names: ['addon1', 'addon2'],
}

const configVars = {FOO: 'foo', BAR: 'bar'}

it('shows most recent release info', function () {
const api = nock('https://api.heroku.com:443')
it('shows most recent release info', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases')
.reply(200, [release])
.get('/apps/myapp/releases/10/config-vars')
.reply(200, configVars)
return runCommand(Cmd, [
await runCommand(Cmd, [
'--app',
'myapp',
])
.then(() => expect(stdout.output).to.equal(`=== Release v10\n\nAdd-ons: addon1\n addon2\nBy: [email protected]\nChange: something changed\nWhen: ${d.toISOString()}\n\n=== v10 Config vars\n\nBAR: bar\nFOO: foo\n`))
.then(() => api.done())
expectOutput(stdout.output, heredoc(`
=== Release v10
Add-ons: addon1
addon2
By: [email protected]
Change: something changed
Eligible for Rollback?: Yes
When: ${d.toISOString()}
=== v10 Config vars
BAR: bar
FOO: foo
`))
})

it('shows most recent release info config vars as shell', function () {
const api = nock('https://api.heroku.com:443')
it('shows most recent release info config vars as shell', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases')
.reply(200, [release])
.get('/apps/myapp/releases/10/config-vars')
.reply(200, configVars)
return runCommand(Cmd, [
await runCommand(Cmd, [
'--app',
'myapp',
'--shell',
])
.then(() => expect(stdout.output).to.equal(`=== Release v10\n\nAdd-ons: addon1\n addon2\nBy: [email protected]\nChange: something changed\nWhen: ${d.toISOString()}\n\n=== v10 Config vars\n\nFOO=foo\nBAR=bar\n`))
.then(() => api.done())
expectOutput(stdout.output, heredoc(`
=== Release v10
Add-ons: addon1
addon2
By: [email protected]
Change: something changed
Eligible for Rollback?: Yes
When: ${d.toISOString()}
=== v10 Config vars
FOO=foo
BAR=bar
`))
})

it('shows release info by id', function () {
const api = nock('https://api.heroku.com:443')
it('shows release info by id', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases/10')
.reply(200, release)
.get('/apps/myapp/releases/10/config-vars')
.reply(200, configVars)
return runCommand(Cmd, [
await runCommand(Cmd, [
'--app',
'myapp',
'v10',
])
.then(() => expect(stdout.output).to.equal(`=== Release v10\n\nAdd-ons: addon1\n addon2\nBy: [email protected]\nChange: something changed\nWhen: ${d.toISOString()}\n\n=== v10 Config vars\n\nBAR: bar\nFOO: foo\n`))
.then(() => api.done())
expectOutput(stdout.output, heredoc(`
=== Release v10
Add-ons: addon1
addon2
By: [email protected]
Change: something changed
Eligible for Rollback?: Yes
When: ${d.toISOString()}
=== v10 Config vars
BAR: bar
FOO: foo
`))
})

it('shows recent release as json', function () {
const api = nock('https://api.heroku.com:443')
it('shows recent release as json', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases/10')
.reply(200, release)
return runCommand(Cmd, [
await runCommand(Cmd, [
'--app',
'myapp',
'--json',
'v10',
])
.then(() => expect(stdout.output).to.contain('"version": 10'))
.then(() => api.done())
expect(stdout.output).to.contain('"version": 10')
})

it('shows a failed release info', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases')
.reply(200, [{
description: 'something changed',
status: 'failed',
eligible_for_rollback: false,
user: {email: '[email protected]'},
created_at: d,
version: 10,
}])
.get('/apps/myapp/releases/10/config-vars')
.reply(200, configVars)
await runCommand(Cmd, [
'--app',
'myapp',
])
expectOutput(stdout.output, heredoc(`
=== Release v10
By: [email protected]
Change: something changed (release command failed)
Eligible for Rollback?: No
When: ${d.toISOString()}
=== v10 Config vars
BAR: bar
FOO: foo
`))
})

it('shows a failed release info', function () {
const api = nock('https://api.heroku.com:443')
it('shows a pending release info', async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases')
.reply(200, [{
description: 'something changed', status: 'failed', user: {email: '[email protected]'}, created_at: d, version: 10,
addon_plan_names: ['addon1', 'addon2'],
description: 'something changed',
status: 'pending',
user: {email: '[email protected]'},
version: 10,
eligible_for_rollback: false,
created_at: d,
}])
.get('/apps/myapp/releases/10/config-vars')
.reply(200, configVars)
return runCommand(Cmd, [
await runCommand(Cmd, [
'--app',
'myapp',
])
.then(() => expect(stdout.output).to.equal(`=== Release v10\n\nBy: [email protected]\nChange: something changed (release command failed)\nWhen: ${d.toISOString()}\n\n=== v10 Config vars\n\nBAR: bar\nFOO: foo\n`))
.then(() => api.done())
expectOutput(stdout.output, heredoc(`
=== Release v10
Add-ons: addon1
addon2
By: [email protected]
Change: something changed (release command executing)
Eligible for Rollback?: No
When: ${d.toISOString()}
=== v10 Config vars
BAR: bar
FOO: foo
`))
})

it('shows a pending release info', function () {
const api = nock('https://api.heroku.com:443')
it("shows an expired release's info", async function () {
nock('https://api.heroku.com')
.get('/apps/myapp/releases')
.reply(200, [{
addon_plan_names: ['addon1', 'addon2'], description: 'something changed', status: 'pending', user: {email: '[email protected]'}, version: 10, created_at: d,
description: 'something changed',
status: 'expired',
eligible_for_rollback: false,
user: {email: '[email protected]'},
created_at: d,
version: 10,
}])
.get('/apps/myapp/releases/10/config-vars')
.reply(200, configVars)
return runCommand(Cmd, [
await runCommand(Cmd, [
'--app',
'myapp',
])
.then(() => expect(stdout.output).to.equal(`=== Release v10\n\nAdd-ons: addon1\n addon2\nBy: [email protected]\nChange: something changed (release command executing)\nWhen: ${d.toISOString()}\n\n=== v10 Config vars\n\nBAR: bar\nFOO: foo\n`))
.then(() => api.done())
expectOutput(stdout.output, heredoc(`
=== Release v10
By: [email protected]
Change: something changed (release expired)
Eligible for Rollback?: No
When: ${d.toISOString()}
=== v10 Config vars
BAR: bar
FOO: foo
`))
})
})
Loading

0 comments on commit f7db68e

Please sign in to comment.