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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ zig-out/

# Package manager files
*.lock
# Rust: keep the committed lock for reproducible git-tag/`cargo install --git`
# consumers (rust template ships Cargo.lock; the broad *.lock above would hide it).
!Cargo.lock
package-lock.json
yarn.lock
pnpm-lock.yaml
Expand Down
91 changes: 72 additions & 19 deletions mise-tasks/scaffold
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Options:
--non-interactive Skip prompts, use defaults
--project NAME Project name (default: destination directory name)
Comment thread
skapoor8 marked this conversation as resolved.
--keep-claude Keep .claude/ directory for AI workflows
--template NAME Language template to apply: uv, zig (default: agnostic)
--template NAME Language template to apply: uv, zig, go, rust, odin (default: agnostic)
--github-org SLUG GitHub org/slug for module paths and install.sh
(default: derived from git remote, else your-github-org)
DOCUMENTATION

# IMPORTS ----------------------------------------------------------------------
Expand Down Expand Up @@ -79,6 +81,25 @@ NON_INTERACTIVE=false
KEEP_CLAUDE=false
PROJECT_NAME=""
TEMPLATE=""
GITHUB_ORG=""

# Org segment used in the template's own module paths (e.g. go.mod). Scaffold
# rewrites github.com/$TEMPLATE_ORG/... to the user's org so paths like the Go
# module become github.com/<their-org>/<project> instead of a duplicated name.
TEMPLATE_ORG="cloudvoyant"

# Templates that distribute source only (no compiled binaries) — the install.sh
# binary-installer prompt is skipped for these. Odin has no package manager and links
# via the host linker, so it ships as source (git tag / GitHub source release).
SOURCE_ONLY_TEMPLATES=("odin")

is_source_only() {
local t="$1" s
for s in "${SOURCE_ONLY_TEMPLATES[@]}"; do
[[ "$t" == "$s" ]] && return 0
done
return 1
}

# Default to project root directory
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
Expand Down Expand Up @@ -112,6 +133,10 @@ while [[ $# -gt 0 ]]; do
TEMPLATE="$2"
shift 2
;;
--github-org)
GITHUB_ORG="$2"
shift 2
;;
*)
log_error "Unknown option: $1"
exit 1
Expand Down Expand Up @@ -232,15 +257,23 @@ apply_template() {
sed_inplace "s/${TEMPLATE_SNAKE}/${PROJECT_SNAKE}/g" "$file" || true
sed_inplace "s/${TEMPLATE_KEBAB}/${PROJECT_KEBAB}/g" "$file" || true
sed_inplace "s/${TEMPLATE_FLAT}/${PROJECT_FLAT}/g" "$file" || true
# Rewrite the module-path org (e.g. Go go.mod) to the user's org. Scoped to
# the project's own repo so other cloudvoyant links (claudevoyant) are untouched.
sed_inplace "s|github.com/${TEMPLATE_ORG}/${PROJECT_KEBAB}|github.com/${GITHUB_ORG}/${PROJECT_KEBAB}|g" "$file" || true
done

# Rename mise_lib_template/ directory inside src/ if template created it
# (directory names are not changed by sed — requires explicit mv)
local src_pkg_dir="$dest/src/mise_lib_template"
if [[ -d "$src_pkg_dir" ]]; then
mv "$src_pkg_dir" "$dest/src/$PROJECT_SNAKE"
# Rename the template package directory inside src/ if one was created
# (directory names are not changed by sed — requires explicit mv).
# Naming convention differs per language: Python (uv) uses snake_case,
# Go uses flatcase (no underscores). Each template ships a distinct dir name.
if [[ -d "$dest/src/mise_lib_template" ]]; then
mv "$dest/src/mise_lib_template" "$dest/src/$PROJECT_SNAKE"
log_info "Renamed src/mise_lib_template/ → src/$PROJECT_SNAKE/"
fi
if [[ -d "$dest/src/miselibtemplate" ]]; then
mv "$dest/src/miselibtemplate" "$dest/src/$PROJECT_FLAT"
log_info "Renamed src/miselibtemplate/ → src/$PROJECT_FLAT/"
fi

log_success "$template template applied"
}
Expand Down Expand Up @@ -323,6 +356,13 @@ log_info "Template: $TEMPLATE_NAME v$TEMPLATE_VERSION"
# DEFAULT PROJECT NAME ---------------------------------------------------------
DEFAULT_PROJECT=$(basename "$DEST_DIR")

