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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ deno.lock
*.launch
.settings/
*.sublime-workspace
.helix

# IDE - VSCode
.vscode/*
Expand Down
2 changes: 1 addition & 1 deletion .licenserc.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
header:
# default is 80, need to make it slightly longer for a long shebang
license-location-threshold: 100
license-location-threshold: 120
license:
spdx-id: MPL-2.0
content: |
Expand Down
2 changes: 2 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ export default {
'^[./]',
],
importOrderTypeScriptVersion: '5.2.2',
// ts and jsx are the default, last is needed for `await using` in bump-omicron.ts
importOrderParserPlugins: ['typescript', 'jsx', 'explicitResourceManagement'],
}
223 changes: 132 additions & 91 deletions tools/deno/bump-omicron.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#! /usr/bin/env -S deno run --allow-run=gh,git --allow-net --allow-read --allow-write --allow-env
#! /usr/bin/env -S deno run --allow-run=gh,git,mktemp --allow-net --allow-read --allow-write --allow-env

/*
* This Source Code Form is subject to the terms of the Mozilla Public
Expand All @@ -10,29 +10,28 @@
import * as flags from 'https://deno.land/std@0.159.0/flags/mod.ts'
import * as path from 'https://deno.land/std@0.159.0/path/mod.ts'
import $ from 'https://deno.land/x/dax@0.39.2/mod.ts'
import { existsSync } from 'jsr:@std/fs@1.0'

const HELP = `
Update tools/console_version in ../omicron with current console commit
hash and tarball hash and create PR in Omicron with that change.
Update tools/console_version in ../omicron to the specified console
commit and create PR in Omicron with that change. We use a git worktree
to avoid touching your Omicron clone.

Requirements:
- GitHub CLI installed
- Omicron is a sibling dir to console

Usage:
./tools/deno/bump-omicron.ts [options]
./tools/deno/bump-omicron.ts [commit-ish=main] [options]

Options:
-d, --dry-run Dry run, showing changes without creating PR
-h, --help Show this help message
-m, --message <msg> Add message to PR title: 'Bump web console (<msg>)'
`

const OMICRON_DIR = '../omicron'
const VERSION_FILE = path.join(OMICRON_DIR, 'tools/console_version')

const OMICRON_DIR = path.resolve('../omicron')
const GH_MISSING = 'GitHub CLI not found. Please install it and try again.'
const VERSION_FILE_MISSING = `Omicron console version file at '${VERSION_FILE}' not found. This script assumes Omicron is cloned in a sibling directory next to Console.`

/**
* These lines get printed in an Omicron PR, so any references to commits or
Expand All @@ -56,29 +55,28 @@ function linkifyGitLog(line: string): string {
return `* ${shaLink} ${rest}`
}

// script starts here
async function makeOmicronWorktree() {
const tmpDir = await $`mktemp -d`.text()
await $`git worktree add ${tmpDir} origin/main`.cwd(OMICRON_DIR).quiet('stdout')

const args = flags.parse(Deno.args, {
alias: { dryRun: ['d', 'dry-run'], h: 'help', m: 'message' },
boolean: ['dryRun', 'help'],
string: ['message'],
})

if (args.help) {
console.info(HELP)
Deno.exit()
return {
dir: tmpDir,
[Symbol.asyncDispose]: async function () {
console.log('Cleaning up worktree')
await $`git worktree remove ${tmpDir}`.cwd(OMICRON_DIR).quiet('stdout')
},
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

const newCommit = await $`git rev-parse HEAD`.text()

const shaUrl = `https://dl.oxide.computer/releases/console/${newCommit}.sha256.txt`
const shaResp = await fetch(shaUrl)
async function fetchTarballSha(commit: string) {
const shaUrl = `https://dl.oxide.computer/releases/console/${commit}.sha256.txt`
const shaResp = await fetch(shaUrl)

if (!shaResp.ok) {
const workflowId =
await $`gh run list -L 1 -w 'Upload assets to dl.oxide.computer' --json databaseId --jq '.[0].databaseId'`.text()
console.error(
`
if (!shaResp.ok) {
const workflowId =
await $`gh run list -L 1 -w 'Upload assets to dl.oxide.computer' --json databaseId --jq '.[0].databaseId'`.text()
console.error(
`
Failed to fetch console tarball SHA. Either the current commit is not on origin/main or the asset upload job is still running.

Status: ${shaResp.status}
Expand All @@ -87,90 +85,133 @@ Body: ${await shaResp.text()}

Running 'gh run watch ${workflowId}' to watch the current upload action.
`
)
await $`gh run watch ${workflowId}`
Deno.exit(1)
}

const newSha2 = (await shaResp.text()).trim()
const newVersionFile = `COMMIT="${newCommit}"\nSHA2="${newSha2}"\n`
)
await $`gh run watch ${workflowId}`
return Deno.exit(1)
}

const oldVersionFile = await Deno.readTextFile(VERSION_FILE).catch(() => {
throw Error(VERSION_FILE_MISSING)
})
return (await shaResp.text()).trim()
}

const oldCommit = /COMMIT="?([a-f0-9]+)"?/.exec(oldVersionFile)?.[1]
if (!oldCommit) throw Error('Could not parse existing version file')
async function getOldCommit() {
const oldVersionFile = await $`git show origin/main:tools/console_version`
.cwd(OMICRON_DIR)
.text()

if (oldCommit === newCommit) {
console.info('Nothing to update: Omicron already has the current commit pinned')
Deno.exit()
const oldCommit = /COMMIT="?([a-f0-9]+)"?/.exec(oldVersionFile)?.[1]
if (!oldCommit) throw new Error('Could not parse existing version file')
return oldCommit
}

const commitRange = `${oldCommit.slice(0, 8)}...${newCommit.slice(0, 8)}`
async function makeOmicronPR(
consoleCommit: string,
prTitle: string,
changesLink: string,
commits: string
) {
const branchName = 'bump-console-' + consoleCommit.slice(0, 8)

{
// create git worktree for latest main in temp dir. `using` ensures this gets
// cleaned up at the end of the block
await using worktree = await makeOmicronWorktree()

const newSha2 = await fetchTarballSha(consoleCommit)
const newVersionFile = `COMMIT="${consoleCommit}"\nSHA2="${newSha2}"\n`

const versionFileAbsPath = path.resolve(worktree.dir, 'tools/console_version')
await Deno.writeTextFile(versionFileAbsPath, newVersionFile)
console.info('Updated ', versionFileAbsPath)

// cd to omicron, pull main, create new branch, commit changes, push, PR it, go back to
// main, delete branch
Deno.chdir(worktree.dir)
await $`git checkout -b ${branchName}`
console.info('Created branch', branchName)

await $`git add tools/console_version`

// commits are console commits, so they won't auto-link in omicron
const commitsMarkdown = commits.split('\n').map(linkifyGitLog).join('\n')
const prBody = `${changesLink}\n\n${commitsMarkdown}`
await $`git commit -m ${prTitle} -m ${prBody}`

await $`git push --set-upstream origin ${branchName}`
console.info('Committed changes and pushed')

// create PR
const prUrl = await $`gh pr create --title ${prTitle} --body ${prBody}`.text()
console.info('PR created:', prUrl)

// set it to auto merge
const prNum = prUrl.match(/\d+$/)![0]
await $`gh pr merge ${prNum} --auto --squash`
console.info('PR set to auto-merge when CI passes')
}

const commits = await $`git log --graph --oneline ${commitRange}`.text()
// commits are console commits, so they won't auto-link in omicron
const commitsMarkdown = commits.split('\n').map(linkifyGitLog).join('\n')
// worktree has been cleaned up, so branch delete is allowed
await $`git branch -D ${branchName}`.cwd(OMICRON_DIR)
}

const changesLine = `https://github.com/oxidecomputer/console/compare/${commitRange}`
// wrapped in a function so we can do early returns rather than early
// Deno.exits, which mess up the worktree cleanup
async function run(commitIsh: string, dryRun: boolean, messageArg: string | undefined) {
await $`git fetch`.cwd(OMICRON_DIR)

const branchName = 'bump-console-' + newCommit.slice(0, 8)
const prBody = `${changesLine}\n\n${commitsMarkdown}`
const oldConsoleCommit = await getOldCommit()
const newConsoleCommit = await $`git rev-parse ${commitIsh}`.text()

console.info(`\n${changesLine}\n\n${commits}\n`)
if (oldConsoleCommit === newConsoleCommit) {
console.info(`Nothing to update: Omicron already has ${newConsoleCommit} pinned`)
return
}

if (args.dryRun) Deno.exit()
const commitRange = `${oldConsoleCommit.slice(0, 8)}...${newConsoleCommit.slice(0, 8)}`
const commits = await $`git log --graph --oneline ${commitRange}`.text()
const changesLink = `https://github.com/oxidecomputer/console/compare/${commitRange}`

const message =
args.message ||
(await $.prompt({
message: 'Description? (enter to skip)',
noClear: true,
}))
console.info(`\n${changesLink}\n\n${commits}\n`)

const prTitle = 'Bump web console' + (message ? ` (${message})` : '')
if (dryRun) return

console.info(`\nPR title: ${prTitle}\n`)
const message =
messageArg ||
(await $.prompt({ message: 'Description? (enter to skip)', noClear: true }))
const prTitle = 'Bump web console' + (message ? ` (${message})` : '')
console.info(`\nPR title: ${prTitle}\n`)

const go = await $.confirm({ message: 'Make Omicron PR?', noClear: true })
if (!go) Deno.exit()
const go = await $.confirm({ message: 'Make Omicron PR?', noClear: true })
if (!go) return

if (!$.commandExistsSync('gh')) throw Error(GH_MISSING)
if (!$.commandExistsSync('gh')) throw new Error(GH_MISSING)

await Deno.writeTextFile(VERSION_FILE, newVersionFile)
console.info('Updated ', VERSION_FILE)
const consoleDir = Deno.cwd() // save it so we can change back

const consoleDir = Deno.cwd()
await makeOmicronPR(newConsoleCommit, prTitle, changesLink, commits)

// cd to omicron, pull main, create new branch, commit changes, push, PR it, go back to
// main, delete branch
Deno.chdir(OMICRON_DIR)
await $`git checkout main`
await $`git pull`
await $`git checkout -b ${branchName}`
console.info('Created branch', branchName)
// bump omicron tag in console to current commit
Deno.chdir(consoleDir)
console.info('Bumping omicron tag in console')
await $`git tag -f -a omicron -m 'pinned commit on omicron main' ${commitIsh}`
await $`git push -f origin tag omicron`
}

await $`git add tools/console_version`
await $`git commit -m ${prTitle} -m ${prBody}`
await $`git push --set-upstream origin ${branchName}`
console.info('Committed changes and pushed')
// script starts here

// create PR
const prUrl = await $`gh pr create --title ${prTitle} --body ${prBody}`.text()
console.info('PR created:', prUrl)
const args = flags.parse(Deno.args, {
alias: { dryRun: ['d', 'dry-run'], h: 'help', m: 'message' },
boolean: ['dryRun', 'help'],
string: ['message'],
})

// set it to auto merge
const prNum = prUrl.match(/\d+$/)![0]
await $`gh pr merge ${prNum} --auto --squash`
console.info('PR set to auto-merge when CI passes')
if (args.help) {
console.info(HELP)
Deno.exit()
}

await $`git checkout main`
await $`git branch -D ${branchName}`
console.info('Checked out omicron main, deleted branch', branchName)
if (!existsSync(OMICRON_DIR)) {
throw new Error(`Omicron repo not found at ${OMICRON_DIR}`)
}

// bump omicron tag in console to current commit
Deno.chdir(consoleDir)
console.info('Bumping omicron tag in console')
await $`git tag -f -a omicron -m 'pinned commit on omicron main'`
await $`git push -f origin tag omicron`
const commitIsh = args._[0]?.toString() || 'main'
await run(commitIsh, args.dryRun, args.message)
1 change: 1 addition & 0 deletions tools/deno/deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Loading