Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
47 changes: 40 additions & 7 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
<!--
When creating a PR, be sure to prepend the PR title with the Conventional Commit type (`feat`, `fix`, or `chore`).

When creating a PR, be sure to prepend the PR title with the Conventional Commit type (`feat`, `fix`, or `chore`). This is how we manage package versioning and generating CHANGELOG notes.
Examples:
- "feat: add growl notification to spaces:wait"
- "fix: handle special characters in app names"
- "chore: add dist directory to .gitignore"
The expected Conventional Commit types are listed below.
Learn more about [Conventional Commits](https://www.conventionalcommits.org/).
-->

`feat: add growl notification to spaces:wait`
## Summary
<!-- Brief description of the changes in this PR. -->

`fix: handle special characters in app names`
## Type of Change
### Breaking Changes (major semver update)
- [ ] Add a `!` after your change type to denote a change that breaks current behavior

`chore: refactor tests`
### Feature Additions (minor semver update)
- [ ] **feat**: Introduces a new feature to the codebase

Learn more about [Conventional Commits](https://www.conventionalcommits.org/).
-->
### Patch Updates (patch semver update)
- [ ] **fix**: Bug fix
- [ ] **perf**: Performance improvement
- [ ] **deps**: Dependency upgrade
- [ ] **revert**: Revert a previous commit
- [ ] **docs**: Documentation change
- [ ] **style**: Styling update
- [ ] **chore**: Change that does not affect production code
- [ ] **refactor**: Refactoring existing code without changing behavior
- [ ] **tests**: Add/update/remove tests
- [ ] **build**: Change to the build system
- [ ] **ci**: Continuous integration workflow update

## Testing
**Notes**:
<!-- Add any context/setup necessary for testing. -->

**Steps**:
1. Replace this text with a list of steps used to validate changes or type 'Passing CI suffices'.
2. ...

## Screenshots (if applicable)

## Related Issues
GitHub issue: #[GitHub issue number]
GUS work item: [WI number](WI link)
21 changes: 12 additions & 9 deletions packages/cli/src/commands/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ function listApps(apps: Heroku.App) {
apps.forEach((app: App) => ux.stdout(regionizeAppName(app)))
}

function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: string | null) {
function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: null | string) {
if (apps.length === 0) {
if (space) ux.stdout(`There are no apps in space ${color.green(space)}.`)
if (space) ux.stdout(`There are no apps in space ${color.space(space)}.`)
else if (team) ux.stdout(`There are no apps in team ${color.magenta(team)}.`)
else ux.stdout('You have no apps.')
} else if (space) {
hux.styledHeader(`Apps in space ${color.green(space)}`)
hux.styledHeader(`Apps in space ${color.space(space)}`)
listApps(apps)
} else if (team) {
hux.styledHeader(`Apps in team ${color.magenta(team)}`)
Expand All @@ -53,6 +53,7 @@ function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: st

const columns = {
Name: {get: regionizeAppName},
// eslint-disable-next-line perfectionist/sort-objects
Email: {get: ({owner}: any) => owner.email},
}

Expand All @@ -65,26 +66,28 @@ function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: st

export default class AppsIndex extends Command {
static description = 'list your apps'
static topic = 'apps'
static hiddenAliases = ['list', 'apps:list']

static examples = [
'$ heroku apps',
]

static flags = {
all: flags.boolean({char: 'A', description: 'include apps in all teams'}),
'internal-routing': flags.boolean({char: 'i', description: 'filter to Internal Web Apps', hidden: true}),
json: flags.boolean({char: 'j', description: 'output in json format'}),

personal: flags.boolean({char: 'p', description: 'list apps in personal account when a default team is set'}),
space: flags.string({
char: 's',
description: 'filter by space',
completion: SpaceCompletion,
description: 'filter by space',
}),
personal: flags.boolean({char: 'p', description: 'list apps in personal account when a default team is set'}),
'internal-routing': flags.boolean({char: 'i', description: 'filter to Internal Web Apps', hidden: true}),
team: flags.team(),
}

static hiddenAliases = ['list', 'apps:list']

static topic = 'apps'

async run() {
const {flags} = await this.parse(AppsIndex)

Expand Down
15 changes: 8 additions & 7 deletions packages/cli/src/commands/apps/info.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Args, ux} from '@oclif/core'
import {hux} from '@heroku/heroku-cli-util'
import {color, hux} from '@heroku/heroku-cli-util'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import * as util from 'util'
import _ from 'lodash'
import {Args, ux} from '@oclif/core'
import {filesize} from 'filesize'
import _ from 'lodash'
import * as util from 'util'

import {getGeneration} from '../../lib/apps/generation.js'

const {countBy, snakeCase} = _
Expand Down Expand Up @@ -38,8 +39,8 @@ async function getInfo(app: string, client: Command, extended: boolean) {
const data: Heroku.App = {
addons,
app: appWithMoreInfo,
dynos,
collaborators,
dynos,
pipeline_coupling: pipelineCouplings,
}

Expand All @@ -66,7 +67,7 @@ function print(info: Heroku.App, addons: Heroku.AddOn[], collaborators: Heroku.C
if (info.app.cron_next_run) data['Cron Next Run'] = formatDate(new Date(info.app.cron_next_run))
if (info.app.database_size) data['Database Size'] = filesize(info.app.database_size, {standard: 'jedec', round: 0})
if (info.app.create_status !== 'complete') data['Create Status'] = info.app.create_status
if (info.app.space) data.Space = info.app.space.name
if (info.app.space) data.Space = color.space(info.app.space.name)
if (info.app.space && info.app.internal_routing) data['Internal Routing'] = info.app.internal_routing
if (info.pipeline_coupling) data.Pipeline = `${info.pipeline_coupling.pipeline.name} - ${info.pipeline_coupling.stage}`

Expand All @@ -87,7 +88,7 @@ function print(info: Heroku.App, addons: Heroku.AddOn[], collaborators: Heroku.C
return stack
})(info.app)

hux.styledHeader(info.app.name)
hux.styledHeader(color.app(info.app.name))
hux.styledObject(data)

if (extended) {
Expand Down
50 changes: 26 additions & 24 deletions packages/cli/src/commands/spaces/create.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {color} from '@heroku-cli/color'
import {color, hux} from '@heroku/heroku-cli-util'
import {Command, flags} from '@heroku-cli/command'
import {RegionCompletion} from '@heroku-cli/command/lib/completions.js'
import {Args, ux} from '@oclif/core'
import {hux} from '@heroku/heroku-cli-util'
import {Space} from '../../lib/types/fir.js'
import tsheredoc from 'tsheredoc'
import {displayShieldState} from '../../lib/spaces/spaces.js'
import {RegionCompletion} from '@heroku-cli/command/lib/completions.js'
import {splitCsv} from '../../lib/spaces/parsers.js'

import {getGeneration} from '../../lib/apps/generation.js'
import {splitCsv} from '../../lib/spaces/parsers.js'
import {displayShieldState} from '../../lib/spaces/spaces.js'
import {Space} from '../../lib/types/fir.js'

const heredoc = tsheredoc.default

export default class Create extends Command {
static topic = 'spaces'
static args = {
space: Args.string({hidden: true}),
}

static description = heredoc`
create a new space
`
Expand All @@ -36,23 +39,21 @@ export default class Create extends Command {
channel: flags.string({hidden: true}),
cidr: flags.string({description: 'RFC-1918 CIDR the space will use'}),
'data-cidr': flags.string({description: 'RFC-1918 CIDR used by Heroku Data resources for the space'}),
features: flags.string({hidden: true, description: 'a list of features separated by commas'}),
generation: flags.string({description: 'generation for space', default: 'cedar', options: ['cedar', 'fir']}),
'kpi-url': flags.string({hidden: true, description: 'self-managed KPI endpoint to use'}),
'log-drain-url': flags.string({hidden: true, description: 'direct log drain url'}),
region: flags.string({description: 'region name', completion: RegionCompletion}),
shield: flags.boolean({hidden: true, description: 'create a Shield space'}),
features: flags.string({description: 'a list of features separated by commas', hidden: true}),
generation: flags.string({default: 'cedar', description: 'generation for space', options: ['cedar', 'fir']}),
'kpi-url': flags.string({description: 'self-managed KPI endpoint to use', hidden: true}),
'log-drain-url': flags.string({description: 'direct log drain url', hidden: true}),
region: flags.string({completion: RegionCompletion, description: 'region name'}),
shield: flags.boolean({description: 'create a Shield space', hidden: true}),
space: flags.string({char: 's', description: 'name of space to create'}),
team: flags.team({required: true}),
}

static args = {
space: Args.string({hidden: true}),
}
static topic = 'spaces'

public async run(): Promise<void> {
const {flags, args} = await this.parse(Create)
const {channel, region, features, generation, 'log-drain-url': logDrainUrl, shield, cidr, 'kpi-url': kpiUrl, 'data-cidr': dataCidr, team} = flags
const {args, flags} = await this.parse(Create)
const {channel, cidr, 'data-cidr': dataCidr, features, generation, 'kpi-url': kpiUrl, 'log-drain-url': logDrainUrl, region, shield, team} = flags
const spaceName = flags.space || args.space

if (!spaceName) {
Expand All @@ -66,11 +67,8 @@ export default class Create extends Command {
const dollarAmountHourly = shield ? '$4.17' : '$1.39'
const spaceType = shield ? 'Shield' : 'Standard'

ux.action.start(`Creating space ${color.green(spaceName as string)} in team ${color.cyan(team as string)}`)
ux.action.start(`Creating space ${color.space(spaceName as string)} in team ${color.cyan(team as string)}`)
const {body: space} = await this.heroku.post<Required<Space>>('/spaces', {
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
body: {
channel_name: channel,
cidr,
Expand All @@ -84,14 +82,18 @@ export default class Create extends Command {
shield,
team,
},
headers: {
Accept: 'application/vnd.heroku+json; version=3.sdk',
},
})
ux.action.stop()

ux.warn(`${color.bold('Spend Alert.')} Each Heroku ${spaceType} Private Space costs ~${dollarAmountHourly}/hour (max ${dollarAmountMonthly}/month), pro-rated to the second.`)
ux.warn(`Use ${color.cmd('heroku spaces:wait')} to track allocation.`)
ux.warn(`Use ${color.command('heroku spaces:wait')} to track allocation.`)

hux.styledHeader(space.name)
hux.styledHeader(color.space(space.name))
hux.styledObject({
// eslint-disable-next-line perfectionist/sort-objects
ID: space.id, Team: space.team.name, Region: space.region.name, CIDR: space.cidr, 'Data CIDR': space.data_cidr, State: space.state, Shield: displayShieldState(space), Generation: getGeneration(space), 'Created at': space.created_at,
}, ['ID', 'Team', 'Region', 'CIDR', 'Data CIDR', 'State', 'Shield', 'Generation', 'Created at'])
}
Expand Down
26 changes: 14 additions & 12 deletions packages/cli/src/commands/spaces/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import {Args, ux} from '@oclif/core'
import {color} from '@heroku/heroku-cli-util'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'
import tsheredoc from 'tsheredoc'

import {getGeneration} from '../../lib/apps/generation.js'
import ConfirmCommand from '../../lib/confirmCommand.js'
import {displayNat} from '../../lib/spaces/spaces.js'
import {color} from '@heroku-cli/color'
import {Space} from '../../lib/types/fir.js'
import {getGeneration} from '../../lib/apps/generation.js'

const heredoc = tsheredoc.default

type RequiredSpaceWithNat = Required<Space> & {outbound_ips?: Required<Heroku.SpaceNetworkAddressTranslation>}
type RequiredSpaceWithNat = {outbound_ips?: Required<Heroku.SpaceNetworkAddressTranslation>} & Required<Space>

export default class Destroy extends Command {
static topic = 'spaces'
static args = {
space: Args.string({hidden: true}),
}

static description = heredoc`
destroy a space
`
Expand All @@ -23,16 +27,14 @@ export default class Destroy extends Command {
`]

static flags = {
space: flags.string({char: 's', description: 'space to destroy'}),
confirm: flags.string({description: 'set to space name to bypass confirm prompt', hasValue: true}),
space: flags.string({char: 's', description: 'space to destroy'}),
}

static args = {
space: Args.string({hidden: true}),
}
static topic = 'spaces'

public async run(): Promise<void> {
const {flags, args} = await this.parse(Destroy)
const {args, flags} = await this.parse(Destroy)
const {confirm} = flags
const spaceName = flags.space || args.space
if (!spaceName) {
Expand Down Expand Up @@ -68,10 +70,10 @@ export default class Destroy extends Command {
await new ConfirmCommand().confirm(
spaceName as string,
confirm,
`Destructive Action\nThis command will destroy the space ${color.bold.red(spaceName as string)}\n${natWarning}\n`,
`Destructive Action\nThis command will destroy the space ${color.space(spaceName as string)}\n${natWarning}\n`,
)

ux.action.start(`Destroying space ${color.cyan(spaceName as string)}`)
ux.action.start(`Destroying space ${color.space(spaceName as string)}`)
await this.heroku.delete(`/spaces/${spaceName}`)
ux.action.stop()
}
Expand Down
17 changes: 9 additions & 8 deletions packages/cli/src/commands/spaces/drains/set.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import {color} from '@heroku-cli/color'
import {color} from '@heroku/heroku-cli-util'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'

export default class Set extends Command {
static topic = 'spaces'
static aliases = ['drains:set']
static args = {
url: Args.string({description: 'URL to replace the log drain with', required: true}),
}

static description = 'replaces the log drain for a space'
static flags = {
space: flags.string({char: 's', description: 'space for which to set log drain', required: true}),
}

static args = {
url: Args.string({required: true, description: 'URL to replace the log drain with'}),
}
static topic = 'spaces'

public async run(): Promise<void> {
const {flags, args} = await this.parse(Set)
const {args, flags} = await this.parse(Set)
const {url} = args
const {space} = flags
const {body: drain} = await this.heroku.put<Heroku.LogDrain>(`/spaces/${space}/log-drain`, {
body: {url},
headers: {Accept: 'application/vnd.heroku+json; version=3.dogwood'},
})
ux.stdout(`Successfully set drain ${color.cyan(drain.url)} for ${color.cyan.bold(space)}.`)
ux.stdout(`Successfully set drain ${color.cyan(drain.url)} for ${color.space(space)}.`)
ux.warn('It may take a few moments for the changes to take effect.')
}
}
Loading
Loading