# DEFAULT GITHUB ORG -----------------------------------------------------------
# Best-effort: derive from an existing git remote. Scaffold usually runs before
# `git init`, so this is often empty — fall back to a placeholder the user edits.
DEFAULT_GITHUB_ORG=$( { git -C "$DEST_DIR" remote get-url origin 2>/dev/null \
| sed -n 's|.*github.com[:/]\([^/]*\)/.*|\1|p'; } || true)
DEFAULT_GITHUB_ORG="${DEFAULT_GITHUB_ORG:-your-github-org}"

# INTERACTIVE PROMPTS ----------------------------------------------------------
if [ "$NON_INTERACTIVE" = false ]; then
log_info "Scaffolding new project from $TEMPLATE_NAME platform"
Expand All @@ -340,6 +380,12 @@ if [ "$NON_INTERACTIVE" = false ]; then
fi
done

# Prompt for GitHub org/slug (skip if already supplied via --github-org)
if [ -z "$GITHUB_ORG" ]; then
read -p "GitHub org/slug [$DEFAULT_GITHUB_ORG]: " input_org
GITHUB_ORG="${input_org:-$DEFAULT_GITHUB_ORG}"
fi

# Prompt for GCP registry configuration
read -p "Configure GCP Artifact Registry? (y/N): " configure_gcp
if [[ "$configure_gcp" =~ ^[Yy]$ ]]; then
Expand All @@ -358,15 +404,8 @@ if [ "$NON_INTERACTIVE" = false ]; then
KEEP_CLAUDE=true
fi

# Prompt for install.sh binary distribution script
read -p "Create install.sh for binary distribution? (y/N): " configure_install
if [[ "$configure_install" =~ ^[Yy]$ ]]; then
CONFIGURE_INSTALL=true
else
CONFIGURE_INSTALL=false
fi

# Prompt for language template — menu built dynamically from templates/ subdirs
# Prompt for language template — menu built dynamically from templates/ subdirs.
# Asked before install.sh so we can skip that prompt for source-only templates.
if [[ -z "$TEMPLATE" ]]; then
echo ""
echo "Select a language template:"
Expand All @@ -385,10 +424,25 @@ if [ "$NON_INTERACTIVE" = false ]; then
fi
fi

# Prompt for install.sh binary distribution script — skipped for source-only
# templates (no binaries to install).
if is_source_only "$TEMPLATE"; then
CONFIGURE_INSTALL=false
log_info "Skipping install.sh — $TEMPLATE distributes source, not binaries"
else
read -p "Create install.sh for binary distribution? (y/N): " configure_install
if [[ "$configure_install" =~ ^[Yy]$ ]]; then
CONFIGURE_INSTALL=true
else
CONFIGURE_INSTALL=false
fi
fi

echo ""
else
# Non-interactive mode: use defaults
PROJECT_NAME="${PROJECT_NAME:-$DEFAULT_PROJECT}"
GITHUB_ORG="${GITHUB_ORG:-$DEFAULT_GITHUB_ORG}"
CONFIGURE_GCP=false
CONFIGURE_INSTALL=false

Expand All @@ -397,7 +451,7 @@ else
exit 1
fi

