mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 16:51:55 +00:00
Closes [#47557](https://github.com/sourcegraph/sourcegraph/issues/47557) This is a somewhat gruesome PR to read, so sorry for that. I wanted to keep this change atomic so it's easy to revert if we need. So, I set out to automate the dates in the release config and realized that fundamentally the architecture of the release tool was coupled to this config in a way that was making life more difficult than it needed to be. This PR refactors the release config in the release tool. It uses an entirely new schema, and for the most part automates every interaction anyone will have with the config. The new schema is designed to do a few things: 1. Deprecate the annoying cached version by specifying two sections of the release config, both a definition and an active releases field. Anything in the active releases is considered in progress. 2. Sets up some architecture to support multi-release functionality from the release tool. There are guard rails right now since this isn't fully supported. Also, * Automatically detect and suggest versions * Automatically generate release dates from a specified date or current time * QOL changes to flow to the appropriate commands if the release config is in the wrong state. For example it will prompt for input if the config is empty for a given version for commands that require a valid state * Guardrails around every interaction I could find that causes problems * New commands to interact with the release config (activate-release, deactivate-release, util:previous-version) ## Usage To set a release as active, either use the activate command directly ``` pnpm run release release:activate-release ``` or transitively through another dependent command. All release commands that interact with the _current_ release will read from the active release in the release config. All release commands that interact with _future_ releases will read and write to the release definitions in the release config. To deactivate the release: ``` pnpm run release release:deactivate-release ``` Most of these steps are not explicitly mandatory, and are coupled into other relevant commands (for example closing the release will deactivate the release also). To add new scheduled release definitions: ``` pnpm run release release:prepare ``` To remove scheduled releases: ``` pnpm run release release:remove ``` To determine previous versions: ``` pnpm run release util:previous-version 4.2.1 Getting previous version from: 4.2.1... 4.2.0 ``` or ``` pnpm run release util:previous-version Getting previous version... 4.4.2 ``` ## Test plan I did a lot of manual testing since so many commands were impacted. Some example output is below, and here is an example of a fake issue that was generated from the release tool: https://github.com/sourcegraph/sourcegraph/issues/47760 Example of how the input will flow if the config is not in the correct state, including version suggestions: ``` > @sourcegraph/dev-release@0.0.1 release /Users/coury@sourcegraph.com/Documents/code/sourcegraph/dev/release > ts-node --transpile-only ./src/main.ts "release:status" No active releases are defined! Attempting to activate... Next minor release: 4.5.0 Next patch release: 4.4.3 Enter the version to activate: ``` ``` > @sourcegraph/dev-release@0.0.1 release /Users/coury@sourcegraph.com/Documents/code/sourcegraph/dev/release > ts-node --transpile-only ./src/main.ts "release:status" No active releases are defined! Attempting to activate... Next minor release: 4.5.0 Next patch release: 4.4.3 Enter the version to activate: 4.4.3 Attempting to detect previous version... Detected previous version: 4.4.2 Release definition not found for: 4.4.3, enter release information. Enter the release date (YYYY-MM-DD). Enter blank to use current date: Using current time: 2023-02-16T11:01:34.710-08:00 Enter the github username of the release captain: coury-clark Enter the slack username of the release captain: coury Version created: { "codeFreezeDate": "2023-02-09T10:01:34.710-08:00", "securityApprovalDate": "2023-02-09T10:01:34.710-08:00", "releaseDate": "2023-02-16T10:01:34.710-08:00", "current": "4.4.3", "captainGitHubUsername": "coury-clark", "captainSlackUsername": "coury" } Release: 4.4.3 activated! ``` <!-- All pull requests REQUIRE a test plan: https://docs.sourcegraph.com/dev/background-information/testing_principles --> ## App preview: - [Web](https://sg-web-cclark-refactor-release-config.onrender.com/search) Check out the [client app preview documentation](https://docs.sourcegraph.com/dev/how-to/client_pr_previews) to learn more.
253 lines
8.4 KiB
TypeScript
253 lines
8.4 KiB
TypeScript
import { readFileSync, writeFileSync } from 'fs'
|
|
|
|
import chalk from 'chalk'
|
|
import { parse as parseJSONC } from 'jsonc-parser'
|
|
import { DateTime } from 'luxon'
|
|
import * as semver from 'semver'
|
|
import { SemVer } from 'semver'
|
|
|
|
import { getPreviousVersion } from './git'
|
|
import { retryInput } from './util'
|
|
|
|
const releaseConfigPath = 'release-config.jsonc'
|
|
|
|
/**
|
|
* Release configuration file format
|
|
*/
|
|
export interface Config {
|
|
teamEmail: string
|
|
|
|
captainSlackUsername: string
|
|
captainGitHubUsername: string
|
|
|
|
previousRelease: string
|
|
upcomingRelease: string
|
|
|
|
oneWorkingWeekBeforeRelease: string
|
|
threeWorkingDaysBeforeRelease: string
|
|
releaseDate: string
|
|
oneWorkingDayAfterRelease: string
|
|
oneWorkingWeekAfterRelease: string
|
|
|
|
slackAnnounceChannel: string
|
|
|
|
dryRun: {
|
|
tags?: boolean
|
|
changesets?: boolean
|
|
trackingIssues?: boolean
|
|
slack?: boolean
|
|
calendar?: boolean
|
|
}
|
|
}
|
|
|
|
export async function getActiveRelease(config: ReleaseConfig): Promise<ActiveRelease> {
|
|
if (!config.in_progress || config.in_progress.releases.length === 0) {
|
|
console.log(chalk.yellow('No active releases are defined! Attempting to activate...'))
|
|
await activateRelease(config)
|
|
}
|
|
if (!config.in_progress) {
|
|
throw new Error('unable to activate a release!')
|
|
}
|
|
if (config.in_progress.releases.length > 1) {
|
|
throw new Error(
|
|
chalk.red(
|
|
'The release config has multiple versions activated. This feature is not yet supported by the release tool! Please activate only a single release.'
|
|
)
|
|
)
|
|
}
|
|
const rel = config.in_progress.releases[0]
|
|
const def = config.scheduledReleases[rel.version]
|
|
const version = new SemVer(rel.version)
|
|
return {
|
|
version,
|
|
previous: new SemVer(rel.previous),
|
|
...(def as ReleaseDates),
|
|
...(def as ReleaseCaptainInformation),
|
|
branch: `${version.minor}.${version.minor}`,
|
|
}
|
|
}
|
|
|
|
export function loadReleaseConfig(): ReleaseConfig {
|
|
return parseJSONC(readFileSync(releaseConfigPath).toString()) as ReleaseConfig
|
|
}
|
|
|
|
export function saveReleaseConfig(config: ReleaseConfig): void {
|
|
writeFileSync(releaseConfigPath, JSON.stringify(config, null, 2))
|
|
}
|
|
|
|
export function newRelease(
|
|
version: SemVer,
|
|
releaseDate: DateTime,
|
|
captainGithub: string,
|
|
captainSlack: string
|
|
): ScheduledReleaseDefinition {
|
|
return {
|
|
...releaseDates(releaseDate),
|
|
current: version.version,
|
|
captainGitHubUsername: captainGithub,
|
|
captainSlackUsername: captainSlack,
|
|
}
|
|
}
|
|
|
|
export async function newReleaseFromInput(versionOverride?: SemVer): Promise<ScheduledReleaseDefinition> {
|
|
let version = versionOverride
|
|
if (!version) {
|
|
version = await selectVersionWithSuggestion('Enter the desired version number')
|
|
}
|
|
|
|
const releaseDateStr = await retryInput(
|
|
'Enter the release date (YYYY-MM-DD). Enter blank to use current date: ',
|
|
val => {
|
|
if (val && /^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
|
return true
|
|
}
|
|
// this will return false if the input doesn't match the regexp above but does exist, allowing blank input to still be valid
|
|
return !val
|
|
},
|
|
'invalid date, expected format YYYY-MM-DD'
|
|
)
|
|
let releaseTime: DateTime
|
|
if (!releaseDateStr) {
|
|
releaseTime = DateTime.now().setZone('America/Los_Angeles')
|
|
console.log(chalk.blue(`Using current time: ${releaseTime.toString()}`))
|
|
} else {
|
|
releaseTime = DateTime.fromISO(releaseDateStr, { zone: 'America/Los_Angeles' })
|
|
}
|
|
|
|
const captainGithubUsername = await retryInput('Enter the github username of the release captain: ', val => !!val)
|
|
const captainSlackUsername = await retryInput('Enter the slack username of the release captain: ', val => !!val)
|
|
|
|
const rel = newRelease(version, releaseTime, captainGithubUsername, captainSlackUsername)
|
|
console.log(chalk.green('Version created:'))
|
|
console.log(chalk.green(JSON.stringify(rel, null, 2)))
|
|
return rel
|
|
}
|
|
|
|
function releaseDates(releaseDate: DateTime): ReleaseDates {
|
|
releaseDate = releaseDate.set({ hour: 10 })
|
|
return {
|
|
codeFreezeDate: releaseDate.plus({ days: -7 }).toString(),
|
|
securityApprovalDate: releaseDate.plus({ days: -7 }).toString(),
|
|
releaseDate: releaseDate.toString(),
|
|
}
|
|
}
|
|
|
|
export function addScheduledRelease(config: ReleaseConfig, release: ScheduledReleaseDefinition): ReleaseConfig {
|
|
config.scheduledReleases[release.current] = release
|
|
return config
|
|
}
|
|
|
|
export function removeScheduledRelease(config: ReleaseConfig, version: string): ReleaseConfig {
|
|
delete config.scheduledReleases[version]
|
|
return config
|
|
}
|
|
|
|
export interface ReleaseDates {
|
|
releaseDate: string
|
|
codeFreezeDate: string
|
|
securityApprovalDate: string
|
|
}
|
|
|
|
export interface ActiveRelease extends ReleaseCaptainInformation, ReleaseDates {
|
|
version: SemVer
|
|
previous: SemVer
|
|
branch: string
|
|
}
|
|
|
|
export interface ActiveReleaseDefinition {
|
|
version: string
|
|
previous: string
|
|
}
|
|
|
|
export interface ReleaseCaptainInformation {
|
|
captainSlackUsername: string
|
|
captainGitHubUsername: string
|
|
}
|
|
|
|
export interface InProgress extends ReleaseCaptainInformation {
|
|
releases: ActiveReleaseDefinition[]
|
|
}
|
|
|
|
export interface ReleaseConfig {
|
|
metadata: {
|
|
teamEmail: string
|
|
slackAnnounceChannel: string
|
|
}
|
|
scheduledReleases: {
|
|
[version: string]: ScheduledReleaseDefinition
|
|
}
|
|
in_progress?: InProgress
|
|
dryRun: {
|
|
tags?: boolean
|
|
changesets?: boolean
|
|
trackingIssues?: boolean
|
|
slack?: boolean
|
|
calendar?: boolean
|
|
}
|
|
}
|
|
|
|
export interface ScheduledReleaseDefinition extends ReleaseDates, ReleaseCaptainInformation {
|
|
current: string
|
|
}
|
|
|
|
// Prompt a user for input and activate the given release version if possible. Will redirect to release creation input if
|
|
// the version isn't defined.
|
|
export async function activateRelease(config: ReleaseConfig): Promise<void> {
|
|
const next = await selectVersionWithSuggestion('Enter the version to activate')
|
|
console.log('Attempting to detect previous version...')
|
|
const previous = getPreviousVersion(next)
|
|
console.log(chalk.blue(`Detected previous version: ${previous.version}`))
|
|
|
|
const scheduled = await getScheduledReleaseWithInput(config, next)
|
|
config.in_progress = {
|
|
captainGitHubUsername: scheduled.captainGitHubUsername,
|
|
captainSlackUsername: scheduled.captainSlackUsername,
|
|
releases: [{ version: next.version, previous: previous.version }],
|
|
}
|
|
saveReleaseConfig(config)
|
|
console.log(chalk.green(`Release: ${next.version} activated!`))
|
|
}
|
|
|
|
export function deactivateAllReleases(config: ReleaseConfig): void {
|
|
delete config.in_progress
|
|
saveReleaseConfig(config)
|
|
}
|
|
|
|
// Prompt a user for a major / minor version input with automation suggestion by adding a minor version to the previous version.
|
|
async function selectVersionWithSuggestion(prompt: string): Promise<SemVer> {
|
|
const probablyMinor = getPreviousVersion().inc('minor')
|
|
const probablyPatch = getPreviousVersion().inc('patch')
|
|
const input = await retryInput(
|
|
`Next minor release: ${probablyMinor.version}\nNext patch release: ${probablyPatch.version}\n${chalk.blue(
|
|
prompt
|
|
)}: `,
|
|
val => {
|
|
const version = semver.parse(val)
|
|
return !!version
|
|
}
|
|
)
|
|
return new SemVer(input)
|
|
}
|
|
|
|
// Prompt a user for a release definition input, and redirect to creation input if it doesn't exist.
|
|
export async function getReleaseDefinition(config: ReleaseConfig): Promise<ScheduledReleaseDefinition> {
|
|
const next = await selectVersionWithSuggestion('Enter the version number to select')
|
|
return getScheduledReleaseWithInput(config, next)
|
|
}
|
|
|
|
// Helper function to get a release definition from the release config, redirecting to creation input if it doesn't exist.
|
|
async function getScheduledReleaseWithInput(
|
|
config: ReleaseConfig,
|
|
releaseVersion: SemVer
|
|
): Promise<ScheduledReleaseDefinition> {
|
|
let scheduled = config.scheduledReleases[releaseVersion.version]
|
|
if (!scheduled) {
|
|
console.log(
|
|
chalk.yellow(`Release definition not found for: ${releaseVersion.version}, enter release information.\n`)
|
|
)
|
|
scheduled = await newReleaseFromInput(releaseVersion)
|
|
addScheduledRelease(config, scheduled)
|
|
}
|
|
return scheduled
|
|
}
|