diff --git a/mise.toml b/mise.toml index 7027386d1dd17fa1985da9c9a1d07fc893fbe0de..05f4a76568efb0ae9b2d045b07e74ca9146448f5 100644 --- a/mise.toml +++ b/mise.toml @@ -20,3 +20,141 @@ run = "bun run typecheck" [tasks.check] description = "Format, lint, and type-check" depends = ["fmt", "lint", "typecheck"] + +[tasks."release:bump"] +description = "Bump one or more package versions" +usage = ''' +arg "" help="SemVer increment or exact version" +arg "" help="Package selectors" var=#true var_min=1 +flag "--preid " help="Prerelease identifier for pre* increments" +''' +shell = "bash -c" +run = ''' +set -euo pipefail + +version=${usage_version:?} +preid=${usage_preid:-} +eval "packages=(${usage_packages:?})" + +exact_version_re='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' +is_exact_version=false +if [[ "$version" =~ $exact_version_re ]]; then + is_exact_version=true +fi + +case "$version" in + patch|minor|major|prepatch|preminor|premajor|prerelease) ;; + *) + if [[ "$is_exact_version" != true ]]; then + echo "Error: unsupported version '$version'" >&2 + echo "Use patch, minor, major, prepatch, preminor, premajor, prerelease, or an exact SemVer." >&2 + exit 1 + fi + ;; +esac + +if [[ -n "$preid" ]]; then + case "$version" in + prepatch|preminor|premajor|prerelease) ;; + *) + echo "Error: --preid only applies to prepatch, preminor, premajor, or prerelease" >&2 + exit 1 + ;; + esac +fi + +resolved_dirs=$(bash scripts/resolve-release-packages.bash "${packages[@]}") +mapfile -t dirs <<< "$resolved_dirs" + +if [[ "$is_exact_version" == true && -n "${dirs[1]:-}" ]]; then + echo "Error: exact versions may only be applied to one package at a time" >&2 + exit 1 +fi + +for dir in "${dirs[@]}"; do + args=(pm version "$version" --no-git-tag-version) + if [[ -n "$preid" ]]; then + args+=(--preid "$preid") + fi + echo "Bumping $dir" + ( + cd "$dir" + bun "${args[@]}" + ) +done +''' + +[tasks."release:pack"] +description = "Check and dry-run npm package packing" +usage = ''' +arg "" help="Package selectors" var=#true var_min=1 +''' +shell = "bash -c" +run = ''' +set -euo pipefail + +eval "packages=(${usage_packages:?})" +resolved_dirs=$(bash scripts/resolve-release-packages.bash "${packages[@]}") +mapfile -t dirs <<< "$resolved_dirs" + +mise run check + +for dir in "${dirs[@]}"; do + echo "Packing $dir (dry run)" + bunx npm pack --dry-run "./$dir" +done +''' + +[tasks."release:publish"] +description = "Check, dry-run, and publish npm packages" +usage = ''' +arg "" help="Package selectors" var=#true var_min=1 +flag "--tag " help="npm dist-tag" default="latest" +flag "--otp " help="One-time password for npm two-factor auth" +''' +shell = "bash -c" +run = ''' +set -euo pipefail + +package_version() { + bun -e 'const fs = require("node:fs"); const path = require("node:path"); const pkg = JSON.parse(fs.readFileSync(path.join(process.argv[1], "package.json"), "utf8")); console.log(pkg.version);' "$1" +} + +eval "packages=(${usage_packages:?})" +tag=${usage_tag:?} +otp=${usage_otp:-} +resolved_dirs=$(bash scripts/resolve-release-packages.bash "${packages[@]}") +mapfile -t dirs <<< "$resolved_dirs" + +if [[ -n "$otp" && -n "${dirs[1]:-}" ]]; then + echo "Error: --otp may only be used with one package at a time" >&2 + echo "Use an npm automation token for multi-package publishing, or publish each package separately with a fresh OTP." >&2 + exit 1 +fi + +for dir in "${dirs[@]}"; do + version=$(package_version "$dir") + if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+- && "$tag" == latest ]]; then + echo "Error: refusing to publish prerelease $dir@$version with tag latest" >&2 + echo "Use --tag beta or another prerelease dist-tag." >&2 + exit 1 + fi +done + +mise run check + +publish_args=(--access public --tag "$tag") +if [[ -n "$otp" ]]; then + publish_args+=(--otp "$otp") +fi + +for dir in "${dirs[@]}"; do + echo "Publishing $dir (dry run)" + bunx npm publish --dry-run "${publish_args[@]}" "./$dir" +done + +for dir in "${dirs[@]}"; do + echo "Publishing $dir" + bunx npm publish "${publish_args[@]}" "./$dir" +done +''' diff --git a/scripts/resolve-release-packages.bash b/scripts/resolve-release-packages.bash new file mode 100755 index 0000000000000000000000000000000000000000..b5c9277b02c6b8569cf8cb14f76c5bfe6347a6f2 --- /dev/null +++ b/scripts/resolve-release-packages.bash @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Amolith +# +# SPDX-License-Identifier: CC0-1.0 + +set -euo pipefail + +valid_packages="answer, handoff, personas, questionnaire" + +resolve_package_dir() { + case "$1" in + answer|pi-answer|@amolith/pi-answer) printf '%s\n' packages/answer ;; + handoff|pi-handoff|@amolith/pi-handoff) printf '%s\n' packages/handoff ;; + personas|pi-personas|@amolith/pi-personas) printf '%s\n' packages/personas ;; + questionnaire|pi-questionnaire|@amolith/pi-questionnaire) printf '%s\n' packages/questionnaire ;; + *) + echo "Error: unknown package '$1'" >&2 + echo "Valid packages: $valid_packages" >&2 + exit 1 + ;; + esac +} + +if [[ $# -eq 0 ]]; then + echo "Error: at least one package is required" >&2 + echo "Valid packages: $valid_packages" >&2 + exit 1 +fi + +results=() +for package in "$@"; do + dir=$(resolve_package_dir "$package") + if [[ ${#results[@]} -gt 0 ]]; then + for existing in "${results[@]}"; do + if [[ "$existing" == "$dir" ]]; then + echo "Error: multiple selectors resolved to '$dir' (duplicate triggered by '$package')" >&2 + exit 1 + fi + done + fi + results+=("$dir") +done + +printf '%s\n' "${results[@]}"