log_info "Non-interactive mode: project=$PROJECT_NAME"
log_info "Non-interactive mode: project=$PROJECT_NAME org=$GITHUB_ORG"
if [[ -n "$TEMPLATE" ]]; then
log_info "Non-interactive mode: template=$TEMPLATE"
else
Expand Down Expand Up @@ -588,7 +642,7 @@ if [ -f "$RELEASE_YML" ]; then
# Remove "Publish template packages" step (name + if + run + env block = ~5 lines)
awk '/- name: Publish template packages/{skip=5; next} skip>0{skip--; next} {print}' "$RELEASE_YML" > "$RELEASE_YML.tmp" && mv "$RELEASE_YML.tmp" "$RELEASE_YML"
# For zig/uv: publish does not use GCP — remove GCP guard from Publish package step
if [[ "$TEMPLATE" == "zig" || "$TEMPLATE" == "uv" ]]; then
if [[ "$TEMPLATE" == "zig" || "$TEMPLATE" == "uv" || "$TEMPLATE" == "go" || "$TEMPLATE" == "rust" || "$TEMPLATE" == "odin" ]]; then
sed_inplace "s/steps.upversion.outputs.new_release_published == 'true' && env.GCP_SA_KEY != ''/steps.upversion.outputs.new_release_published == 'true'/" "$RELEASE_YML"
log_info "Removed GCP_SA_KEY guard from Publish package step ($TEMPLATE publishes without GCP)"
fi
Expand Down Expand Up @@ -645,8 +699,7 @@ log_info "Processing install.sh template"
if [ "$CONFIGURE_INSTALL" = true ]; then
# User wants install.sh - process the template
if [ -f "$DEST_DIR/install.sh.template" ]; then
# Get GitHub org from git remote (or use placeholder)
GITHUB_ORG=$(git -C "$DEST_DIR" remote get-url origin 2>/dev/null | sed -n 's|.*github.com[:/]\([^/]*\)/.*|\1|p' || echo "your-github-org")
# GITHUB_ORG was resolved earlier (flag / prompt / git-remote fallback)

