From f7741104309e8722421eb23fce4a9fecd8e85ca6 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson <2608646+ryanatkn@users.noreply.github.com> Date: Wed, 7 Apr 2021 15:34:12 -0500 Subject: [PATCH] update build, deploy, and start tasks to work with SvelteKit (#157) --- README.md | 10 +++- changelog.md | 8 ++- src/build.task.ts | 67 +++++++++++++++------- src/cert.task.ts | 4 +- src/config/defaultBuildConfig.ts | 20 ++++--- src/deploy.task.ts | 96 +++++++++++++++++++++----------- src/docs/README.md | 1 + src/docs/deploy.md | 16 ++++++ src/docs/tasks.md | 2 +- src/fs/watchNodeFs.ts | 1 + src/start.task.ts | 61 +++++++++++--------- src/task/runTask.ts | 6 +- src/version.task.ts | 19 +++++-- 13 files changed, 210 insertions(+), 101 deletions(-) create mode 100644 src/docs/deploy.md diff --git a/README.md b/README.md index 5208a7b80f..2380a020c0 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ It includes: ## docs - [unbundled development](/src/docs/unbundled.md) for web frontends, servers, and libraries +- [deploy](/src/docs/deploy.md) to GitHub pages - [`task`](/src/task) runner - [dev server](/src/server) - [`gen`](/src/gen) code generation @@ -148,8 +149,13 @@ npm run bootstrap # build and link `gro` - needed only once gro test # make sure everything looks good - same as `npm test` # development -gro dev # start dev server in watch mode -gro project/dist # update the `gro` CLI +gro dev # start dev server in watch mode; it's designed as a long-running process +gro build # update the `gro` CLI locally (see comment directly below: slow because bug) +# gro project/dist # TODO this should be the command but it's currently bugged + +# use your development version of `gro` locally in another project +cd ../otherproject +npm link ../gro # release gro build # build for release and update the `gro` CLI diff --git a/changelog.md b/changelog.md index 1b83555d98..bc7002f395 100644 --- a/changelog.md +++ b/changelog.md @@ -2,8 +2,14 @@ ## 0.15.0 +- **break**: make `src/build.task.ts`, `src/deploy.task.ts`, + and `src/start.task.ts` work with SvelteKit + ([#157](https://github.com/feltcoop/gro/pull/157)) + - add flag `gro deploy --clean` to reset deployment state + - add flag `--branch` to both tasks, default to `main` + - default to the `deploy` branch instead of `gh-pages` - **break**: rename `toEnvString` and `toEnvNumber` from `stringFromEnv` and `numberFromEnv` - ([#154](https://github.com/feltcoop/gro/pull/154)) + ([#158](https://github.com/feltcoop/gro/pull/158)) ## 0.14.0 diff --git a/src/build.task.ts b/src/build.task.ts index cdd1ec8617..306594b570 100644 --- a/src/build.task.ts +++ b/src/build.task.ts @@ -2,19 +2,27 @@ import type {Task} from './task/task.js'; import {createBuild} from './project/build.js'; import type {MapInputOptions, MapOutputOptions, MapWatchOptions} from './project/build.js'; import {getDefaultEsbuildOptions} from './build/esbuildBuildHelpers.js'; -import {DIST_DIR, isThisProjectGro, sourceIdToBasePath, toBuildExtension} from './paths.js'; +import { + DIST_DIR, + isThisProjectGro, + sourceIdToBasePath, + SVELTE_KIT_BUILD_PATH, + toBuildExtension, +} from './paths.js'; import {Timings} from './utils/time.js'; import {loadGroConfig} from './config/config.js'; import type {GroConfig} from './config/config.js'; import {configureLogLevel} from './utils/log.js'; import {buildSourceDirectory} from './build/buildSourceDirectory.js'; -import type {SpawnedProcess} from './utils/process.js'; +import {SpawnedProcess, spawnProcess} from './utils/process.js'; import type {TaskEvents as ServerTaskEvents} from './server.task.js'; -import {hasApiServerConfig} from './config/defaultBuildConfig.js'; +import {hasApiServerConfig, hasSvelteKitFrontend} from './config/defaultBuildConfig.js'; import {printTiming} from './utils/print.js'; import {resolveInputFiles} from './build/utils.js'; -import {green} from './utils/terminal.js'; import {toCommonBaseDir} from './utils/path.js'; +import {clean} from './fs/clean.js'; +import {move} from './fs/node.js'; +import {printBuildConfigLabel} from './config/buildConfig.js'; export interface TaskArgs { mapInputOptions?: MapInputOptions; @@ -55,6 +63,19 @@ export const task: Task = { const esbuildOptions = getDefaultEsbuildOptions(config.target, config.sourcemap, dev); + const timingToClean = timings.start('clean'); + await clean({dist: true}, log); + timingToClean(); + + // If this is a SvelteKit frontend, for now, just build it and exit immediately. + // TODO support merging SvelteKit and Gro builds (and then delete `felt-server`'s build task) + if ((await hasSvelteKitFrontend()) && !isThisProjectGro) { + const timingToBuildSvelteKit = timings.start('SvelteKit build'); + await spawnProcess('npx', ['svelte-kit', 'build']); + await move(SVELTE_KIT_BUILD_PATH, DIST_DIR); + timingToBuildSvelteKit(); + } + // TODO think this through // This is like a "prebuild" phase. // Build everything with esbuild and Gro's `Filer` first, @@ -62,7 +83,9 @@ export const task: Task = { // See the other reference to `isThisProjectGro` for comments about its weirdness. let spawnedApiServer: SpawnedProcess | null = null; if (!isThisProjectGro) { + const timingToPrebuild = timings.start('prebuild'); await buildSourceDirectory(config, dev, log); + timingToPrebuild(); events.emit('build.prebuild'); // now that the prebuild is ready, we can start the API server, if it exists @@ -85,32 +108,34 @@ export const task: Task = { // and therefore belong in the default Rollup build. // If more customization is needed, users should implement their own `src/build.task.ts`, // which can be bootstrapped by copy/pasting this one. (and updating the imports) + const timingToBuild = timings.start('build'); await Promise.all( buildConfigsToBuild.map(async (buildConfig) => { const inputFiles = await resolveInputFiles(buildConfig); + if (!inputFiles.length) { + log.trace('no input files in', printBuildConfigLabel(buildConfig)); + return; + } // TODO ok wait, does `outputDir` need to be at the output dir path? const outputDir = `${DIST_DIR}${toBuildExtension( sourceIdToBasePath(toCommonBaseDir(inputFiles)), )}`; // const outputDir = paths.dist; - log.info(`building ${green(buildConfig.name)}`, outputDir, inputFiles); - if (inputFiles.length) { - const build = createBuild({ - dev, - sourcemap: config.sourcemap, - inputFiles, - outputDir, - mapInputOptions, - mapOutputOptions, - mapWatchOptions, - esbuildOptions, - }); - await build.promise; - } else { - log.warn(`no input files in build "${buildConfig.name}"`); - } + log.info('building', printBuildConfigLabel(buildConfig), outputDir, inputFiles); + const build = createBuild({ + dev, + sourcemap: config.sourcemap, + inputFiles, + outputDir, + mapInputOptions, + mapOutputOptions, + mapWatchOptions, + esbuildOptions, + }); + await build.promise; }), ); + timingToBuild(); // done! clean up the API server if (spawnedApiServer) { @@ -119,7 +144,9 @@ export const task: Task = { args.closeApiServer(spawnedApiServer); } else { spawnedApiServer!.child.kill(); + const timingToCloseServer = timings.start('close server'); await spawnedApiServer!.closed; + timingToCloseServer(); } } diff --git a/src/cert.task.ts b/src/cert.task.ts index 22bef89aa1..e87eb0e80e 100644 --- a/src/cert.task.ts +++ b/src/cert.task.ts @@ -12,8 +12,8 @@ export const task: Task = { const host = args.host || 'localhost'; const certFile = `${host}-cert.pem`; const keyFile = `${host}-privkey.pem`; - if (await pathExists(certFile)) throw Error(`File ${certFile} already exists. Aborting.`); - if (await pathExists(keyFile)) throw Error(`File ${keyFile} already exists. Aborting.`); + if (await pathExists(certFile)) throw Error(`File ${certFile} already exists. Canceling.`); + if (await pathExists(keyFile)) throw Error(`File ${keyFile} already exists. Canceling.`); await spawnProcess( 'openssl', `req -x509 -newkey rsa:2048 -nodes -sha256 -subj /CN=${host} -keyout ${keyFile} -out ${certFile}`.split( diff --git a/src/config/defaultBuildConfig.ts b/src/config/defaultBuildConfig.ts index a8abda3b8b..b452bbe0d3 100644 --- a/src/config/defaultBuildConfig.ts +++ b/src/config/defaultBuildConfig.ts @@ -5,6 +5,8 @@ import {toBuildExtension, basePathToSourceId, toBuildOutPath, paths} from '../pa import {pathExists} from '../fs/node.js'; import {getExtensions} from '../fs/mime.js'; +export const GIT_DEPLOY_BRANCH = 'main'; // deploy and publish from this branch + // Gro currently enforces that the primary build config // for the Node platform has this value as its name. // This convention speeds up running tasks by standardizing where Gro can look for built files. @@ -43,13 +45,14 @@ export const API_SERVER_DEFAULT_PORT_DEV = 3001; export const toApiServerBuildPath = (dev: boolean, buildDir = paths.build): string => toBuildOutPath(dev, API_SERVER_BUILD_CONFIG_NAME, API_SERVER_BUILD_BASE_PATH, buildDir); -export const hasDeprecatedGroFrontend = async (): Promise => { - const [hasIndexHtml, hasIndexTs] = await Promise.all([ - pathExists('src/index.html'), - pathExists('src/index.ts'), - ]); - return hasIndexHtml && hasIndexTs; -}; +const SVELTE_KIT_FRONTEND_PATHS = ['src/app.html', 'src/routes']; +export const hasSvelteKitFrontend = async (): Promise => + everyPathExists(SVELTE_KIT_FRONTEND_PATHS); + +const DEPRECATED_GRO_FRONTEND_PATHS = ['src/index.html', 'src/index.ts']; +export const hasDeprecatedGroFrontend = async (): Promise => + everyPathExists(DEPRECATED_GRO_FRONTEND_PATHS); + export const toDefaultBrowserBuild = (assetPaths = toDefaultAssetPaths()): PartialBuildConfig => ({ name: 'browser', platform: 'browser', @@ -57,3 +60,6 @@ export const toDefaultBrowserBuild = (assetPaths = toDefaultAssetPaths()): Parti dist: true, }); const toDefaultAssetPaths = (): string[] => Array.from(getExtensions()); + +const everyPathExists = async (paths: string[]): Promise => + (await Promise.all(paths.map((path) => pathExists(path)))).every((v) => !!v); diff --git a/src/deploy.task.ts b/src/deploy.task.ts index 23e0711ba7..6de9ffa03d 100644 --- a/src/deploy.task.ts +++ b/src/deploy.task.ts @@ -2,49 +2,78 @@ import {join, basename} from 'path'; import type {Task} from './task/task.js'; import {spawnProcess} from './utils/process.js'; -import {copy, pathExists} from './fs/node.js'; +import {copy} from './fs/node.js'; import {paths} from './paths.js'; import {printError, printPath} from './utils/print.js'; +import {magenta, green, rainbow, red} from './utils/terminal.js'; +import {GIT_DEPLOY_BRANCH} from './config/defaultBuildConfig.js'; + +// TODO support other kinds of deployments +// TODO add a flag to delete the existing deployment branch to avoid bloat (and maybe run `git gc --auto`) export interface TaskArgs { + branch?: string; // optional branch to deploy from; defaults to 'main' dry?: boolean; + clean?: boolean; // clean the git worktree and Gro cache } // TODO customize const distDir = paths.dist; const distDirName = basename(distDir); -const deploymentBranch = 'gh-pages'; -const deploymentStaticContentDir = join(paths.source, 'project/gh-pages/'); -const initialFile = 'package.json'; +const deploymentBranch = 'deploy'; +const initialFile = 'package.json'; // this is a single file that's copied into the new branch to bootstrap it const TEMP_PREFIX = '__TEMP__'; -// TODO support other kinds of deployments -// TODO add a flag to delete the existing deployment branch to avoid bloat (and maybe run `git gc --auto`) - export const task: Task = { - description: 'deploy to gh-pages', + description: 'deploy to static hosting', run: async ({invokeTask, args, log}): Promise => { - const {dry} = args; + const {branch, dry, clean} = args; + + const sourceBranch = branch || GIT_DEPLOY_BRANCH; + + // Exit early if the git working directory has any unstaged or staged changes. + const gitDiffResult = await spawnProcess('git', ['diff-index', '--quiet', 'HEAD']); + if (!gitDiffResult.ok) { + log.error(red('git working directory is unclean~ please commit or stash to proceed')); + return; + } + + // Ensure we're on the right branch. + const gitCheckoutResult = await spawnProcess('git', ['checkout', sourceBranch]); + if (!gitCheckoutResult.ok) { + log.error(red(`failed git checkout with exit code ${gitCheckoutResult.code}`)); + return; + } + // TODO filter stdout? `--quiet` didn't work // Set up the deployment branch if necessary. // If the `deploymentBranch` already exists, this is a no-op. + log.info(magenta('↓↓↓↓↓↓↓'), green('ignore any errors in here'), magenta('↓↓↓↓↓↓↓')); await spawnProcess( `git checkout --orphan ${deploymentBranch} && ` + + // TODO there's definitely a better way to do this `cp ${initialFile} ${TEMP_PREFIX}${initialFile} && ` + `git rm -rf . && ` + `mv ${TEMP_PREFIX}${initialFile} ${initialFile} && ` + `git add ${initialFile} && ` + - 'git commit -m "setup" && git checkout main', + `git commit -m "setup" && git checkout ${sourceBranch}`, [], + // this uses `shell: true` because the above is unwieldy with standard command construction {shell: true}, ); // Clean up any existing worktree. await cleanGitWorktree(); + log.info(magenta('↑↑↑↑↑↑↑'), green('ignore any errors in here'), magenta('↑↑↑↑↑↑↑')); // Get ready to build from scratch. await invokeTask('clean'); + if (clean) { + log.info(rainbow('all clean')); + return; + } + // Set up the deployment worktree in the dist directory. await spawnProcess('git', ['worktree', 'add', distDirName, deploymentBranch]); @@ -52,40 +81,41 @@ export const task: Task = { // Run the build. await invokeTask('build'); - // Copy everything from `src/project/gh-pages`. (like `CNAME` for GitHub custom domains) - // If any files conflict, throw an error! - // That should be part of the build process. - if (await pathExists(deploymentStaticContentDir)) { - await copy(deploymentStaticContentDir, distDir); - } + // Update the initial file. await copy(initialFile, join(distDir, initialFile)); } catch (err) { - log.error('Build failed:', printError(err)); + log.error(red('build failed'), 'but', green('no changes were made to git'), printError(err)); if (dry) { - log.info('Dry deploy failed! Files are available in', printPath(distDirName)); + log.info(red('dry deploy failed:'), 'files are available in', printPath(distDirName)); } else { await cleanGitWorktree(true); } - throw Error(`Deploy aborted due to build failure. See the error above.`); + throw Error(`Deploy safely canceled due to build failure. See the error above.`); } // At this point, `dist/` is ready to be committed and deployed! if (dry) { - log.info('Dry deploy complete! Files are available in', printPath(distDirName)); - } else { - await spawnProcess('git', ['add', '.'], {cwd: distDir}); - await spawnProcess('git', ['commit', '-m', 'deployment'], { - cwd: distDir, - }); - await spawnProcess('git', ['push', 'origin', deploymentBranch], { - cwd: distDir, - }); - - // Clean up the worktree so it doesn't interfere with development. - // TODO maybe add a flag to preserve these files instead of overloading `dry`? - // or maybe just create a separate `deploy` dir to avoid problems? - await cleanGitWorktree(); + log.info(green('dry deploy complete:'), 'files are available in', printPath(distDirName)); + return; + } + + try { + // TODO wait is this `cwd` correct or vestiges of the old code? + const gitArgs = {cwd: distDir}; + await spawnProcess('git', ['add', '.'], gitArgs); + await spawnProcess('git', ['commit', '-m', 'deployment'], gitArgs); + await spawnProcess('git', ['push', 'origin', deploymentBranch], gitArgs); + } catch (err) { + log.error(red('updating git failed:'), printError(err)); + throw Error(`Deploy failed in a bad state: built but not pushed. See the error above.`); } + + // Clean up the worktree so it doesn't interfere with development. + // TODO maybe add a flag to preserve these files instead of overloading `dry`? + // or maybe just create a separate `deploy` dir to avoid problems? + await cleanGitWorktree(); + + log.info(rainbow('deployed')); }, }; diff --git a/src/docs/README.md b/src/docs/README.md index c26ef4d100..d7c726aad5 100644 --- a/src/docs/README.md +++ b/src/docs/README.md @@ -3,6 +3,7 @@ > [gro](/../..) / docs / README.md - [config](config.md) +- [deploy](deploy.md) - [options](options.md) - [publish](publish.md) - [tasks](tasks.md) diff --git a/src/docs/deploy.md b/src/docs/deploy.md new file mode 100644 index 0000000000..4629ca4bb5 --- /dev/null +++ b/src/docs/deploy.md @@ -0,0 +1,16 @@ +# gro/deploy + +For now, Gro's `gro deploy` task supports only static deployments to +[GitHub pages](https://pages.github.com/). +Eventually we want to expand builtin support, +including servers and other static clouds, +but for now you need to implement `src/deploy.task.ts` yourself outside of GitHub pages. + +```bash +gro deploy # prepares dist/ +gro deploy --branch my-branch # deploy from a branch other than 'main' +gro deploy --dry # prepare dist/ but don't commit or push +gro deploy --clean # if something goes wrong, this resets git and gro state +``` + +See [`src/deploy.task.ts`](/src/deploy.task.ts) for more. diff --git a/src/docs/tasks.md b/src/docs/tasks.md index 5b203e1c8c..129d206c5e 100644 --- a/src/docs/tasks.md +++ b/src/docs/tasks.md @@ -10,7 +10,7 @@ What is a `Task`? See [`src/tasks/README.md`](../task). - [cert](../cert.task.ts) - creates a self-signed cert for https with openssl - [check](../check.task.ts) - check that everything is ready to commit - [clean](../clean.task.ts) - remove temporary dev and build files -- [deploy](../deploy.task.ts) - deploy to gh-pages +- [deploy](../deploy.task.ts) - deploy to static hosting - [dev](../dev.task.ts) - start dev server - [format](../format.task.ts) - format source files - [gen](../gen.task.ts) - run code generation scripts diff --git a/src/fs/watchNodeFs.ts b/src/fs/watchNodeFs.ts index a388e927ea..4f9ac9f9da 100644 --- a/src/fs/watchNodeFs.ts +++ b/src/fs/watchNodeFs.ts @@ -30,6 +30,7 @@ export const DEBOUNCE_DEFAULT = 10; // ignore some things in a typical Gro project // note this set is exported & mutable 🤭 +// TODO use gitignore? expose gitignore interface to gro users? export const ignoredPaths = new Set(['.git', '.svelte', 'node_modules', '.DS_Store']); const defaultFilter: PathFilter = (file) => !ignoredPaths.has(file.path); diff --git a/src/start.task.ts b/src/start.task.ts index dc6ac8a006..30dfdb6c16 100644 --- a/src/start.task.ts +++ b/src/start.task.ts @@ -1,15 +1,16 @@ import type {Task} from './task/task.js'; import {pathExists} from './fs/node.js'; import {Timings} from './utils/time.js'; -import {paths, sourceIdToBasePath, toBuildExtension} from './paths.js'; +import {isThisProjectGro, paths, sourceIdToBasePath, toBuildExtension} from './paths.js'; import type {GroConfig} from './config/config.js'; import {loadGroConfig} from './config/config.js'; -import {spawn} from './utils/process.js'; +import {spawn, spawnProcess} from './utils/process.js'; import type {SpawnedProcess} from './utils/process.js'; import {green} from './utils/terminal.js'; import type {BuildConfig} from './config/buildConfig.js'; import {printTiming} from './utils/print.js'; import {resolveInputFiles} from './build/utils.js'; +import {hasSvelteKitFrontend} from './config/defaultBuildConfig.js'; export interface TaskEvents { 'start.spawned': (spawneds: SpawnedProcess[], config: GroConfig) => void; @@ -20,40 +21,48 @@ export const task: Task<{}, TaskEvents> = { dev: false, run: async ({log, invokeTask, dev, events}) => { const timings = new Timings(); + + // build if needed if (!(await pathExists(paths.dist))) { log.info(green('dist not detected; building')); const timingToBuild = timings.start('build'); await invokeTask('build'); timingToBuild(); } + const timingToLoadConfig = timings.start('load config'); const config = await loadGroConfig(dev); timingToLoadConfig(); - const inputs: { - buildConfig: BuildConfig; - inputFile: string; - }[] = ( - await Promise.all( - // TODO this needs to be changed, might need to configure on each `buildConfig` - // maybe `dist: ['/path/to']` or `dist: {'/path/to': ...}` - config.builds.map(async (buildConfig) => - (await resolveInputFiles(buildConfig)).map((inputFile) => ({buildConfig, inputFile})), - ), - ) - ).flat(); - const spawneds: SpawnedProcess[] = inputs - .map((input) => { - if (!input.buildConfig.dist) return null!; - const path = toEntryPath(input.buildConfig); - if (!path) { - log.error('expected to find entry path for build config', input.buildConfig); - return null!; - } - return spawn('node', [path]); - }) - .filter(Boolean); - events.emit('start.spawned', spawneds, config); + // detect if we're in a SvelteKit project, and prefer that to Gro's system for now + if ((await hasSvelteKitFrontend()) && !isThisProjectGro) { + await spawnProcess('npx', ['svelte-kit', 'start']); + } else { + const inputs: { + buildConfig: BuildConfig; + inputFile: string; + }[] = ( + await Promise.all( + // TODO this needs to be changed, might need to configure on each `buildConfig` + // maybe `dist: ['/path/to']` or `dist: {'/path/to': ...}` + config.builds.map(async (buildConfig) => + (await resolveInputFiles(buildConfig)).map((inputFile) => ({buildConfig, inputFile})), + ), + ) + ).flat(); + const spawneds: SpawnedProcess[] = inputs + .map((input) => { + if (!input.buildConfig.dist) return null!; + const path = toEntryPath(input.buildConfig); + if (!path) { + log.error('expected to find entry path for build config', input.buildConfig); + return null!; + } + return spawn('node', [path]); + }) + .filter(Boolean); + events.emit('start.spawned', spawneds, config); + } for (const [key, timing] of timings.getAll()) { log.trace(printTiming(key, timing)); } diff --git a/src/task/runTask.ts b/src/task/runTask.ts index 6b5d6a312d..edf9044891 100644 --- a/src/task/runTask.ts +++ b/src/task/runTask.ts @@ -1,6 +1,6 @@ import type {EventEmitter} from 'events'; -import {cyan, red, gray} from '../utils/terminal.js'; +import {cyan, red} from '../utils/terminal.js'; import {printLogLabel, SystemLogger} from '../utils/log.js'; import type {TaskModuleMeta} from './taskModule.js'; import type {Args} from './task.js'; @@ -39,7 +39,7 @@ export const runTask = async ( dev, args, events, - log: new SystemLogger([`${printLogLabel(task.name)}${gray('[log]')}`]), + log: new SystemLogger([`${printLogLabel(task.name)}`]), invokeTask: (invokedTaskName, invokedArgs = args, invokedEvents = events, invokedDev = dev) => invokeTask(invokedTaskName, invokedArgs, invokedEvents, invokedDev), }); @@ -51,7 +51,7 @@ export const runTask = async ( ? err.message : `Unexpected error running task ${cyan( task.name, - )}. Aborting. If this is unexpected try running \`gro clean\`.`, + )}. Canceling. If this is unexpected try running \`gro clean\`.`, ), error: err, }; diff --git a/src/version.task.ts b/src/version.task.ts index 329b4fe681..ff40591860 100644 --- a/src/version.task.ts +++ b/src/version.task.ts @@ -7,15 +7,15 @@ import {green, bgBlack, rainbow, red} from './utils/terminal.js'; import {readFile} from './fs/node.js'; import {loadPackageJson} from './project/packageJson.js'; import type {Logger} from './utils/log.js'; +import {GIT_DEPLOY_BRANCH} from './config/defaultBuildConfig.js'; // version.task.ts // - usage: `gro version patch` // - forwards args to `npm version`: https://docs.npmjs.com/cli/v6/commands/npm-version // - runs the production build -// - publishes to npm from the `main` branch (TODO configure) -// - syncs commits and tags to GitHub `main` branch +// - publishes to npm from the `main` branch, configurable with `--branch` +// - syncs commits and tags to the configured main branch -// TODO configure branch // TODO add `dry` option so it can be tested type VersionIncrement = string; @@ -30,17 +30,24 @@ interface ValidateVersionIncrement { (v: unknown): asserts v is VersionIncrement; } -export const task: Task = { +export interface TaskArgs { + _: string[]; + branch?: string; +} + +export const task: Task = { description: 'bump version, publish to npm, and sync to GitHub', run: async ({args, log, invokeTask}): Promise => { + const {branch = GIT_DEPLOY_BRANCH} = args; + const versionIncrement = args._[0]; validateVersionIncrement(versionIncrement); // Confirm with the user that we're doing what they expect. await confirmWithUser(versionIncrement, log); - // Make sure we're on the main branch: - await spawnProcess('git', ['checkout', 'main']); // TODO allow configuring `'main'` + // Make sure we're on the right branch: + await spawnProcess('git', ['checkout', branch]); // And updated to the latest: await spawnProcess('git', ['pull']);