diff --git a/.gitignore b/.gitignore index 843696fd2..9a5086b72 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ deno.lock *.launch .settings/ *.sublime-workspace +.helix # IDE - VSCode .vscode/* diff --git a/.licenserc.yaml b/.licenserc.yaml index 313e56c79..ad6d0d211 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -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: | diff --git a/.prettierrc.js b/.prettierrc.js index dbce9f0a1..73997802c 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -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'], } diff --git a/tools/deno/bump-omicron.ts b/tools/deno/bump-omicron.ts index 1f639f968..01130890f 100755 --- a/tools/deno/bump-omicron.ts +++ b/tools/deno/bump-omicron.ts @@ -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 @@ -10,17 +10,19 @@ 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 @@ -28,11 +30,8 @@ Options: -m, --message Add message to PR title: 'Bump web console ()' ` -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 @@ -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') + }, + } } -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} @@ -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) diff --git a/tools/deno/deno.jsonc b/tools/deno/deno.jsonc new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tools/deno/deno.jsonc @@ -0,0 +1 @@ +{}