# Process install.sh.template
sed "s/{{PROJECT_NAME}}/$PROJECT_NAME/g; \
Expand Down
45 changes: 45 additions & 0 deletions mise-tasks/templates/publish
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,51 @@ log_info "Uploading mise-zig-template $VERSION binaries to GitHub release v$VERS
(cd "$TMP_DIR/mise-zig-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-zig-template $VERSION assets attached to GitHub release v$VERSION"

# --- mise-go-template → GitHub Releases (attach to existing release) ---
# Go has no manifest version field; only version.txt is bumped. The mise-lib-template
# GitHub Release at v$VERSION already exists (created by semantic-release before this runs).
log_info "Scaffolding mise-go-template..."
mkdir -p "$TMP_DIR/mise-go-template"
mise run scaffold -- \
--src . --dest "$TMP_DIR/mise-go-template" \
--project mise-go-template --template go --non-interactive

echo "$VERSION" > "$TMP_DIR/mise-go-template/version.txt"

log_info "Uploading mise-go-template $VERSION binaries to GitHub release v$VERSION..."
(cd "$TMP_DIR/mise-go-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-go-template $VERSION assets attached to GitHub release v$VERSION"

# --- mise-rust-template → GitHub Releases (attach to existing release) ---
# Same release as zig; GH_REPO ensures gh targets the base repo from .tmp/.
log_info "Scaffolding mise-rust-template..."
mkdir -p "$TMP_DIR/mise-rust-template"
mise run scaffold -- \
--src . --dest "$TMP_DIR/mise-rust-template" \
--project mise-rust-template --template rust --non-interactive

echo "$VERSION" > "$TMP_DIR/mise-rust-template/version.txt"
sed_inplace "s/^version = \".*\"/version = \"$VERSION\"/" \
"$TMP_DIR/mise-rust-template/Cargo.toml"

log_info "Uploading mise-rust-template $VERSION binaries to GitHub release v$VERSION..."
(cd "$TMP_DIR/mise-rust-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-rust-template $VERSION assets attached to GitHub release v$VERSION"

# --- mise-odin-template → GitHub Releases (attach to existing release) ---
# Odin has no manifest — only version.txt needs the release version (simpler than zig).
log_info "Scaffolding mise-odin-template..."
mkdir -p "$TMP_DIR/mise-odin-template"
mise run scaffold -- \
--src . --dest "$TMP_DIR/mise-odin-template" \
--project mise-odin-template --template odin --non-interactive

echo "$VERSION" > "$TMP_DIR/mise-odin-template/version.txt"

log_info "Uploading mise-odin-template $VERSION binaries to GitHub release v$VERSION..."
(cd "$TMP_DIR/mise-odin-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-odin-template $VERSION assets attached to GitHub release v$VERSION"

# --- mise-pnpm-template → npm ---
log_info "Scaffolding mise-pnpm-template..."
mkdir -p "$TMP_DIR/mise-pnpm-template"
Expand Down
45 changes: 45 additions & 0 deletions mise-tasks/templates/publish-rc
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,51 @@ log_info "Uploading mise-zig-template $RC_TAG binaries to GitHub pre-release..."
(cd "$TMP_DIR/mise-zig-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-zig-template $RC_TAG assets uploaded to GitHub pre-release"

# --- mise-go-template RC → assets on GitHub pre-release ---
# Pre-release already created above; go publish uploads to the existing release.
log_info "Scaffolding mise-go-template RC..."
mkdir -p "$TMP_DIR/mise-go-template"
mise run scaffold -- \
--src . --dest "$TMP_DIR/mise-go-template" \
--project mise-go-template --template go --non-interactive

echo "${RC_TAG#v}" > "$TMP_DIR/mise-go-template/version.txt"

log_info "Uploading mise-go-template $RC_TAG binaries to GitHub pre-release..."
(cd "$TMP_DIR/mise-go-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-go-template $RC_TAG assets uploaded to GitHub pre-release"

# --- mise-rust-template RC → assets on GitHub pre-release ---
# Pre-release already created above; rust publish uploads to existing release.
log_info "Scaffolding mise-rust-template RC..."
mkdir -p "$TMP_DIR/mise-rust-template"
mise run scaffold -- \
--src . --dest "$TMP_DIR/mise-rust-template" \
--project mise-rust-template --template rust --non-interactive

echo "${RC_TAG#v}" > "$TMP_DIR/mise-rust-template/version.txt"
sed_inplace "s/^version = \".*\"/version = \"${RC_TAG#v}\"/" \
"$TMP_DIR/mise-rust-template/Cargo.toml"

log_info "Uploading mise-rust-template $RC_TAG binaries to GitHub pre-release..."
(cd "$TMP_DIR/mise-rust-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-rust-template $RC_TAG assets uploaded to GitHub pre-release"

# --- mise-odin-template RC → assets on GitHub pre-release ---
# Pre-release already created above; odin publish uploads to the existing release.
# Odin has no manifest — only version.txt needs the RC version.
log_info "Scaffolding mise-odin-template RC..."
mkdir -p "$TMP_DIR/mise-odin-template"
mise run scaffold -- \
--src . --dest "$TMP_DIR/mise-odin-template" \
--project mise-odin-template --template odin --non-interactive

echo "${RC_TAG#v}" > "$TMP_DIR/mise-odin-template/version.txt"

log_info "Uploading mise-odin-template $RC_TAG binaries to GitHub pre-release..."
(cd "$TMP_DIR/mise-odin-template" && GH_REPO="$BASE_GH_REPO" mise run publish)
log_success "mise-odin-template $RC_TAG assets uploaded to GitHub pre-release"

# --- mise-pnpm-template RC → npm ---
log_info "Scaffolding mise-pnpm-template RC..."
mkdir -p "$TMP_DIR/mise-pnpm-template"
Expand Down
13 changes: 8 additions & 5 deletions templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ Templates are sets of language-specific override and extension files that are la

## Available Templates

| Name | Language | Registry | Description |
| ------ | ---------- | --------------------- | ------------------------------------------------------- |
| `uv` | Python | PyPI | Python library using uv, ruff, pytest |
| `zig` | Zig | GitHub Releases + GCP | Zig library/binary with cross-platform build |
| `pnpm` | TypeScript | npm | TypeScript library using pnpm, vitest, ESLint, Prettier |
| Name | Language | Registry | Description |
| ------ | ---------- | ------------------------ | ---------------------------------------------------------- |
| `uv` | Python | PyPI | Python library using uv, ruff, pytest |
| `zig` | Zig | GitHub Releases + GCP | Zig library/binary with cross-platform build |
| `go` | Go | GitHub Releases | Go library/CLI with cross-platform build |
| `rust` | Rust | GitHub Releases | Rust library/binary; git-install + cross-platform binaries |
| `odin` | Odin | GitHub Releases (source) | Odin source library; git-tag/source releases, no binaries |
| `pnpm` | TypeScript | npm | TypeScript library using pnpm, vitest, ESLint, Prettier |

## Task Contract

Expand Down
16 changes: 16 additions & 0 deletions templates/go/.releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
["@semantic-release/exec", {
"prepareCmd": "echo '${nextRelease.version}' > version.txt"
}],
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "version.txt"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}],
"@semantic-release/github"
]
}
56 changes: 56 additions & 0 deletions templates/go/CLAUDE.md.append
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!-- @context: go, build-system, cross-platform -->

## Go Development (go template)

**Build system**: `go build` / `go test` via mise tasks — do not invoke `go` directly for
release builds; use the tasks so the version ldflags and cross-compile matrix stay consistent.

**Common commands:**
- `mise run build` — compile all packages (`go build ./...`)
- `mise run build:prod` — stripped static release binary (`CGO_ENABLED=0 go build -ldflags "-X main.version=$VERSION -s -w"`)
- `mise run test` — run tests (`go test ./...`)
- `mise run run` — build and run (`go run .`)
- `mise run format` / `mise run format:check` — `gofmt -w .` / `gofmt -l .`
- `mise run lint` / `mise run lint:fix` — `go vet ./...`
- `mise run build:all-platforms` — cross-compile all targets to `dist/release/`
- `mise run publish` — build all platforms, upload binaries to the existing GitHub release
(created by `mise run upversion`)

**Source layout:**
- `main.go` — CLI entry point (`package main`); `var version` is set at build time via ldflags
- `src/<project>/` — library package, imported by module path; add exported functions here
- `go.mod` — module manifest; the module path org must match your GitHub org (see Publishing)

**Version injection:** the binary version comes from `-ldflags "-X main.version=$VERSION"`,
where `$VERSION` is read from `version.txt`. Do not hardcode versions in source.

**Writing tests:**
- Test files are `*_test.go` next to the code, `func TestXxx(t *testing.T)`
- Run with `mise run test` (wraps `go test ./...`); `go test -v ./...` for per-test output
- `go test -run TestName ./...` to run a single test

**Cross-compilation targets** (pure-Go, `CGO_ENABLED=0` — no C toolchain needed):
- Linux: `x86_64-linux`, `aarch64-linux`
- macOS: `x86_64-macos`, `aarch64-macos`
- Windows: `x86_64-windows` (binary is `.exe` inside the `.tar.gz`)

**Adding dependencies:**
1. `go get github.com/org/dep@vX.Y.Z`
2. `go mod tidy`
3. Commit the updated `go.mod` / `go.sum`

**Publishing:**
- Libraries publish by tag alone: `mise run upversion` creates the `vX.Y.Z` tag + GitHub
Release, which makes the module consumable via `go get <module>@vX.Y.Z`. **The `go.mod`
module path org must match this repo's GitHub org** — update it if scaffold left the
placeholder org.
- CLIs additionally get prebuilt binaries: `mise run publish` cross-compiles all platforms and
uploads the `.tar.gz` assets to the existing release (requires `GH_TOKEN`/`GITHUB_TOKEN`).
- End users install via `install.sh` (downloads from GitHub Releases) or
`go install <module>@vX.Y.Z`.

**Graduating to GoReleaser:** this template hand-rolls the cross-compile matrix to match the
repo's semantic-release-owns-the-release model. If you later need signed checksums, Homebrew
taps, Linux packages (`nfpm`), or SBOMs, migrate `publish`/`build:all-platforms` to
[GoReleaser](https://goreleaser.com/) — but note it wants to own tag→changelog→release itself,
which conflicts with the `.releaserc.json` flow, so you would also rework `upversion`.
Loading