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

Get buildpacks from latest release for fir apps #3092

Merged
merged 4 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/cli/src/commands/buildpacks/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Command, flags as Flags} from '@heroku-cli/command'
import {ux} from '@oclif/core'
import {App} from '../../lib/types/fir'
import color from '@heroku-cli/color'

import {BuildpackCommand} from '../../lib/buildpacks/buildpacks'

Expand All @@ -14,12 +16,16 @@ export default class Index extends Command {
async run() {
const {flags} = await this.parse(Index)
const buildpacksCommand = new BuildpackCommand(this.heroku)

const buildpacks = await buildpacksCommand.fetch(flags.app)
const {body: app} = await this.heroku.get<App>(`/apps/${flags.app}`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
const buildpacks = await buildpacksCommand.fetch(flags.app, app.generation === 'fir')
if (buildpacks.length === 0) {
this.log(`${flags.app} has no Buildpack URL set.`)
} else {
ux.styledHeader(`${flags.app} Buildpack URL${buildpacks.length > 1 ? 's' : ''}`)
ux.styledHeader(`${color.app(flags.app)} Buildpack${buildpacks.length > 1 ? 's' : ''}`)
buildpacksCommand.display(buildpacks, '')
}
}
Expand Down
42 changes: 35 additions & 7 deletions packages/cli/src/lib/buildpacks/buildpacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ux} from '@oclif/core'
import {findIndex as lodashFindIndex} from 'lodash'
import {Result} from 'true-myth'
import push from '../git/push'
import {OciImage, Release} from '../../lib/types/fir'

const validUrl = require('valid-url')

Expand All @@ -26,14 +27,41 @@ export class BuildpackCommand {
this.registry = new BuildpackRegistry()
}

async fetch(app: string): Promise<any[]> {
const buildpacks = await this.heroku.get(`/apps/${app}/buildpack-installations`)
async fetch(app: string, isFirApp = false): Promise<any[]> {
let buildpacks: any
if (isFirApp) {
const {body: releases} = await this.heroku.request<Release[]>(`/apps/${app}/releases`, {
partial: true,
headers: {
Range: 'version ..; max=10, order=desc',
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
const latestImageId = releases[0].oci_image?.id
const {body: ociImages} = await this.heroku.get<OciImage[]>(`/apps/${app}/oci-images/${latestImageId}`, {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
buildpacks = ociImages[0].buildpacks.map((b, index) => {
return {
buildpack: {
url: b.id || b.homepage,
name: b.id,
},
ordinal: index,
}
})
} else {
const buildpacksBody = await this.heroku.get(`/apps/${app}/buildpack-installations`)
buildpacks = buildpacksBody.body
}

return this.mapBuildpackResponse(buildpacks)
}

mapBuildpackResponse(buildpacks: {body: any}): BuildpackResponse[] {
const body = buildpacks.body
return body.map((bp: BuildpackResponse) => {
mapBuildpackResponse(buildpacks: BuildpackResponse[]): BuildpackResponse[] {
return buildpacks.map((bp: BuildpackResponse) => {
bp.buildpack.url = bp.buildpack.url.replace(/^urn:buildpack:/, '')
return bp
})
Expand All @@ -55,7 +83,7 @@ export class BuildpackCommand {
}

Result.match({
Ok: _ => {},
Ok: () => {},
Err: err => {
ux.error(`Could not find the buildpack: ${buildpack}. ${err}`, {exit: 1})
},
Expand Down Expand Up @@ -117,7 +145,7 @@ export class BuildpackCommand {
}

async put(app: string, buildpackUpdates: {buildpack: string}[]): Promise<BuildpackResponse[]> {
const buildpacks = await this.heroku.put(`/apps/${app}/buildpack-installations`, {
const {body: buildpacks} = await this.heroku.put<any>(`/apps/${app}/buildpack-installations`, {
headers: {Range: ''},
body: {updates: buildpackUpdates},
})
Expand Down
164 changes: 152 additions & 12 deletions packages/cli/test/unit/commands/buildpacks/index.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,205 @@
/* eslint-disable mocha/no-setup-in-describe */
import {expect, test} from '@oclif/test'
import * as nock from 'nock'

import {BuildpackInstallationsStub as Stubber} from '../../../helpers/buildpacks/buildpack-installations-stub'
nock.disableNetConnect()

const cedarApp = {
acm: false,
archived_at: null,
build_stack: {name: 'heroku-24'},
created_at: '2024-09-06T17:45:29Z',
git_url: 'https://git.heroku.com',
id: '12345678-aaaa-bbbb-cccc-b2443790f501',
generation: 'cedar',
maintenance: false,
name: 'example',
owner: {email: 'example-owner@heroku.com'},
internal_routing: null,
region: {name: 'virginia'},
released_at: '2024-11-13T20:07:47Z',
repo_size: null,
slug_size: null,
stack: {name: 'heroku-24'},
updated_at: '2024-11-13T20:07:47Z',
web_url: 'https://cedar-example-app.herokuapp.com',
}

const firApp = {
acm: false,
archived_at: null,
build_stack: {name: 'heroku-24'},
created_at: '2024-09-06T17:45:29Z',
generation: 'fir',
git_url: 'https://git.heroku.com',
id: '12345678-aaaa-bbbb-cccc-b2443790f501',
maintenance: false,
name: 'example',
owner: {email: 'example-owner@heroku.com'},
internal_routing: null,
region: {name: 'virginia'},
released_at: '2024-11-13T20:07:47Z',
repo_size: null,
slug_size: null,
stack: {name: 'heroku-24'},
updated_at: '2024-11-13T20:07:47Z',
web_url: 'https://fir-example-app.herokuapp.com',
}

const releases = [
{
addon_plan_names: [
'heroku-postgresql:dev',
],
artifacts: [
{
type: 'oci-image',
id: '01234567-89ab-cdef-0123-456789abcdef',
},
],
app: {
name: 'example',
id: '01234567-89ab-cdef-0123-456789abcdef',
},
created_at: '2012-01-01T12:00:00Z',
description: 'Added new feature',
id: '01234567-89ab-cdef-0123-456789abcdef',
updated_at: '2012-01-01T12:00:00Z',
oci_image: {
id: '01234567-89ab-cdef-0123-456789abcdef',
},
slug: {
id: '01234567-89ab-cdef-0123-456789abcdef',
},
status: 'succeeded',
user: {
id: '01234567-89ab-cdef-0123-456789abcdef',
email: 'username@example.com',
},
version: 11,
current: true,
output_stream_url: 'https://release-output.heroku.com/streams/01234567-89ab-cdef-0123-456789abcdef',
eligible_for_rollback: true,
},
]

const ociImages = [
{
id: '01234567-89ab-cdef-0123-456789abcdef',
base_image_name: 'heroku/heroku:22-cnb',
base_top_layer: 'sha256:ea36ae5fbc1e7230e0a782bf216fb46500e210382703baa6bab8acf2c6a23f78',
commit: '60883d9e8947a57e04dc9124f25df004866a2051',
commit_description: 'fixed a bug with API documentation',
image_repo: 'd7ba1ace-b396-4691-968c-37ae53153426/builds',
digest: 'sha256:dc14ae5fbc1e7230e0a782bf216fb46500e210631703bcc6bab8acf2c6a23f42',
stack: {
id: 'ba46bf09-7bd1-42fd-90df-a1a9a93eb4a2',
name: 'cnb',
},
process_types: {
web: {
name: 'web',
display_cmd: 'bundle exec puma -p $PORT',
command: '/cnb/process/web',
working_dir: '/workspace/webapp',
default: true,
},
},
buildpacks: [
{
id: 'heroku/ruby',
version: '2.0.0',
homepage: 'https://github.com/heroku/buildpacks-ruby',
},
],
created_at: '2012-01-01T12:00:00Z',
updated_at: '2012-01-01T12:00:00Z',
architecture: 'arm64',
},
]
describe('buildpacks', function () {
test
.nock('https://api.heroku.com', (api: nock.Scope) => {
api.get(`/apps/${cedarApp.name}`).reply(200, cedarApp)
Stubber.get(api, ['https://github.com/heroku/heroku-buildpack-ruby'])
})
.stdout()
.stderr()
.command(['buildpacks', '-a', 'example'])
.command(['buildpacks', '-a', cedarApp.name])
.it('# displays the buildpack URL', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`=== example Buildpack URL
`=== ⬢ ${cedarApp.name} Buildpack

https://github.com/heroku/heroku-buildpack-ruby
`)
})

test
.nock('https://api.heroku.com', (api: nock.Scope) => {
api.get(`/apps/${cedarApp.name}`).reply(200, cedarApp)
Stubber.get(api, [{url: 'urn:buildpack:heroku/ruby', name: 'heroku/ruby'}])
})
.stdout()
.stderr()
.command(['buildpacks', '-a', 'example'])
.command(['buildpacks', '-a', cedarApp.name])
.it('# maps buildpack urns to names', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`=== example Buildpack URL
`=== ⬢ ${cedarApp.name} Buildpack

heroku/ruby
`)
})

test
.nock('https://api.heroku.com', (api: nock.Scope) => {
api.get(`/apps/${cedarApp.name}`).reply(200, cedarApp)
Stubber.get(api, ['https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby.tgz'])
})
.stdout()
.stderr()
.command(['buildpacks', '-a', 'example'])
.command(['buildpacks', '-a', cedarApp.name])
.it('# does not map buildpack s3 to names', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`=== example Buildpack URL
`=== ⬢ ${cedarApp.name} Buildpack

https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby.tgz
`)
})

test
.nock('https://api.heroku.com', (api: nock.Scope) => {
api.get(`/apps/${cedarApp.name}`).reply(200, cedarApp)
Stubber.get(api)
})
.stdout()
.stderr()
.command(['buildpacks', '-a', 'example'])
.command(['buildpacks', '-a', cedarApp.name])
.it('# with no buildpack URL set does not display a buildpack URL', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`example has no Buildpack URL set.
`${cedarApp.name} has no Buildpack URL set.
`)
})

test
.nock('https://api.heroku.com', (api: nock.Scope) => {
api.get(`/apps/${cedarApp.name}`).reply(200, cedarApp)
Stubber.get(api, [
'https://github.com/heroku/heroku-buildpack-java',
'https://github.com/heroku/heroku-buildpack-ruby',
])
})
.stdout()
.stderr()
.command(['buildpacks', '-a', 'example'])
.command(['buildpacks', '-a', cedarApp.name])
.it('# with two buildpack URLs set displays the buildpack URL', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`=== example Buildpack URLs
`=== ⬢ ${cedarApp.name} Buildpacks

1. https://github.com/heroku/heroku-buildpack-java
2. https://github.com/heroku/heroku-buildpack-ruby
Expand All @@ -89,21 +208,42 @@ https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby.tgz

test
.nock('https://api.heroku.com', (api: nock.Scope) => {
api.get(`/apps/${cedarApp.name}`).reply(200, cedarApp)
Stubber.get(api, [
'https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku/java.tgz',
'https://buildpack-registry.s3.amazonaws.com/buildpacks/rust-lang/rust.tgz',
])
})
.stdout()
.stderr()
.command(['buildpacks', '-a', 'example'])
.command(['buildpacks', '-a', cedarApp.name])
.it('# returns the buildpack registry name back', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`=== example Buildpack URLs
`=== ⬢ ${cedarApp.name} Buildpacks

1. heroku/java
2. rust-lang/rust
`)
})

test
.nock('https://api.heroku.com', {
reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'},
}, (api: nock.Scope) => {
api.get(`/apps/${firApp.name}`).reply(200, firApp)
api.get(`/apps/${firApp.name}/releases`).reply(200, releases)
api.get(`/apps/${firApp.name}/oci-images/${releases[0].id}`).reply(200, ociImages)
})
.stdout()
.stderr()
.command(['buildpacks', '-a', cedarApp.name])
.it('# returns cnb buildpack ids for fir apps', ctx => {
expect(ctx.stderr).to.equal('')
expect(ctx.stdout).to.equal(
`=== ⬢ ${firApp.name} Buildpack

heroku/ruby
`)
})
})
Loading