From 271b580fce9633b64f202b7d7d510942058e2fe6 Mon Sep 17 00:00:00 2001 From: Siddharth Kapoor Date: Mon, 13 Jul 2026 10:27:38 -0400 Subject: [PATCH 1/4] feat: add go language template Go library/CLI template implementing the full task contract (build, test, lint, format, publish, docker, version) with starter source, a Dockerfile, and CLAUDE.md conventions. Builds a runnable binary; module path is github.com//; idiomatic flatcase package name. --- templates/go/.releaserc.json | 16 ++++++ templates/go/CLAUDE.md.append | 56 +++++++++++++++++++ .../go/docs/development-guide.template.md | 54 ++++++++++++++++++ templates/go/go.mod | 3 + templates/go/install.sh.template | 53 ++++++++++++++++++ templates/go/main.go | 23 ++++++++ templates/go/mise-tasks/build/_default | 10 ++++ templates/go/mise-tasks/build/all-platforms | 37 ++++++++++++ templates/go/mise-tasks/build/prod | 6 ++ templates/go/mise-tasks/clean | 5 ++ templates/go/mise-tasks/docker/build | 5 ++ templates/go/mise-tasks/docker/run | 5 ++ templates/go/mise-tasks/docker/test | 5 ++ templates/go/mise-tasks/format/_default | 5 ++ templates/go/mise-tasks/format/check | 10 ++++ templates/go/mise-tasks/install | 12 ++++ templates/go/mise-tasks/lint/_default | 5 ++ templates/go/mise-tasks/lint/fix | 6 ++ templates/go/mise-tasks/publish/_default | 18 ++++++ templates/go/mise-tasks/publish/rc | 36 ++++++++++++ templates/go/mise-tasks/run | 5 ++ templates/go/mise-tasks/test | 5 ++ templates/go/mise-tasks/version/_default | 5 ++ templates/go/mise-tasks/version/next | 4 ++ templates/go/mise.toml | 23 ++++++++ templates/go/src/miselibtemplate/lib.go | 18 ++++++ templates/go/src/miselibtemplate/lib_test.go | 21 +++++++ 27 files changed, 451 insertions(+) create mode 100644 templates/go/.releaserc.json create mode 100644 templates/go/CLAUDE.md.append create mode 100644 templates/go/docs/development-guide.template.md create mode 100644 templates/go/go.mod create mode 100644 templates/go/install.sh.template create mode 100644 templates/go/main.go create mode 100755 templates/go/mise-tasks/build/_default create mode 100755 templates/go/mise-tasks/build/all-platforms create mode 100755 templates/go/mise-tasks/build/prod create mode 100755 templates/go/mise-tasks/clean create mode 100755 templates/go/mise-tasks/docker/build create mode 100755 templates/go/mise-tasks/docker/run create mode 100755 templates/go/mise-tasks/docker/test create mode 100755 templates/go/mise-tasks/format/_default create mode 100755 templates/go/mise-tasks/format/check create mode 100755 templates/go/mise-tasks/install create mode 100755 templates/go/mise-tasks/lint/_default create mode 100755 templates/go/mise-tasks/lint/fix create mode 100755 templates/go/mise-tasks/publish/_default create mode 100755 templates/go/mise-tasks/publish/rc create mode 100755 templates/go/mise-tasks/run create mode 100755 templates/go/mise-tasks/test create mode 100755 templates/go/mise-tasks/version/_default create mode 100755 templates/go/mise-tasks/version/next create mode 100644 templates/go/mise.toml create mode 100644 templates/go/src/miselibtemplate/lib.go create mode 100644 templates/go/src/miselibtemplate/lib_test.go diff --git a/templates/go/.releaserc.json b/templates/go/.releaserc.json new file mode 100644 index 0000000..3417634 --- /dev/null +++ b/templates/go/.releaserc.json @@ -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" + ] +} diff --git a/templates/go/CLAUDE.md.append b/templates/go/CLAUDE.md.append new file mode 100644 index 0000000..223c20d --- /dev/null +++ b/templates/go/CLAUDE.md.append @@ -0,0 +1,56 @@ + + +## 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//` — 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 @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 @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`. diff --git a/templates/go/docs/development-guide.template.md b/templates/go/docs/development-guide.template.md new file mode 100644 index 0000000..478dc7b --- /dev/null +++ b/templates/go/docs/development-guide.template.md @@ -0,0 +1,54 @@ +# {{PROJECT_NAME}} Development Guide + +Generated from {{TEMPLATE_NAME}} v{{TEMPLATE_VERSION}}. + +## Prerequisites + +- [mise](https://mise.jdx.dev/) — manages Go and all other tools +- Go 1.24 is installed automatically by mise via `mise install` +- [gh CLI](https://cli.github.com/) for publishing GitHub releases + +## Getting Started + +```bash +mise install # install Go 1.24 + node + shellcheck + shfmt +mise run build # go build ./... +mise run test # go test ./... +``` + +## Project Structure + +``` +main.go # CLI entry point (package main) +src/{{PROJECT_NAME}}/ # Library package +go.mod # Module manifest +mise.toml # Task runner and tool versions +``` + +## Development Workflow + +1. **Write code** in `src/{{PROJECT_NAME}}/` (library logic) or `main.go` (CLI) +2. **Write tests** as `*_test.go` files with `func TestXxx(t *testing.T)` +3. **Run tests**: `mise run test` +4. **Check format**: `mise run format:check`; fix with `mise run format` + +## Cross-Platform Compilation + +```bash +mise run build:all-platforms +# Outputs dist/release/{{PROJECT_NAME}}-vVERSION-{target}.tar.gz per target +``` + +## Publishing + +1. Ensure `GH_TOKEN` or `GITHUB_TOKEN` is set +2. Push to `main` — CI runs `mise run upversion` (creates the tag + Release) then + `mise run publish` (uploads binaries) +3. Consume as a library with `go get @vX.Y.Z` + +## Adding Dependencies + +```bash +go get github.com/org/dep@v1.2.3 +go mod tidy +``` diff --git a/templates/go/go.mod b/templates/go/go.mod new file mode 100644 index 0000000..03f2876 --- /dev/null +++ b/templates/go/go.mod @@ -0,0 +1,3 @@ +module github.com/cloudvoyant/mise-lib-template + +go 1.24 diff --git a/templates/go/install.sh.template b/templates/go/install.sh.template new file mode 100644 index 0000000..1316d07 --- /dev/null +++ b/templates/go/install.sh.template @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# install.sh — Install {{PROJECT_NAME}} binary +# Usage: curl -fsSL https://raw.githubusercontent.com/{{GITHUB_ORG}}/{{PROJECT_NAME}}/main/install.sh | bash +set -euo pipefail + +PROJECT="{{PROJECT_NAME}}" +GITHUB_ORG="{{GITHUB_ORG}}" +REPO="$GITHUB_ORG/$PROJECT" + +# Detect OS and arch +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +ARCH="$(uname -m)" + +case "$ARCH" in + x86_64) ARCH="x86_64" ;; + aarch64|arm64) ARCH="aarch64" ;; + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; +esac + +case "$OS" in + linux) TARGET="${ARCH}-linux" ;; + darwin) TARGET="${ARCH}-macos" ;; + *) echo "Unsupported OS: $OS" >&2; exit 1 ;; +esac + +# Determine install directory +if [[ $EUID -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" +else + INSTALL_DIR="$HOME/.local/bin" + mkdir -p "$INSTALL_DIR" +fi + +# Get latest release tag +echo "Fetching latest $PROJECT release..." +LATEST_URL="https://api.github.com/repos/$REPO/releases/latest" +TAG=$(curl -fsSL "$LATEST_URL" | grep '"tag_name"' | sed 's/.*"tag_name": *"\(.*\)".*/\1/') + +# Download +ASSET_NAME="${PROJECT}-${TAG}-${TARGET}.tar.gz" +DOWNLOAD_URL="https://github.com/$REPO/releases/download/$TAG/$ASSET_NAME" + +echo "Downloading $ASSET_NAME..." +TMP_DIR=$(mktemp -d) +trap "rm -rf $TMP_DIR" EXIT +curl -fsSL "$DOWNLOAD_URL" | tar -xz -C "$TMP_DIR" + +# Install +cp "$TMP_DIR/$PROJECT" "$INSTALL_DIR/$PROJECT" +chmod +x "$INSTALL_DIR/$PROJECT" + +echo "Installed $PROJECT $TAG to $INSTALL_DIR/$PROJECT" +echo "Make sure $INSTALL_DIR is in your PATH." diff --git a/templates/go/main.go b/templates/go/main.go new file mode 100644 index 0000000..dbddaad --- /dev/null +++ b/templates/go/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "os" + + "github.com/cloudvoyant/mise-lib-template/src/miselibtemplate" +) + +// version is injected at build time via -ldflags "-X main.version=$VERSION". +var version = "dev" + +func main() { + for _, arg := range os.Args[1:] { + if arg == "--version" || arg == "-v" { + fmt.Println(version) + return + } + } + + fmt.Println("Hello from mise-lib-template!") + fmt.Printf("StartsWith(\"hello\", \"he\"): %v\n", miselibtemplate.StartsWith("hello", "he")) +} diff --git a/templates/go/mise-tasks/build/_default b/templates/go/mise-tasks/build/_default new file mode 100755 index 0000000..7fd0d6e --- /dev/null +++ b/templates/go/mise-tasks/build/_default @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +#MISE description="Build Go project (debug)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +# Compile-check every package, then emit a runnable binary (plain `go build ./...` +# discards its output, so the task looked like a no-op). +go build ./... +mkdir -p bin +go build -o "bin/$PROJECT" . +echo "Build successful → bin/$PROJECT" diff --git a/templates/go/mise-tasks/build/all-platforms b/templates/go/mise-tasks/build/all-platforms new file mode 100755 index 0000000..ee4eab4 --- /dev/null +++ b/templates/go/mise-tasks/build/all-platforms @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +#MISE description="Cross-compile for all supported platforms" +#MISE hide=true +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +# "GOOS/GOARCH:target-label" — labels match the zig template so install.sh is shared. +TARGETS=( + "linux/amd64:x86_64-linux" + "linux/arm64:aarch64-linux" + "darwin/amd64:x86_64-macos" + "darwin/arm64:aarch64-macos" + "windows/amd64:x86_64-windows" +) + +mkdir -p dist/release + +for entry in "${TARGETS[@]}"; do + goos="${entry%%/*}" + rest="${entry#*/}" + goarch="${rest%%:*}" + target="${rest#*:}" + log_info "Building for $target ($goos/$goarch)..." + ext="" + [[ "$goos" == "windows" ]] && ext=".exe" + bindir="dist/release/$target" + mkdir -p "$bindir" + GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 \ + go build -ldflags "-X main.version=$VERSION -s -w" \ + -o "$bindir/${PROJECT}${ext}" . + archive="dist/release/${PROJECT}-v${VERSION}-${target}.tar.gz" + tar -czf "$archive" -C "$bindir" "${PROJECT}${ext}" + log_info "Packaged $archive" +done + +log_success "All platform builds complete in dist/release/" diff --git a/templates/go/mise-tasks/build/prod b/templates/go/mise-tasks/build/prod new file mode 100755 index 0000000..d88ed9b --- /dev/null +++ b/templates/go/mise-tasks/build/prod @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Build optimized static release binary" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +mkdir -p dist +CGO_ENABLED=0 go build -ldflags "-X main.version=$VERSION -s -w" -o "dist/$PROJECT" . diff --git a/templates/go/mise-tasks/clean b/templates/go/mise-tasks/clean new file mode 100755 index 0000000..671ce8b --- /dev/null +++ b/templates/go/mise-tasks/clean @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Remove Go build artifacts" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +rm -rf dist/ bin/ diff --git a/templates/go/mise-tasks/docker/build b/templates/go/mise-tasks/docker/build new file mode 100755 index 0000000..883e3ca --- /dev/null +++ b/templates/go/mise-tasks/docker/build @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build Docker image" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker build -t $PROJECT:$VERSION . diff --git a/templates/go/mise-tasks/docker/run b/templates/go/mise-tasks/docker/run new file mode 100755 index 0000000..2a297ca --- /dev/null +++ b/templates/go/mise-tasks/docker/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run Docker container" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker run --rm $PROJECT:$VERSION diff --git a/templates/go/mise-tasks/docker/test b/templates/go/mise-tasks/docker/test new file mode 100755 index 0000000..037aac7 --- /dev/null +++ b/templates/go/mise-tasks/docker/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run tests inside Docker" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker run --rm $PROJECT:$VERSION go test ./... diff --git a/templates/go/mise-tasks/format/_default b/templates/go/mise-tasks/format/_default new file mode 100755 index 0000000..5583def --- /dev/null +++ b/templates/go/mise-tasks/format/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Format Go source files in-place" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +gofmt -w . diff --git a/templates/go/mise-tasks/format/check b/templates/go/mise-tasks/format/check new file mode 100755 index 0000000..815d2b9 --- /dev/null +++ b/templates/go/mise-tasks/format/check @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +#MISE description="Check Go formatting without modifying files" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +unformatted=$(gofmt -l .) +if [ -n "$unformatted" ]; then + echo "Unformatted files:" >&2 + echo "$unformatted" >&2 + exit 1 +fi diff --git a/templates/go/mise-tasks/install b/templates/go/mise-tasks/install new file mode 100755 index 0000000..c956476 --- /dev/null +++ b/templates/go/mise-tasks/install @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +#MISE description="Install npm tools and download Go module dependencies" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +npm install --no-save --prefix "${MISE_PROJECT_ROOT:-$PWD}" \ + semantic-release \ + @semantic-release/changelog \ + @semantic-release/exec \ + @semantic-release/git \ + @semantic-release/github \ + conventional-changelog-conventionalcommits +go mod download 2>/dev/null || true diff --git a/templates/go/mise-tasks/lint/_default b/templates/go/mise-tasks/lint/_default new file mode 100755 index 0000000..4418dee --- /dev/null +++ b/templates/go/mise-tasks/lint/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Vet Go source (static analysis)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +go vet ./... diff --git a/templates/go/mise-tasks/lint/fix b/templates/go/mise-tasks/lint/fix new file mode 100755 index 0000000..b3987e5 --- /dev/null +++ b/templates/go/mise-tasks/lint/fix @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Auto-format then vet Go source" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +gofmt -w . +go vet ./... diff --git a/templates/go/mise-tasks/publish/_default b/templates/go/mise-tasks/publish/_default new file mode 100755 index 0000000..ce619cd --- /dev/null +++ b/templates/go/mise-tasks/publish/_default @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +#MISE description="Upload cross-platform binaries to existing GitHub release" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +log_info "Building release binaries for all platforms..." +mise run build:all-platforms + +# Collect built assets +ASSETS=() +while IFS= read -r bin; do ASSETS+=("$bin"); done \ + < <(find dist/release -maxdepth 1 -type f -name "$PROJECT-*" 2>/dev/null) + +log_info "Uploading binaries to GitHub release v$VERSION..." +gh release upload "v$VERSION" "${ASSETS[@]}" --clobber + +log_success "Published $PROJECT v$VERSION to GitHub Releases" diff --git a/templates/go/mise-tasks/publish/rc b/templates/go/mise-tasks/publish/rc new file mode 100755 index 0000000..c277118 --- /dev/null +++ b/templates/go/mise-tasks/publish/rc @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +#MISE description="Publish RC binaries to GitHub pre-release" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +SHORT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "dev") +TIMESTAMP=$(date -u +%Y%m%d%H%M) +# Human-readable UTC timestamp (yyyymmddhhmm) prefixes the sha so each commit gets a +# unique, time-sortable RC tag. semver: v2.2.0-rc.202607091142.abc1234 +RC_TAG="v${VERSION}-rc.${TIMESTAMP}.${SHORT_SHA}" +# Re-runs reuse the SHA/minute but bump GITHUB_RUN_ATTEMPT; fold it in so a re-run mints a +# fresh tag instead of colliding with the pre-release already published. +[ "${GITHUB_RUN_ATTEMPT:-1}" != "1" ] && RC_TAG="${RC_TAG}.${GITHUB_RUN_ATTEMPT}" + +log_info "Cleaning up previous RC pre-releases for v${VERSION}-rc.*..." +while IFS= read -r tag; do + [[ "$tag" == "$RC_TAG" ]] && continue + gh release delete "$tag" --yes --cleanup-tag 2>/dev/null || true +done < <(gh release list --json tagName,isPrerelease \ + --jq '.[] | select(.isPrerelease) | .tagName' \ + | grep "^v${VERSION}-rc\." || true) + +log_info "Creating GitHub pre-release $RC_TAG..." +gh release create "$RC_TAG" \ + --title "RC: $RC_TAG" \ + --notes "Pre-release build from commit $SHORT_SHA. Not for production use." \ + --prerelease + +log_info "Setting RC version ${RC_TAG#v}..." +echo "${RC_TAG#v}" > version.txt + +log_info "Publishing $PROJECT $RC_TAG to GitHub pre-release..." +mise run publish + +log_success "Published $PROJECT $RC_TAG to GitHub pre-release" diff --git a/templates/go/mise-tasks/run b/templates/go/mise-tasks/run new file mode 100755 index 0000000..01d98ae --- /dev/null +++ b/templates/go/mise-tasks/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build and run the project" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +go run . diff --git a/templates/go/mise-tasks/test b/templates/go/mise-tasks/test new file mode 100755 index 0000000..a68c023 --- /dev/null +++ b/templates/go/mise-tasks/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run Go test suite" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +go test ./... diff --git a/templates/go/mise-tasks/version/_default b/templates/go/mise-tasks/version/_default new file mode 100755 index 0000000..db0a60c --- /dev/null +++ b/templates/go/mise-tasks/version/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Print current version" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cat version.txt diff --git a/templates/go/mise-tasks/version/next b/templates/go/mise-tasks/version/next new file mode 100755 index 0000000..aa30c62 --- /dev/null +++ b/templates/go/mise-tasks/version/next @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#MISE description="Preview next semantic-release version" +set -euo pipefail +mise run upversion -- --dry-run diff --git a/templates/go/mise.toml b/templates/go/mise.toml new file mode 100644 index 0000000..5f2591e --- /dev/null +++ b/templates/go/mise.toml @@ -0,0 +1,23 @@ +# ============================================================================= +# mise.toml - go template +# Overlays the base mise.toml during scaffold. Tasks are file tasks under +# mise-tasks/ (this template overrides only the tasks that differ from base). +# ============================================================================= + +[env] +_.path = ['{{config_root}}/node_modules/.bin'] +PROJECT = "mise-lib-template" +VERSION = "{{exec(command='cat version.txt 2>/dev/null || echo 0.1.0')}}" + +[tools] +go = "1.24" +node = "lts" +bats = "latest" +shellcheck = "latest" +shfmt = "latest" + +[task_config] +includes = ["mise-tasks"] + +[settings] +experimental = true diff --git a/templates/go/src/miselibtemplate/lib.go b/templates/go/src/miselibtemplate/lib.go new file mode 100644 index 0000000..4df8d9b --- /dev/null +++ b/templates/go/src/miselibtemplate/lib.go @@ -0,0 +1,18 @@ +// Package miselibtemplate is the library surface generated from mise-go-template. +package miselibtemplate + +// StartsWith reports whether haystack begins with needle. +func StartsWith(haystack, needle string) bool { + if len(needle) > len(haystack) { + return false + } + return haystack[:len(needle)] == needle +} + +// EndsWith reports whether haystack ends with needle. +func EndsWith(haystack, needle string) bool { + if len(needle) > len(haystack) { + return false + } + return haystack[len(haystack)-len(needle):] == needle +} diff --git a/templates/go/src/miselibtemplate/lib_test.go b/templates/go/src/miselibtemplate/lib_test.go new file mode 100644 index 0000000..725dcd4 --- /dev/null +++ b/templates/go/src/miselibtemplate/lib_test.go @@ -0,0 +1,21 @@ +package miselibtemplate + +import "testing" + +func TestStartsWith(t *testing.T) { + if !StartsWith("hello world", "hello") { + t.Error(`StartsWith("hello world", "hello") = false, want true`) + } + if StartsWith("hello", "world") { + t.Error(`StartsWith("hello", "world") = true, want false`) + } +} + +func TestEndsWith(t *testing.T) { + if !EndsWith("hello world", "world") { + t.Error(`EndsWith("hello world", "world") = false, want true`) + } + if EndsWith("hello", "world") { + t.Error(`EndsWith("hello", "world") = true, want false`) + } +} From 8552e6e96aac6a3d2e4db7a3a82fce3a04a96427 Mon Sep 17 00:00:00 2001 From: Siddharth Kapoor Date: Mon, 13 Jul 2026 10:27:38 -0400 Subject: [PATCH 2/4] feat: add rust language template Rust library/binary template implementing the full task contract with cargo build/test, rustfmt/clippy, starter source, a Dockerfile, and CLAUDE.md conventions. Distributes via git-tag install and cross-platform release binaries. --- templates/rust/.releaserc.json | 16 +++++ templates/rust/CLAUDE.md.append | 59 +++++++++++++++++ templates/rust/Cargo.lock | 7 ++ templates/rust/Cargo.toml | 17 +++++ .../rust/docs/development-guide.template.md | 66 +++++++++++++++++++ templates/rust/install.sh.template | 62 +++++++++++++++++ templates/rust/mise-tasks/build/_default | 8 +++ templates/rust/mise-tasks/build/all-platforms | 37 +++++++++++ templates/rust/mise-tasks/build/prod | 5 ++ templates/rust/mise-tasks/clean | 6 ++ templates/rust/mise-tasks/docker/build | 5 ++ templates/rust/mise-tasks/docker/run | 5 ++ templates/rust/mise-tasks/docker/test | 5 ++ templates/rust/mise-tasks/format/_default | 5 ++ templates/rust/mise-tasks/format/check | 5 ++ templates/rust/mise-tasks/install | 25 +++++++ templates/rust/mise-tasks/lint/_default | 5 ++ templates/rust/mise-tasks/lint/fix | 5 ++ templates/rust/mise-tasks/publish/_default | 18 +++++ templates/rust/mise-tasks/publish/rc | 37 +++++++++++ templates/rust/mise-tasks/run | 5 ++ templates/rust/mise-tasks/test | 5 ++ templates/rust/mise-tasks/version/_default | 5 ++ templates/rust/mise-tasks/version/next | 4 ++ templates/rust/mise.toml | 26 ++++++++ templates/rust/src/lib.rs | 20 ++++++ templates/rust/src/main.rs | 16 +++++ 27 files changed, 479 insertions(+) create mode 100644 templates/rust/.releaserc.json create mode 100644 templates/rust/CLAUDE.md.append create mode 100644 templates/rust/Cargo.lock create mode 100644 templates/rust/Cargo.toml create mode 100644 templates/rust/docs/development-guide.template.md create mode 100644 templates/rust/install.sh.template create mode 100755 templates/rust/mise-tasks/build/_default create mode 100755 templates/rust/mise-tasks/build/all-platforms create mode 100755 templates/rust/mise-tasks/build/prod create mode 100755 templates/rust/mise-tasks/clean create mode 100755 templates/rust/mise-tasks/docker/build create mode 100755 templates/rust/mise-tasks/docker/run create mode 100755 templates/rust/mise-tasks/docker/test create mode 100755 templates/rust/mise-tasks/format/_default create mode 100755 templates/rust/mise-tasks/format/check create mode 100755 templates/rust/mise-tasks/install create mode 100755 templates/rust/mise-tasks/lint/_default create mode 100755 templates/rust/mise-tasks/lint/fix create mode 100755 templates/rust/mise-tasks/publish/_default create mode 100755 templates/rust/mise-tasks/publish/rc create mode 100755 templates/rust/mise-tasks/run create mode 100755 templates/rust/mise-tasks/test create mode 100755 templates/rust/mise-tasks/version/_default create mode 100755 templates/rust/mise-tasks/version/next create mode 100644 templates/rust/mise.toml create mode 100644 templates/rust/src/lib.rs create mode 100644 templates/rust/src/main.rs diff --git a/templates/rust/.releaserc.json b/templates/rust/.releaserc.json new file mode 100644 index 0000000..ede8170 --- /dev/null +++ b/templates/rust/.releaserc.json @@ -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 && sed -i 's/^version = \".*\"/version = \"${nextRelease.version}\"/' Cargo.toml" + }], + ["@semantic-release/git", { + "assets": ["CHANGELOG.md", "version.txt", "Cargo.toml", "Cargo.lock"], + "message": "chore(release): ${nextRelease.version} [skip ci]" + }], + "@semantic-release/github" + ] +} diff --git a/templates/rust/CLAUDE.md.append b/templates/rust/CLAUDE.md.append new file mode 100644 index 0000000..b7615f7 --- /dev/null +++ b/templates/rust/CLAUDE.md.append @@ -0,0 +1,59 @@ + + +## Rust Development (rust template) + +**Build system**: `cargo` — do not invoke `rustc` directly or hand-roll build scripts. + +**Common commands:** +- `mise run build` — debug build (`cargo build`) +- `mise run build:prod` — release build (`cargo build --release`) +- `mise run test` — run tests (`cargo test`) +- `mise run run` — build and run (`cargo run`) +- `mise run lint` — `cargo clippy --all-targets --all-features -- -D warnings` +- `mise run lint:fix` — `cargo clippy --fix --allow-dirty --allow-staged` +- `mise run format` / `mise run format:check` — `cargo fmt` / `cargo fmt --check` +- `mise run publish` — cross-compile Linux binaries, upload to the existing GitHub release + +**Source layout:** +- `src/lib.rs` — library crate (consumed via `{ git, tag }`) +- `src/main.rs` — CLI entry point (depends on the lib crate) +- `Cargo.toml` — package manifest; keep `[package]` first (the version sed depends on it) +- `Cargo.lock` — committed for reproducible git-tag consumers; regenerated on `cargo build`. + The base `.gitignore`'s broad `*.lock` is overridden by a `!Cargo.lock` negation so the + lock is tracked; do not remove that negation. + +**Writing tests:** +- Unit tests live in `#[cfg(test)] mod tests { ... }` inside the source file +- Run with `mise run test` (wraps `cargo test`) +- `cargo test -- --nocapture` for stdout during tests + +**Consuming this project (no crates.io required):** +- Library dependency (pinned, reproducible — Cargo locks the commit in `Cargo.lock`): + ```toml + [dependencies] + = { git = "https://github.com//", tag = "vX.Y.Z" } + # branch = "main" / rev = "" also supported + ``` +- Installable binary (no registry): + ```bash + cargo install --git https://github.com// --tag vX.Y.Z + # --branch / --rev / --bin also supported + ``` +- Prebuilt binary (no Rust toolchain, Linux only in v1): `install.sh` downloads the + cross-compiled tarball from the GitHub Release. + +**Cross-compilation (v1 = Linux only):** +- `mise run build:all-platforms` uses `cross` (Docker) to build the four Linux triples: + `x86_64`/`aarch64` × `unknown-linux-gnu`/`unknown-linux-musl` +- `mise run install` adds clippy, rustfmt, and the four cross targets via `rustup`, then + installs `cross` (requires Docker at build time). +- macOS / Windows prebuilt binaries are a known gap — those users run `cargo install --git` + (needs a local toolchain). Closing the gap is an opt-in: run `dist init` (cargo-dist) in + this project to take over the release matrix, dropping semantic-release. +- `cross` requires Docker on the runner; `cargo install cross` is done by `mise run install`. + +**Publishing:** +- `mise run upversion` creates the GitHub Release (via `@semantic-release/github`) and syncs + the version into `Cargo.toml`/`version.txt` +- `mise run publish` cross-compiles and uploads binaries to the existing release +- Requires `GH_TOKEN` or `GITHUB_TOKEN` in environment diff --git a/templates/rust/Cargo.lock b/templates/rust/Cargo.lock new file mode 100644 index 0000000..71cc4ee --- /dev/null +++ b/templates/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "mise_lib_template" +version = "0.0.0" diff --git a/templates/rust/Cargo.toml b/templates/rust/Cargo.toml new file mode 100644 index 0000000..bfadcb1 --- /dev/null +++ b/templates/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mise_lib_template" +version = "0.0.0" +edition = "2021" +description = "A library and CLI scaffolded from mise-rust-template." +license = "MIT" + +# A single crate exposing both a library (for `{ git, tag }` consumers) and a +# binary (for `cargo install --git`). Keeping `[package]` first makes the +# version sed in .releaserc.json / templates:publish unambiguous. +[lib] +name = "mise_lib_template" +path = "src/lib.rs" + +[[bin]] +name = "mise_lib_template" +path = "src/main.rs" diff --git a/templates/rust/docs/development-guide.template.md b/templates/rust/docs/development-guide.template.md new file mode 100644 index 0000000..81451d1 --- /dev/null +++ b/templates/rust/docs/development-guide.template.md @@ -0,0 +1,66 @@ +# {{PROJECT_NAME}} Development Guide + +Generated from {{TEMPLATE_NAME}} v{{TEMPLATE_VERSION}}. + +## Prerequisites + +- [mise](https://mise.jdx.dev/) — manages Rust and all other tools +- Rust 1.88 is installed by `mise install`; clippy, rustfmt, and cross targets are added by `mise run install` +- [gh CLI](https://cli.github.com/) for publishing GitHub releases +- Docker — required by `cross` for `mise run build:all-platforms` + +## Getting Started + +```bash +mise install # Rust 1.88 + node + shell tools (from mise.toml) +mise run install # clippy + rustfmt + cross targets + cross binary + npm sr plugins +mise run build # debug build +mise run test # run tests +``` + +## Project Structure + +``` +src/lib.rs # Library crate (consumed via { git, tag }) +src/main.rs # CLI entry point (depends on the lib crate) +Cargo.toml # Package manifest ([package] first) +Cargo.lock # Committed lock for reproducible git-tag consumers +mise.toml # Task runner and tool versions +``` + +## Development Workflow + +1. **Write code** in `src/lib.rs` (library logic) or `src/main.rs` (CLI) +2. **Write tests** in `#[cfg(test)] mod tests { ... }` blocks +3. **Run tests**: `mise run test` +4. **Check format**: `mise run format:check`; fix with `mise run format` +5. **Lint**: `mise run lint` (clippy denies warnings) + +## Consuming This Project (no crates.io) + +```toml +# library dependency (Cargo pins the resolved commit in Cargo.lock) +[dependencies] +{{PROJECT_NAME}} = { git = "https://github.com//{{PROJECT_NAME}}", tag = "vX.Y.Z" } +``` + +```bash +# installable binary +cargo install --git https://github.com//{{PROJECT_NAME}} --tag vX.Y.Z +``` + +## Cross-Platform Compilation (Linux only in v1) + +```bash +mise run build:all-platforms +# cross build --release for: x86_64/aarch64 × unknown-linux-{gnu,musl} +# Outputs target/dist/{{PROJECT_NAME}}-vVERSION-.tar.gz +``` + +macOS/Windows prebuilt binaries are a known gap — those users run `cargo install --git`. + +## Publishing + +1. Ensure `GH_TOKEN` or `GITHUB_TOKEN` is set (and Docker is available for `cross`) +2. Push to `main` — CI runs `mise run upversion` then `mise run publish` +3. `upversion` creates the release + tag; `publish` only uploads binaries to it diff --git a/templates/rust/install.sh.template b/templates/rust/install.sh.template new file mode 100644 index 0000000..1435969 --- /dev/null +++ b/templates/rust/install.sh.template @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# install.sh — Install {{PROJECT_NAME}} binary (prebuilt Linux tarball) +# Usage: curl -fsSL https://raw.githubusercontent.com/{{GITHUB_ORG}}/{{PROJECT_NAME}}/main/install.sh | bash +# +# Linux only in v1. On macOS/Windows (or to build from source), use Cargo directly: +# cargo install --git https://github.com/{{GITHUB_ORG}}/{{PROJECT_NAME}} --tag +# +# Set LIBC=musl to fetch the statically-linked musl build instead of the default gnu build. +set -euo pipefail + +PROJECT="{{PROJECT_NAME}}" +GITHUB_ORG="{{GITHUB_ORG}}" +REPO="$GITHUB_ORG/$PROJECT" +LIBC="${LIBC:-gnu}" + +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +ARCH="$(uname -m)" + +case "$ARCH" in + x86_64) ARCH="x86_64" ;; + aarch64|arm64) ARCH="aarch64" ;; + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; +esac + +if [[ "$OS" != "linux" ]]; then + echo "Prebuilt binaries are Linux-only. On $OS, install with Cargo:" >&2 + echo " cargo install --git https://github.com/$REPO --tag " >&2 + exit 1 +fi + +case "$LIBC" in + gnu) TARGET="${ARCH}-unknown-linux-gnu" ;; + musl) TARGET="${ARCH}-unknown-linux-musl" ;; + *) echo "Unsupported LIBC: $LIBC (use gnu or musl)" >&2; exit 1 ;; +esac + +if [[ $EUID -eq 0 ]]; then + INSTALL_DIR="/usr/local/bin" +else + INSTALL_DIR="$HOME/.local/bin" + mkdir -p "$INSTALL_DIR" +fi + +echo "Fetching latest $PROJECT release..." +LATEST_URL="https://api.github.com/repos/$REPO/releases/latest" +TAG=$(curl -fsSL "$LATEST_URL" | grep '"tag_name"' | sed 's/.*"tag_name": *"\(.*\)".*/\1/') + +ASSET_NAME="${PROJECT}-${TAG}-${TARGET}.tar.gz" +DOWNLOAD_URL="https://github.com/$REPO/releases/download/$TAG/$ASSET_NAME" + +echo "Downloading $ASSET_NAME..." +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT +curl -fsSL "$DOWNLOAD_URL" | tar -xz -C "$TMP_DIR" + +# Cargo emits the binary under the crate/[[bin]] name (snake_case); the archived +# member matches that. Install it under the kebab command name ($PROJECT). +cp "$TMP_DIR/${PROJECT//-/_}" "$INSTALL_DIR/$PROJECT" +chmod +x "$INSTALL_DIR/$PROJECT" + +echo "Installed $PROJECT $TAG to $INSTALL_DIR/$PROJECT" +echo "Make sure $INSTALL_DIR is in your PATH." diff --git a/templates/rust/mise-tasks/build/_default b/templates/rust/mise-tasks/build/_default new file mode 100755 index 0000000..8084f8c --- /dev/null +++ b/templates/rust/mise-tasks/build/_default @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +#MISE description="Build Rust project (debug)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +# First build after scaffold rename regenerates Cargo.lock's root package name (self-heal). +cargo build +# Cargo emits the binary under the crate/[[bin]] name (snake_case), not $PROJECT (kebab). +echo "Build successful → target/debug/${PROJECT//-/_}" diff --git a/templates/rust/mise-tasks/build/all-platforms b/templates/rust/mise-tasks/build/all-platforms new file mode 100755 index 0000000..231e828 --- /dev/null +++ b/templates/rust/mise-tasks/build/all-platforms @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +#MISE description="Cross-compile Linux binaries for all supported targets" +#MISE hide=true +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +# Linux-only in v1: `cross` (Docker) cannot legally cross-compile to macOS or +# Windows-MSVC from a Linux host (no bundled SDK). macOS/Windows users install via +# `cargo install --git`. `mise run install` adds each target's std lib via `rustup target +# add`; `cross` supplies the linker/toolchain via Docker for the actual cross-build. +TARGETS=( + "x86_64-unknown-linux-gnu" + "aarch64-unknown-linux-gnu" + "x86_64-unknown-linux-musl" + "aarch64-unknown-linux-musl" +) + +# Native build first to settle state (and self-heal Cargo.lock) after scaffold rename. +mise run build >/dev/null + +# Publish flows invoke this task directly (not via `install`), so ensure `cross` — the +# Docker-backed cross-compiler that provides each target's linker/toolchain — is present. +command -v cross >/dev/null 2>&1 || cargo install cross + +mkdir -p target/dist + +for target in "${TARGETS[@]}"; do + log_info "Building for $target..." + cross build --release --target "$target" + archive="target/dist/${PROJECT}-v${VERSION}-${target}.tar.gz" + # Cargo emits the binary under the [[bin]] name (snake_case), not $PROJECT (kebab). + tar -czf "$archive" -C "target/${target}/release" "${PROJECT//-/_}" + log_info "Packaged $archive" +done + +log_success "All platform builds complete in target/dist/" diff --git a/templates/rust/mise-tasks/build/prod b/templates/rust/mise-tasks/build/prod new file mode 100755 index 0000000..281c2ed --- /dev/null +++ b/templates/rust/mise-tasks/build/prod @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build optimized release binary" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo build --release diff --git a/templates/rust/mise-tasks/clean b/templates/rust/mise-tasks/clean new file mode 100755 index 0000000..42e525e --- /dev/null +++ b/templates/rust/mise-tasks/clean @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Remove Rust build artifacts" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo clean +rm -rf target/dist diff --git a/templates/rust/mise-tasks/docker/build b/templates/rust/mise-tasks/docker/build new file mode 100755 index 0000000..883e3ca --- /dev/null +++ b/templates/rust/mise-tasks/docker/build @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build Docker image" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker build -t $PROJECT:$VERSION . diff --git a/templates/rust/mise-tasks/docker/run b/templates/rust/mise-tasks/docker/run new file mode 100755 index 0000000..2a297ca --- /dev/null +++ b/templates/rust/mise-tasks/docker/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run Docker container" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker run --rm $PROJECT:$VERSION diff --git a/templates/rust/mise-tasks/docker/test b/templates/rust/mise-tasks/docker/test new file mode 100755 index 0000000..02cef66 --- /dev/null +++ b/templates/rust/mise-tasks/docker/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run tests inside Docker" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker run --rm $PROJECT:$VERSION cargo test diff --git a/templates/rust/mise-tasks/format/_default b/templates/rust/mise-tasks/format/_default new file mode 100755 index 0000000..b33dafa --- /dev/null +++ b/templates/rust/mise-tasks/format/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Format Rust source files in-place" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo fmt diff --git a/templates/rust/mise-tasks/format/check b/templates/rust/mise-tasks/format/check new file mode 100755 index 0000000..36d6ac1 --- /dev/null +++ b/templates/rust/mise-tasks/format/check @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Check Rust formatting without modifying files" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo fmt --check diff --git a/templates/rust/mise-tasks/install b/templates/rust/mise-tasks/install new file mode 100755 index 0000000..e4881bb --- /dev/null +++ b/templates/rust/mise-tasks/install @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +#MISE description="Install npm tools, cross, and fetch Rust dependencies" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" +npm install --no-save --prefix "${MISE_PROJECT_ROOT:-$PWD}" \ + semantic-release \ + @semantic-release/changelog \ + @semantic-release/exec \ + @semantic-release/git \ + @semantic-release/github \ + conventional-changelog-conventionalcommits +# clippy and rustfmt are not included in the default minimal toolchain. Warn (not +# fail) so a re-run in an offline/CI-cached env stays green, but surface real errors. +rustup component add clippy rustfmt || log_warn "rustup component add clippy rustfmt failed" +# Cross-compile targets (Linux only in v1 — `cross` builds these inside Docker). +rustup target add \ + x86_64-unknown-linux-gnu \ + aarch64-unknown-linux-gnu \ + x86_64-unknown-linux-musl \ + aarch64-unknown-linux-musl \ + || log_warn "rustup target add (cross targets) failed" +# `cross` runs each cross-compile inside Docker; installed idempotently. +cargo install cross || log_warn "cargo install cross failed (Docker required at build time)" +cargo fetch || log_warn "cargo fetch failed" diff --git a/templates/rust/mise-tasks/lint/_default b/templates/rust/mise-tasks/lint/_default new file mode 100755 index 0000000..767aa66 --- /dev/null +++ b/templates/rust/mise-tasks/lint/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Lint with clippy (deny warnings)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo clippy --all-targets --all-features -- -D warnings diff --git a/templates/rust/mise-tasks/lint/fix b/templates/rust/mise-tasks/lint/fix new file mode 100755 index 0000000..deaa700 --- /dev/null +++ b/templates/rust/mise-tasks/lint/fix @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Auto-fix clippy findings" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo clippy --fix --allow-dirty --allow-staged diff --git a/templates/rust/mise-tasks/publish/_default b/templates/rust/mise-tasks/publish/_default new file mode 100755 index 0000000..5fab9c8 --- /dev/null +++ b/templates/rust/mise-tasks/publish/_default @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +#MISE description="Upload cross-platform binaries to the existing GitHub release" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +log_info "Building release binaries for all platforms..." +mise run build:all-platforms + +# Collect built assets +ASSETS=() +while IFS= read -r bin; do ASSETS+=("$bin"); done \ + < <(find target/dist -maxdepth 1 -type f -name "$PROJECT-*" 2>/dev/null) + +log_info "Uploading binaries to GitHub release v$VERSION..." +gh release upload "v$VERSION" "${ASSETS[@]}" --clobber + +log_success "Published $PROJECT v$VERSION to GitHub Releases" diff --git a/templates/rust/mise-tasks/publish/rc b/templates/rust/mise-tasks/publish/rc new file mode 100755 index 0000000..2e04b34 --- /dev/null +++ b/templates/rust/mise-tasks/publish/rc @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +#MISE description="Publish RC binaries to GitHub pre-release" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +SHORT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "dev") +TIMESTAMP=$(date -u +%Y%m%d%H%M) +# Human-readable UTC timestamp (yyyymmddhhmm) prefixes the sha so each commit gets a +# unique, time-sortable RC tag. semver: v2.2.0-rc.202607091142.abc1234 +RC_TAG="v${VERSION}-rc.${TIMESTAMP}.${SHORT_SHA}" +# Re-runs reuse the SHA/minute but bump GITHUB_RUN_ATTEMPT; fold it in so a re-run mints a +# fresh tag instead of colliding with the pre-release already published. +[ "${GITHUB_RUN_ATTEMPT:-1}" != "1" ] && RC_TAG="${RC_TAG}.${GITHUB_RUN_ATTEMPT}" + +log_info "Cleaning up previous RC pre-releases for v${VERSION}-rc.*..." +while IFS= read -r tag; do + [[ "$tag" == "$RC_TAG" ]] && continue + gh release delete "$tag" --yes --cleanup-tag 2>/dev/null || true +done < <(gh release list --json tagName,isPrerelease \ + --jq '.[] | select(.isPrerelease) | .tagName' \ + | grep "^v${VERSION}-rc\." || true) + +log_info "Creating GitHub pre-release $RC_TAG..." +gh release create "$RC_TAG" \ + --title "RC: $RC_TAG" \ + --notes "Pre-release build from commit $SHORT_SHA. Not for production use." \ + --prerelease + +log_info "Setting RC version ${RC_TAG#v}..." +echo "${RC_TAG#v}" > version.txt +sed_inplace "s/^version = \".*\"/version = \"${RC_TAG#v}\"/" Cargo.toml + +log_info "Publishing $PROJECT $RC_TAG to GitHub pre-release..." +mise run publish + +log_success "Published $PROJECT $RC_TAG to GitHub pre-release" diff --git a/templates/rust/mise-tasks/run b/templates/rust/mise-tasks/run new file mode 100755 index 0000000..f33e49f --- /dev/null +++ b/templates/rust/mise-tasks/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build and run the project" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo run diff --git a/templates/rust/mise-tasks/test b/templates/rust/mise-tasks/test new file mode 100755 index 0000000..16d90e4 --- /dev/null +++ b/templates/rust/mise-tasks/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run Rust test suite" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cargo test diff --git a/templates/rust/mise-tasks/version/_default b/templates/rust/mise-tasks/version/_default new file mode 100755 index 0000000..db0a60c --- /dev/null +++ b/templates/rust/mise-tasks/version/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Print current version" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cat version.txt diff --git a/templates/rust/mise-tasks/version/next b/templates/rust/mise-tasks/version/next new file mode 100755 index 0000000..aa30c62 --- /dev/null +++ b/templates/rust/mise-tasks/version/next @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#MISE description="Preview next semantic-release version" +set -euo pipefail +mise run upversion -- --dry-run diff --git a/templates/rust/mise.toml b/templates/rust/mise.toml new file mode 100644 index 0000000..2655397 --- /dev/null +++ b/templates/rust/mise.toml @@ -0,0 +1,26 @@ +# ============================================================================= +# mise.toml - rust template +# Overlays the base mise.toml during scaffold. Tasks are file tasks under +# mise-tasks/ (this template overrides only the tasks that differ from base). +# ============================================================================= + +[env] +_.path = ['{{config_root}}/node_modules/.bin'] +PROJECT = "mise-lib-template" +VERSION = "{{exec(command='cat version.txt 2>/dev/null || echo 0.1.0')}}" + +[tools] +# rustfmt+clippy declared here (comma-string form — mise rejects the array form) so +# mise auto-installs them; `format:check`/`lint` then work without the install task, +# which the contract test runs tasks without. +rust = { version = "1.88", components = "rustfmt,clippy" } +node = "lts" +bats = "latest" +shellcheck = "latest" +shfmt = "latest" + +[task_config] +includes = ["mise-tasks"] + +[settings] +experimental = true diff --git a/templates/rust/src/lib.rs b/templates/rust/src/lib.rs new file mode 100644 index 0000000..10d2ee9 --- /dev/null +++ b/templates/rust/src/lib.rs @@ -0,0 +1,20 @@ +//! Minimal library crate scaffolded from mise-rust-template. +//! +//! Depend on this crate directly from git without crates.io: +//! `mise_lib_template = { git = "https://github.com//", tag = "vX.Y.Z" }` + +/// Returns `true` when `haystack` begins with `needle`. +pub fn starts_with(haystack: &str, needle: &str) -> bool { + haystack.as_bytes().starts_with(needle.as_bytes()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn starts_with_matches_prefix() { + assert!(starts_with("hello world", "hello")); + assert!(!starts_with("hello", "world")); + } +} diff --git a/templates/rust/src/main.rs b/templates/rust/src/main.rs new file mode 100644 index 0000000..554f9c5 --- /dev/null +++ b/templates/rust/src/main.rs @@ -0,0 +1,16 @@ +use mise_lib_template::starts_with; + +fn main() { + // --version resolves to CARGO_PKG_VERSION, which Cargo fills from + // [package] version in Cargo.toml (kept in sync with version.txt). + if std::env::args().any(|a| a == "--version" || a == "-V") { + println!("mise_lib_template {}", env!("CARGO_PKG_VERSION")); + return; + } + + println!("Hello from mise-lib-template!"); + println!( + "starts_with(\"hello\", \"he\"): {}", + starts_with("hello", "he"), + ); +} From d7b3805ef9d74964d3800a4fa0329dd58190a824 Mon Sep 17 00:00:00 2001 From: Siddharth Kapoor Date: Mon, 13 Jul 2026 10:27:38 -0400 Subject: [PATCH 3/4] feat: add odin language template Odin template implementing the full task contract via the github:odin-lang/Odin backend, with starter source, a Dockerfile, and CLAUDE.md conventions. Distributes source (git tag / GitHub source release) rather than binaries, matching Odin's no-package-manager convention. --- templates/odin/.releaserc.json | 16 +++++ templates/odin/CLAUDE.md.append | 61 +++++++++++++++++++ .../odin/docs/development-guide.template.md | 58 ++++++++++++++++++ templates/odin/mise-tasks/build/_default | 7 +++ templates/odin/mise-tasks/build/prod | 7 +++ templates/odin/mise-tasks/clean | 5 ++ templates/odin/mise-tasks/docker/build | 5 ++ templates/odin/mise-tasks/docker/run | 5 ++ templates/odin/mise-tasks/docker/test | 5 ++ templates/odin/mise-tasks/format/_default | 8 +++ templates/odin/mise-tasks/format/check | 7 +++ templates/odin/mise-tasks/install | 20 ++++++ templates/odin/mise-tasks/lint/_default | 5 ++ templates/odin/mise-tasks/lint/fix | 6 ++ templates/odin/mise-tasks/publish/_default | 17 ++++++ templates/odin/mise-tasks/publish/rc | 32 ++++++++++ templates/odin/mise-tasks/run | 5 ++ templates/odin/mise-tasks/test | 5 ++ templates/odin/mise-tasks/version/_default | 5 ++ templates/odin/mise-tasks/version/next | 4 ++ templates/odin/mise.toml | 26 ++++++++ templates/odin/src/lib.odin | 27 ++++++++ templates/odin/src/main.odin | 13 ++++ 23 files changed, 349 insertions(+) create mode 100644 templates/odin/.releaserc.json create mode 100644 templates/odin/CLAUDE.md.append create mode 100644 templates/odin/docs/development-guide.template.md create mode 100755 templates/odin/mise-tasks/build/_default create mode 100755 templates/odin/mise-tasks/build/prod create mode 100755 templates/odin/mise-tasks/clean create mode 100755 templates/odin/mise-tasks/docker/build create mode 100755 templates/odin/mise-tasks/docker/run create mode 100755 templates/odin/mise-tasks/docker/test create mode 100755 templates/odin/mise-tasks/format/_default create mode 100755 templates/odin/mise-tasks/format/check create mode 100755 templates/odin/mise-tasks/install create mode 100755 templates/odin/mise-tasks/lint/_default create mode 100755 templates/odin/mise-tasks/lint/fix create mode 100755 templates/odin/mise-tasks/publish/_default create mode 100755 templates/odin/mise-tasks/publish/rc create mode 100755 templates/odin/mise-tasks/run create mode 100755 templates/odin/mise-tasks/test create mode 100755 templates/odin/mise-tasks/version/_default create mode 100755 templates/odin/mise-tasks/version/next create mode 100644 templates/odin/mise.toml create mode 100644 templates/odin/src/lib.odin create mode 100644 templates/odin/src/main.odin diff --git a/templates/odin/.releaserc.json b/templates/odin/.releaserc.json new file mode 100644 index 0000000..3417634 --- /dev/null +++ b/templates/odin/.releaserc.json @@ -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" + ] +} diff --git a/templates/odin/CLAUDE.md.append b/templates/odin/CLAUDE.md.append new file mode 100644 index 0000000..dfbe8b4 --- /dev/null +++ b/templates/odin/CLAUDE.md.append @@ -0,0 +1,61 @@ + + +## Odin Development (odin template) + +**Compiler**: Odin (LLVM backend). Installed via the `github:odin-lang/Odin` mise +backend — the registry short-name `odin` is broken (dead plugin repos). Odin release +tags are calendar-based (`dev-YYYY-MM`), not semver; the pinned tool version in +`mise.toml` must be bumped by hand periodically. + +**System dependency (Linux):** Odin's backend shells out to **clang** as the linker. +`mise-action` installs the Odin compiler but NOT clang/LLVM. `mise run install` installs +`clang llvm` on CI Linux runners; for local Linux dev install them yourself (LLVM 17–22). +macOS uses the system/Homebrew clang. + +**Common commands:** +- `mise run build` — debug build (`odin build src -out:bin/`) +- `mise run build:prod` — optimized build (`odin build src -o:speed`) +- `mise run test` — run `@(test)` procs (`odin test src`, uses `core:testing`) +- `mise run run` — build and run (`odin run src`) +- `mise run lint` — vet the source (`odin check src -vet -strict-style`) +- `mise run publish` — cut/ensure the GitHub **source** release for the current version + +**No formatter:** Odin ships no `odin fmt`. `mise run format` / `mise run format-check` +are intentional **no-ops** (they warn and exit 0). The de-facto formatter is `odinfmt` +(part of DanielGavin/ols), which is a separate tool, not in the mise registry, and has no +`--check` mode — install it opt-in if your team wants formatting. + +**Source layout:** +- `src/lib.odin` — library procs + inline `@(test)` procs +- `src/main.odin` — entry point; imports lib procs (same package), prints the baked version +- Odin builds a *directory* as a single package; keep all `src/*.odin` in `package main` +- There is NO package manifest — `version.txt` is the sole version source + +**Writing tests:** +- Mark test procs with `@(test)` and take a `t: ^testing.T` parameter +- Assert with `testing.expect(t, cond)` / `testing.expectf(...)` +- Run with `mise run test` (wraps `odin test src`) + +**Consuming this as a library (the Odin way):** +- Odin has no package manager. Vendor the `src/` package or add a git submodule and wire + it with `-collection:name=path`, then `import name "name:..."`. The git tag is the + distribution; there is no library artifact to publish. + +**Publishing (source releases, no binaries):** +- Odin has no package manager and no binary-library convention, so this template + distributes **source**, not compiled artifacts. The git tag + its GitHub release (with + the source archive GitHub attaches automatically) *is* the release. +- `mise run upversion` creates the GitHub Release (via `@semantic-release/github`); + `mise run publish` just ensures that release exists (idempotent) — it uploads nothing. +- `mise run publish:rc` cuts a source pre-release tagged `vX.Y.Z-rc..`. +- Requires `GH_TOKEN` or `GITHUB_TOKEN` in environment. +- Consumers vendor `src/` or add a git submodule at the tag (see "Consuming this as a + library" above) — there is no binary to download and no `install.sh` flow. + +**Why no cross-compiled binaries:** Odin drives clang and links with the *host* linker by +default, so a single CI runner cannot reliably link foreign targets (macOS/Windows/arm64) +without per-arch cross-linkers/SDKs — and a bad cross-link can even exit 0 with an unusable +binary (odin-lang/Odin#4821). Combined with Odin's source-first distribution model, +shipping binaries adds risk for no ecosystem benefit. If you *do* need app binaries for a +specific platform, run `mise run build:prod` on a matching runner and attach the result +yourself. diff --git a/templates/odin/docs/development-guide.template.md b/templates/odin/docs/development-guide.template.md new file mode 100644 index 0000000..862c406 --- /dev/null +++ b/templates/odin/docs/development-guide.template.md @@ -0,0 +1,58 @@ +# {{PROJECT_NAME}} Development Guide + +Generated from {{TEMPLATE_NAME}} v{{TEMPLATE_VERSION}}. + +## Prerequisites + +- [mise](https://mise.jdx.dev/) — manages the Odin compiler and all other tools +- **clang / LLVM (17–22)** on Linux — Odin's backend links via clang. `mise run install` + installs it on CI Linux runners; install it yourself for local Linux dev. macOS uses + the system/Homebrew clang. +- [gh CLI](https://cli.github.com/) for publishing GitHub releases + +## Getting Started + +```bash +mise install # install Odin + node + shellcheck + shfmt +mise run install # npm plugins + clang/LLVM (CI Linux) +mise run build # debug build → bin/ +mise run test # run @(test) procs +``` + +## Project Structure + +``` +src/lib.odin # Library procs + inline @(test) procs +src/main.odin # Entry point (same package), prints baked version +mise.toml # Task runner and tool versions +version.txt # Single source of truth for the version (no manifest) +``` + +## Development Workflow + +1. **Write code** in `src/lib.odin` (library procs) or `src/main.odin` (entry) +2. **Write tests** as `@(test)` procs taking `t: ^testing.T` +3. **Run tests**: `mise run test` +4. **Lint**: `mise run lint` (`odin check src -vet -strict-style`) +5. **Format**: no-op — Odin ships no formatter (see CLAUDE.md; `odinfmt` is opt-in) + +## Publishing (source, not binaries) + +Odin has no package manager and no binary-library convention, so this project distributes +**source**: the git tag and its GitHub release (with the source archive GitHub attaches +automatically) are the release. No cross-compiled binaries are published — Odin links via +the host linker, so foreign targets can't link reliably on a single runner (and a bad +cross-link can exit 0 with an unusable binary, odin-lang/Odin#4821). + +1. Ensure `GH_TOKEN` or `GITHUB_TOKEN` is set +2. Push to `main` — CI runs `mise run upversion` (creates the release) then `mise run publish` +3. `mise run publish` just ensures the source release exists (idempotent); it uploads nothing +4. `mise run publish:rc` cuts a source pre-release for testing + +If you need an application binary for a specific platform, run `mise run build:prod` on a +matching runner and attach it to the release yourself. + +## Consuming as a Library + +Odin has no package manager. Vendor `src/` or add it as a git submodule, then wire it with +`-collection:name=path` and `import name "name:..."`. The git tag is the distribution. diff --git a/templates/odin/mise-tasks/build/_default b/templates/odin/mise-tasks/build/_default new file mode 100755 index 0000000..75776ba --- /dev/null +++ b/templates/odin/mise-tasks/build/_default @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +#MISE description="Build Odin project (debug)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +mkdir -p bin +odin build src -define:VERSION="$VERSION" -out:"bin/$PROJECT" +echo "Build successful → bin/$PROJECT" diff --git a/templates/odin/mise-tasks/build/prod b/templates/odin/mise-tasks/build/prod new file mode 100755 index 0000000..76be6a8 --- /dev/null +++ b/templates/odin/mise-tasks/build/prod @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +#MISE description="Build optimized release binary" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +mkdir -p bin +odin build src -o:speed -define:VERSION="$VERSION" -out:"bin/$PROJECT" +echo "Release build successful → bin/$PROJECT" diff --git a/templates/odin/mise-tasks/clean b/templates/odin/mise-tasks/clean new file mode 100755 index 0000000..b709c35 --- /dev/null +++ b/templates/odin/mise-tasks/clean @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Remove Odin build artifacts" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +rm -rf bin/ diff --git a/templates/odin/mise-tasks/docker/build b/templates/odin/mise-tasks/docker/build new file mode 100755 index 0000000..883e3ca --- /dev/null +++ b/templates/odin/mise-tasks/docker/build @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build Docker image" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker build -t $PROJECT:$VERSION . diff --git a/templates/odin/mise-tasks/docker/run b/templates/odin/mise-tasks/docker/run new file mode 100755 index 0000000..2a297ca --- /dev/null +++ b/templates/odin/mise-tasks/docker/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run Docker container" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker run --rm $PROJECT:$VERSION diff --git a/templates/odin/mise-tasks/docker/test b/templates/odin/mise-tasks/docker/test new file mode 100755 index 0000000..9b3545f --- /dev/null +++ b/templates/odin/mise-tasks/docker/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run tests inside Docker" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +docker run --rm $PROJECT:$VERSION odin test src diff --git a/templates/odin/mise-tasks/format/_default b/templates/odin/mise-tasks/format/_default new file mode 100755 index 0000000..a574462 --- /dev/null +++ b/templates/odin/mise-tasks/format/_default @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +#MISE description="Format Odin source (no-op: Odin has no bundled formatter)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +# Odin ships no `odin fmt`. odinfmt (DanielGavin/ols) is a separate, non-registry tool; +# install it opt-in to format src/. This task is a deliberate no-op so the contract holds. +echo "WARNING: Odin has no bundled formatter (no 'odin fmt')." >&2 +echo " Install odinfmt from DanielGavin/ols to format src/ (opt-in)." >&2 diff --git a/templates/odin/mise-tasks/format/check b/templates/odin/mise-tasks/format/check new file mode 100755 index 0000000..c145c7c --- /dev/null +++ b/templates/odin/mise-tasks/format/check @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +#MISE description="Check Odin formatting (no-op: no bundled formatter)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +# No native formatter and odinfmt has no --check mode; a no-op keeps format:check green +# (it is a RUNNABLE contract task). Must exit 0. +echo "WARNING: format:check is a no-op — Odin has no bundled formatter." >&2 diff --git a/templates/odin/mise-tasks/install b/templates/odin/mise-tasks/install new file mode 100755 index 0000000..a00c3cc --- /dev/null +++ b/templates/odin/mise-tasks/install @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +#MISE description="Install npm tools and ensure clang/LLVM for the Odin linker" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" + +# Odin's LLVM backend links via clang; ubuntu CI runners lack it and mise-action does not +# provide it. Install it only in CI-on-Linux when it's missing (idempotent; leaves local +# macOS/Homebrew clang alone). Assumes a GitHub-hosted runner with passwordless sudo. +if [ "${CI:-}" = "true" ] && [ "$(uname -s)" = "Linux" ] && ! command -v clang >/dev/null 2>&1; then + sudo apt-get update -y + sudo apt-get install -y clang llvm +fi + +npm install --no-save --prefix "${MISE_PROJECT_ROOT:-$PWD}" \ + semantic-release \ + @semantic-release/changelog \ + @semantic-release/exec \ + @semantic-release/git \ + @semantic-release/github \ + conventional-changelog-conventionalcommits diff --git a/templates/odin/mise-tasks/lint/_default b/templates/odin/mise-tasks/lint/_default new file mode 100755 index 0000000..0ad2695 --- /dev/null +++ b/templates/odin/mise-tasks/lint/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Vet Odin source with the compiler's static checks" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +odin check src -vet -strict-style diff --git a/templates/odin/mise-tasks/lint/fix b/templates/odin/mise-tasks/lint/fix new file mode 100755 index 0000000..2dfdd3d --- /dev/null +++ b/templates/odin/mise-tasks/lint/fix @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +#MISE description="Auto-fix mise-tasks/ shell script formatting with shfmt" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +# Odin has no source autofixer; only the shell task scripts are formatted (matches zig). +shfmt -w mise-tasks/ diff --git a/templates/odin/mise-tasks/publish/_default b/templates/odin/mise-tasks/publish/_default new file mode 100755 index 0000000..99fd3d1 --- /dev/null +++ b/templates/odin/mise-tasks/publish/_default @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +#MISE description="Publish a source release to GitHub (Odin ships as source, no binaries)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +# Odin ships as source (see CLAUDE.md): the git tag + GitHub release is the artifact, so +# we publish no binaries. Idempotent — upversion usually creates the release already; only +# create it here if missing, so `mise run publish` is safe to run standalone. +if gh release view "v$VERSION" >/dev/null 2>&1; then + log_info "Source release v$VERSION already exists — nothing to upload (source-only)" +else + log_info "Creating source release v$VERSION..." + gh release create "v$VERSION" --generate-notes +fi + +log_success "Published $PROJECT v$VERSION as a source release (no binaries)" diff --git a/templates/odin/mise-tasks/publish/rc b/templates/odin/mise-tasks/publish/rc new file mode 100755 index 0000000..f7c4afe --- /dev/null +++ b/templates/odin/mise-tasks/publish/rc @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +#MISE description="Publish an RC source pre-release to GitHub (no binaries)" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +source "${MISE_PROJECT_ROOT:-$PWD}/mise-tasks/utils" + +SHORT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "dev") +TIMESTAMP=$(date -u +%Y%m%d%H%M) +# Human-readable UTC timestamp (yyyymmddhhmm) prefixes the sha so each commit gets a +# unique, time-sortable RC tag. semver: v2.2.0-rc.202607091142.abc1234 +RC_TAG="v${VERSION}-rc.${TIMESTAMP}.${SHORT_SHA}" +# Re-runs reuse the SHA/minute but bump GITHUB_RUN_ATTEMPT; fold it in so a re-run mints a +# fresh tag instead of colliding with the pre-release already published. +[ "${GITHUB_RUN_ATTEMPT:-1}" != "1" ] && RC_TAG="${RC_TAG}.${GITHUB_RUN_ATTEMPT}" + +log_info "Cleaning up previous RC pre-releases for v${VERSION}-rc.*..." +while IFS= read -r tag; do + [[ "$tag" == "$RC_TAG" ]] && continue + gh release delete "$tag" --yes --cleanup-tag 2>/dev/null || true +done < <(gh release list --json tagName,isPrerelease \ + --jq '.[] | select(.isPrerelease) | .tagName' \ + | grep "^v${VERSION}-rc\." || true) + +# The pre-release (with its auto-created git tag at HEAD) is the source artifact — Odin +# ships no binaries, so there is nothing further to upload. +log_info "Creating GitHub source pre-release $RC_TAG..." +gh release create "$RC_TAG" \ + --title "RC: $RC_TAG" \ + --notes "Source pre-release from commit $SHORT_SHA. Not for production use." \ + --prerelease + +log_success "Published $PROJECT $RC_TAG as a source pre-release (no binaries)" diff --git a/templates/odin/mise-tasks/run b/templates/odin/mise-tasks/run new file mode 100755 index 0000000..72fb423 --- /dev/null +++ b/templates/odin/mise-tasks/run @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build and run the project" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +odin run src -define:VERSION="$VERSION" diff --git a/templates/odin/mise-tasks/test b/templates/odin/mise-tasks/test new file mode 100755 index 0000000..3865854 --- /dev/null +++ b/templates/odin/mise-tasks/test @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Run Odin test suite" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +odin test src diff --git a/templates/odin/mise-tasks/version/_default b/templates/odin/mise-tasks/version/_default new file mode 100755 index 0000000..db0a60c --- /dev/null +++ b/templates/odin/mise-tasks/version/_default @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Print current version" +set -euo pipefail +cd "${MISE_PROJECT_ROOT:-$PWD}" +cat version.txt diff --git a/templates/odin/mise-tasks/version/next b/templates/odin/mise-tasks/version/next new file mode 100755 index 0000000..aa30c62 --- /dev/null +++ b/templates/odin/mise-tasks/version/next @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +#MISE description="Preview next semantic-release version" +set -euo pipefail +mise run upversion -- --dry-run diff --git a/templates/odin/mise.toml b/templates/odin/mise.toml new file mode 100644 index 0000000..995d543 --- /dev/null +++ b/templates/odin/mise.toml @@ -0,0 +1,26 @@ +# ============================================================================= +# mise.toml - odin template +# Overlays the base mise.toml during scaffold. Tasks are file tasks under +# mise-tasks/ (this template overrides only the tasks that differ from base). +# ============================================================================= + +[env] +_.path = ['{{config_root}}/node_modules/.bin'] +PROJECT = "mise-lib-template" +VERSION = "{{exec(command='cat version.txt 2>/dev/null || echo 0.1.0')}}" + +[tools] +# The mise registry short-name "odin" is broken (dead vfox/asdf plugin repos); use the +# github backend. Odin tags are calendar-based `dev-YYYY-MM`, NOT semver — pin an explicit +# dated release (bump this periodically; it won't look like semver to Renovate). +"github:odin-lang/Odin" = "dev-2026-07" +node = "lts" +bats = "latest" +shellcheck = "latest" +shfmt = "latest" + +[task_config] +includes = ["mise-tasks"] + +[settings] +experimental = true diff --git a/templates/odin/src/lib.odin b/templates/odin/src/lib.odin new file mode 100644 index 0000000..09a849a --- /dev/null +++ b/templates/odin/src/lib.odin @@ -0,0 +1,27 @@ +// mise_lib_template — generated from mise-odin-template. +package main + +import "core:strings" +import "core:testing" + +// starts_with reports whether haystack begins with needle. +starts_with :: proc(haystack, needle: string) -> bool { + return strings.has_prefix(haystack, needle) +} + +// ends_with reports whether haystack ends with needle. +ends_with :: proc(haystack, needle: string) -> bool { + return strings.has_suffix(haystack, needle) +} + +@(test) +test_starts_with :: proc(t: ^testing.T) { + testing.expect(t, starts_with("hello world", "hello")) + testing.expect(t, !starts_with("hello", "world")) +} + +@(test) +test_ends_with :: proc(t: ^testing.T) { + testing.expect(t, ends_with("hello world", "world")) + testing.expect(t, !ends_with("hello", "world")) +} diff --git a/templates/odin/src/main.odin b/templates/odin/src/main.odin new file mode 100644 index 0000000..43e7264 --- /dev/null +++ b/templates/odin/src/main.odin @@ -0,0 +1,13 @@ +package main + +import "core:fmt" + +// VERSION is injected at compile time via `-define:VERSION=$VERSION` (see mise-tasks/build/*). +// A plain `odin run src` with no -define falls back to "dev". +VERSION :: #config(VERSION, "dev") + +main :: proc() { + fmt.println("Hello from mise-lib-template!") + fmt.printf("version: %s\n", VERSION) + fmt.printf("starts_with(\"hello\", \"he\"): %t\n", starts_with("hello", "he")) +} From 4e769ffdee828eb9ed17dc05497672ddc27c6139 Mon Sep 17 00:00:00 2001 From: Siddharth Kapoor Date: Mon, 13 Jul 2026 10:27:38 -0400 Subject: [PATCH 4/4] feat: wire scaffold, tests, and docs for the new templates Shared scaffold, test, and catalog changes supporting the go, rust, and odin templates. - scaffold: add --github-org flag/prompt (git-remote fallback) so module paths resolve to the user's org; render go packages as flatcase; skip the install.sh prompt for source-only templates - tests: scaffold, docker, and template-export bats coverage for the three templates - publish: attach the new templates to the template-package release flow - docs: list the templates in the catalog README - gitignore: keep the rust template's committed Cargo.lock --- .gitignore | 3 + mise-tasks/scaffold | 91 ++++++-- mise-tasks/templates/publish | 45 ++++ mise-tasks/templates/publish-rc | 45 ++++ templates/README.md | 13 +- test/docker.bats | 68 ++++++ test/scaffold-templates.bats | 355 ++++++++++++++++++++++++++++++++ test/template-export.bats | 46 +++++ 8 files changed, 642 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index b418614..92d43d9 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/mise-tasks/scaffold b/mise-tasks/scaffold index da4527c..041be20 100755 --- a/mise-tasks/scaffold +++ b/mise-tasks/scaffold @@ -18,7 +18,9 @@ Options: --non-interactive Skip prompts, use defaults --project NAME Project name (default: destination directory name) --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 ---------------------------------------------------------------------- @@ -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// 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)" @@ -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 @@ -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" } @@ -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" @@ -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 @@ -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:" @@ -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 @@ -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 @@ -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 @@ -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; \ diff --git a/mise-tasks/templates/publish b/mise-tasks/templates/publish index bcb34dd..68732e2 100755 --- a/mise-tasks/templates/publish +++ b/mise-tasks/templates/publish @@ -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" diff --git a/mise-tasks/templates/publish-rc b/mise-tasks/templates/publish-rc index 5983c54..6e2c2ed 100755 --- a/mise-tasks/templates/publish-rc +++ b/mise-tasks/templates/publish-rc @@ -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" diff --git a/templates/README.md b/templates/README.md index 124e461..d3eb8b1 100644 --- a/templates/README.md +++ b/templates/README.md @@ -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 diff --git a/test/docker.bats b/test/docker.bats index 88c066c..57111d4 100644 --- a/test/docker.bats +++ b/test/docker.bats @@ -67,6 +67,16 @@ run_scaffold() { assert_tasks_runnable "$DEST" } +@test "go scaffold preserves docker task contract" { + run_scaffold "go" + assert_docker_tasks "$DEST" +} + +@test "go scaffold: contract tasks are runnable" { + run_scaffold "go" + assert_tasks_runnable "$DEST" +} + @test "pnpm scaffold preserves docker task contract" { run_scaffold "pnpm" assert_docker_tasks "$DEST" @@ -94,6 +104,11 @@ run_scaffold() { [ -f "$DEST/mise-tasks/docker/build" ] } +@test "go: docker:build defined as a file task" { + run_scaffold "go" + [ -f "$DEST/mise-tasks/docker/build" ] +} + @test "pnpm: docker:build defined as a file task" { run_scaffold "pnpm" [ -f "$DEST/mise-tasks/docker/build" ] @@ -121,3 +136,56 @@ run_scaffold() { grep -q 'mise run build' "$DEST/Dockerfile" grep -q 'mise.*run.*run' "$DEST/Dockerfile" } + +@test "go scaffold includes Dockerfile" { + run_scaffold "go" + [ -f "$DEST/Dockerfile" ] + grep -q 'mise run build' "$DEST/Dockerfile" + grep -q 'mise.*run.*run' "$DEST/Dockerfile" +} + +@test "rust scaffold preserves docker task contract" { + run_scaffold "rust" + assert_docker_tasks "$DEST" +} + +@test "rust scaffold: contract tasks are runnable" { + run_scaffold "rust" + assert_tasks_runnable "$DEST" +} + +@test "rust: docker:build defined as a file task" { + run_scaffold "rust" + [ -f "$DEST/mise-tasks/docker/build" ] +} + +@test "rust scaffold includes Dockerfile" { + run_scaffold "rust" + [ -f "$DEST/Dockerfile" ] + grep -q 'mise run build' "$DEST/Dockerfile" + grep -q 'mise.*run.*run' "$DEST/Dockerfile" +} + +@test "odin scaffold preserves docker task contract" { + run_scaffold "odin" + assert_docker_tasks "$DEST" +} + +# Runs build+test which compile via clang for odin — skip if clang absent. +@test "odin scaffold: contract tasks are runnable" { + command -v clang >/dev/null 2>&1 || skip "clang/LLVM required for odin build/test" + run_scaffold "odin" + assert_tasks_runnable "$DEST" +} + +@test "odin: docker:build defined as a file task" { + run_scaffold "odin" + [ -f "$DEST/mise-tasks/docker/build" ] +} + +@test "odin scaffold includes Dockerfile" { + run_scaffold "odin" + [ -f "$DEST/Dockerfile" ] + grep -q 'mise run build' "$DEST/Dockerfile" + grep -q 'mise.*run.*run' "$DEST/Dockerfile" +} diff --git a/test/scaffold-templates.bats b/test/scaffold-templates.bats index 1803a1d..d5d5611 100644 --- a/test/scaffold-templates.bats +++ b/test/scaffold-templates.bats @@ -19,6 +19,7 @@ teardown() { run_scaffold() { local template="${1:-}" local project="${2:-my-lib}" + local github_org="${3:-}" local args=( --src "$TEMPLATE_SRC" --dest "$DEST" @@ -26,6 +27,7 @@ run_scaffold() { --non-interactive ) [[ -n "$template" ]] && args+=(--template "$template") + [[ -n "$github_org" ]] && args+=(--github-org "$github_org") bash "$TEMPLATE_SRC/mise-tasks/scaffold" "${args[@]}" } @@ -215,6 +217,105 @@ run_scaffold() { grep -q 'my_lib' "$DEST/src/lib.zig" } +# ── go template ─────────────────────────────────────────────────────────────── + +@test "go: scaffold succeeds" { + run run_scaffold "go" + [ "$status" -eq 0 ] +} + +@test "go: honors full task contract" { + run_scaffold "go" + assert_contract_tasks "$DEST" +} + +@test "go: contract tasks are runnable" { + run_scaffold "go" + assert_tasks_runnable "$DEST" +} + +@test "go: docker tasks present" { + run_scaffold "go" + assert_docker_tasks "$DEST" +} + +@test "go: go.mod created with project module path" { + run_scaffold "go" "my-lib" + [ -f "$DEST/go.mod" ] + grep -q 'my-lib' "$DEST/go.mod" +} + +@test "go: go.mod uses github org, not duplicated project name" { + # Default org fallback (no git remote in the bats temp dest) is your-github-org. + run_scaffold "go" "my-lib" + grep -q '^module github.com/your-github-org/my-lib$' "$DEST/go.mod" + # Regression guard: org segment must not collapse to the project name. + ! grep -q 'github.com/my-lib/my-lib' "$DEST/go.mod" +} + +@test "go: --github-org sets module path org segment" { + run_scaffold "go" "my-lib" "acme-corp" + grep -q '^module github.com/acme-corp/my-lib$' "$DEST/go.mod" + grep -q 'github.com/acme-corp/my-lib/src/mylib' "$DEST/main.go" +} + +@test "go: org rewrite leaves unrelated cloudvoyant links intact" { + # The org rewrite must be scoped to the project's repo path, not blanket-replace + # every github.com/cloudvoyant/* link (e.g. the claudevoyant plugin in the docs). + run_scaffold "go" "my-lib" "acme-corp" + grep -q 'github.com/cloudvoyant/claudevoyant' "$DEST/docs/user-guide.md" + ! grep -rq 'acme-corp/claudevoyant' "$DEST" +} + +@test "go: main.go and src lib package created" { + run_scaffold "go" "my-lib" + [ -f "$DEST/main.go" ] + # Go packages use flatcase (no underscores): my-lib → mylib + [ -d "$DEST/src/mylib" ] + [ -f "$DEST/src/mylib/lib.go" ] + [ -f "$DEST/src/mylib/lib_test.go" ] +} + +@test "go: package name is flatcase, not snake_case" { + run_scaffold "go" "my-lib" + [ ! -d "$DEST/src/my_lib" ] + grep -q '^package mylib$' "$DEST/src/mylib/lib.go" +} + +@test "go: TEMPLATE is mise-go-template" { + run_scaffold "go" + grep -q 'TEMPLATE.*"mise-go-template"' "$DEST/mise.toml" +} + +@test "go: CLAUDE.md contains Go conventions" { + run_scaffold "go" + grep -q "go build" "$DEST/CLAUDE.md" + grep -q "go test" "$DEST/CLAUDE.md" +} + +@test "go: sample-code.txt removed" { + run_scaffold "go" + [ ! -f "$DEST/src/sample-code.txt" ] +} + +@test "go: mise-tasks/ scripts are executable" { + run_scaffold "go" + [ -x "$DEST/mise-tasks/publish/_default" ] + [ -x "$DEST/mise-tasks/build/all-platforms" ] +} + +@test "go: no pyproject.toml / build.zig / Cargo.toml created" { + run_scaffold "go" + [ ! -f "$DEST/pyproject.toml" ] + [ ! -f "$DEST/build.zig" ] + [ ! -f "$DEST/Cargo.toml" ] +} + +@test "go: project name replaced in lib source" { + run_scaffold "go" "my-lib" + grep -q 'mylib' "$DEST/src/mylib/lib.go" +} + # ── pnpm template ───────────────────────────────────────────────────────────── @test "pnpm: scaffold succeeds" { @@ -343,6 +444,16 @@ run_scaffold() { ! grep -q 'templates:' "$DEST/.github/workflows/ci.yml" } +@test "go: ci.yml has publish-rc job calling publish-rc task" { + run_scaffold "go" + grep -q 'mise run publish:rc' "$DEST/.github/workflows/ci.yml" +} + +@test "go: ci.yml has no template-only tasks" { + run_scaffold "go" + ! grep -q 'templates:' "$DEST/.github/workflows/ci.yml" +} + @test "uv: mise-tasks/publish-rc is executable" { run_scaffold "uv" [ -x "$DEST/mise-tasks/publish/rc" ] @@ -353,6 +464,11 @@ run_scaffold() { [ -x "$DEST/mise-tasks/publish/rc" ] } +@test "go: mise-tasks/publish-rc is executable" { + run_scaffold "go" + [ -x "$DEST/mise-tasks/publish/rc" ] +} + # ── Invalid template ────────────────────────────────────────────────────────── @test "invalid template name exits with error" { @@ -382,6 +498,14 @@ run_scaffold() { grep -q 'build:all-platforms' "$DEST/mise-tasks/publish/_default" } +@test "go: nested override replaced base (build + publish)" { + run_scaffold "go" + grep -q 'go build ./...' "$DEST/mise-tasks/build/_default" + grep -q 'build:all-platforms' "$DEST/mise-tasks/publish/_default" + grep -q 'gh release upload' "$DEST/mise-tasks/publish/_default" + ! grep -q 'gcloud' "$DEST/mise-tasks/publish/_default" +} + @test "agnostic: all task files executable (incl. nested)" { run_scaffold "" assert_tasks_executable "$DEST" @@ -401,3 +525,234 @@ run_scaffold() { run_scaffold "pnpm" assert_tasks_executable "$DEST" } + +@test "go: all task files executable (incl. nested)" { + run_scaffold "go" + assert_tasks_executable "$DEST" +} + +# ── rust template ───────────────────────────────────────────────────────────── + +@test "rust: scaffold succeeds" { + run run_scaffold "rust" + [ "$status" -eq 0 ] +} + +@test "rust: honors full task contract" { + run_scaffold "rust" + assert_contract_tasks "$DEST" +} + +@test "rust: contract tasks are runnable" { + run_scaffold "rust" + assert_tasks_runnable "$DEST" +} + +@test "rust: docker tasks present" { + run_scaffold "rust" + assert_docker_tasks "$DEST" +} + +@test "rust: Cargo.toml created with project name" { + run_scaffold "rust" "my-lib" + [ -f "$DEST/Cargo.toml" ] + grep -q 'my_lib' "$DEST/Cargo.toml" +} + +@test "rust: Cargo.lock created" { + run_scaffold "rust" + [ -f "$DEST/Cargo.lock" ] +} + +@test "rust: src/lib.rs and src/main.rs created" { + run_scaffold "rust" + [ -f "$DEST/src/lib.rs" ] + [ -f "$DEST/src/main.rs" ] +} + +@test "rust: TEMPLATE is mise-rust-template" { + run_scaffold "rust" + grep -q 'TEMPLATE.*"mise-rust-template"' "$DEST/mise.toml" +} + +@test "rust: CLAUDE.md contains Rust conventions" { + run_scaffold "rust" + grep -q "cargo build" "$DEST/CLAUDE.md" + grep -q "cargo clippy" "$DEST/CLAUDE.md" +} + +@test "rust: sample-code.txt removed" { + run_scaffold "rust" + [ ! -f "$DEST/src/sample-code.txt" ] +} + +@test "rust: mise-tasks/ scripts are executable" { + run_scaffold "rust" + [ -x "$DEST/mise-tasks/publish/_default" ] + [ -x "$DEST/mise-tasks/build/all-platforms" ] +} + +@test "rust: no pyproject.toml created" { + run_scaffold "rust" + [ ! -f "$DEST/pyproject.toml" ] +} + +@test "rust: no build.zig.zon created" { + run_scaffold "rust" + [ ! -f "$DEST/build.zig.zon" ] +} + +@test "rust: project name replaced in src/lib.rs" { + run_scaffold "rust" "my-lib" + grep -q 'mise-rust-template' "$DEST/src/lib.rs" +} + +@test "rust: project name replaced in src/main.rs" { + run_scaffold "rust" "my-lib" + grep -q 'my_lib' "$DEST/src/main.rs" +} + +@test "rust: ci.yml has publish-rc job calling publish-rc task" { + run_scaffold "rust" + grep -q 'mise run publish:rc' "$DEST/.github/workflows/ci.yml" +} + +@test "rust: ci.yml has no template-only tasks" { + run_scaffold "rust" + ! grep -q 'templates:' "$DEST/.github/workflows/ci.yml" +} + +@test "rust: mise-tasks/publish-rc is executable" { + run_scaffold "rust" + [ -x "$DEST/mise-tasks/publish/rc" ] +} + +@test "rust: nested override replaced base (build + publish)" { + run_scaffold "rust" + grep -q 'cargo build' "$DEST/mise-tasks/build/_default" + grep -q 'build:all-platforms' "$DEST/mise-tasks/publish/_default" +} + +@test "rust: nested task files are the template versions" { + run_scaffold "rust" + grep -q 'cargo test' "$DEST/mise-tasks/test" + grep -q 'cargo clippy' "$DEST/mise-tasks/lint/_default" +} + +@test "rust: all task files executable (incl. nested)" { + run_scaffold "rust" + assert_tasks_executable "$DEST" +} + +# ── odin template ────────────────────────────────────────────────────────────── + +@test "odin: scaffold succeeds" { + run run_scaffold "odin" + [ "$status" -eq 0 ] +} + +@test "odin: honors full task contract" { + run_scaffold "odin" + assert_contract_tasks "$DEST" +} + +# NOTE: assert_tasks_runnable runs build+test, which for odin actually compile via clang. +# Skip if clang is absent (local dev / CI without clang pre-installed). +@test "odin: contract tasks are runnable" { + command -v clang >/dev/null 2>&1 || skip "clang/LLVM required for odin build/test" + run_scaffold "odin" + assert_tasks_runnable "$DEST" +} + +@test "odin: docker tasks present" { + run_scaffold "odin" + assert_docker_tasks "$DEST" +} + +@test "odin: src/lib.odin and src/main.odin created" { + run_scaffold "odin" + [ -f "$DEST/src/lib.odin" ] + [ -f "$DEST/src/main.odin" ] +} + +@test "odin: no foreign manifest created" { + run_scaffold "odin" + [ ! -f "$DEST/build.zig.zon" ] + [ ! -f "$DEST/Cargo.toml" ] + [ ! -f "$DEST/go.mod" ] + [ ! -f "$DEST/pyproject.toml" ] +} + +@test "odin: TEMPLATE is mise-odin-template" { + run_scaffold "odin" + grep -q 'TEMPLATE.*"mise-odin-template"' "$DEST/mise.toml" +} + +@test "odin: CLAUDE.md contains Odin conventions" { + run_scaffold "odin" + grep -q "odin build" "$DEST/CLAUDE.md" + grep -q "no bundled formatter\|no 'odin fmt'\|no .odin fmt." "$DEST/CLAUDE.md" +} + +@test "odin: sample-code.txt removed" { + run_scaffold "odin" + [ ! -f "$DEST/src/sample-code.txt" ] +} + +@test "odin: mise-tasks/ scripts are executable" { + run_scaffold "odin" + [ -x "$DEST/mise-tasks/publish/_default" ] + [ -x "$DEST/mise-tasks/build/prod" ] +} + +@test "odin: publish is source-only (no cross-compiled binaries)" { + run_scaffold "odin" + # Odin ships as source: no build:all-platforms task, and publish uploads no binaries. + [ ! -f "$DEST/mise-tasks/build/all-platforms" ] + grep -q 'gh release' "$DEST/mise-tasks/publish/_default" + ! grep -q 'build:all-platforms' "$DEST/mise-tasks/publish/_default" + ! grep -q 'release upload' "$DEST/mise-tasks/publish/_default" +} + +@test "odin: project name replaced in src/lib.odin" { + run_scaffold "odin" "my-lib" + grep -q 'my_lib' "$DEST/src/lib.odin" +} + +@test "odin: mise.toml uses github odin backend" { + run_scaffold "odin" + grep -q 'github:odin-lang/Odin' "$DEST/mise.toml" +} + +@test "odin: ci.yml has publish-rc job calling publish-rc task" { + run_scaffold "odin" + grep -q 'mise run publish:rc' "$DEST/.github/workflows/ci.yml" +} + +@test "odin: ci.yml has no template-only tasks" { + run_scaffold "odin" + ! grep -q 'templates:' "$DEST/.github/workflows/ci.yml" +} + +@test "odin: mise-tasks/publish-rc is executable" { + run_scaffold "odin" + [ -x "$DEST/mise-tasks/publish/rc" ] +} + +@test "odin: nested override replaced base (docker + publish)" { + run_scaffold "odin" + grep -q 'docker build -t' "$DEST/mise-tasks/docker/build" + # Odin's publish override is source-only (differs from the base TODO stub). + grep -q 'source release' "$DEST/mise-tasks/publish/_default" +} + +@test "odin: format tasks are no-op stubs" { + run_scaffold "odin" + grep -q 'no-op\|WARNING' "$DEST/mise-tasks/format/_default" + grep -q 'WARNING' "$DEST/mise-tasks/format/check" +} + +@test "odin: all task files executable (incl. nested)" { + run_scaffold "odin" + assert_tasks_executable "$DEST" +} diff --git a/test/template-export.bats b/test/template-export.bats index 7af4f5e..02aaae9 100644 --- a/test/template-export.bats +++ b/test/template-export.bats @@ -144,6 +144,12 @@ teardown() { echo "$listing" | grep -q "templates/zig/mise.toml" } +@test "git archive includes templates/go/mise.toml" { + local listing + listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) + echo "$listing" | grep -q "templates/go/mise.toml" +} + @test "git archive includes templates/uv/CLAUDE.md.append" { local listing listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) @@ -156,9 +162,49 @@ teardown() { echo "$listing" | grep -q "templates/zig/CLAUDE.md.append" } +@test "git archive includes templates/go/CLAUDE.md.append" { + local listing + listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) + echo "$listing" | grep -q "templates/go/CLAUDE.md.append" +} + @test "templates/uv/CLAUDE.md.append not in export-ignore" { # Ensure .gitattributes does not accidentally exclude templates/ run git -C "$REPO_DIR" check-attr export-ignore -- templates/uv/CLAUDE.md.append # Should either be unset or not export-ignore ! echo "$output" | grep -q "export-ignore: set" } + +@test "git archive includes templates/rust/mise.toml" { + local listing + listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) + echo "$listing" | grep -q "templates/rust/mise.toml" +} + +@test "git archive includes templates/rust/CLAUDE.md.append" { + local listing + listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) + echo "$listing" | grep -q "templates/rust/CLAUDE.md.append" +} + +@test "templates/rust/CLAUDE.md.append not in export-ignore" { + run git -C "$REPO_DIR" check-attr export-ignore -- templates/rust/CLAUDE.md.append + ! echo "$output" | grep -q "export-ignore: set" +} + +@test "git archive includes templates/odin/mise.toml" { + local listing + listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) + echo "$listing" | grep -q "templates/odin/mise.toml" +} + +@test "git archive includes templates/odin/CLAUDE.md.append" { + local listing + listing=$(git -C "$REPO_DIR" archive HEAD --format=tar | tar -t) + echo "$listing" | grep -q "templates/odin/CLAUDE.md.append" +} + +@test "templates/odin/CLAUDE.md.append not in export-ignore" { + run git -C "$REPO_DIR" check-attr export-ignore -- templates/odin/CLAUDE.md.append + ! echo "$output" | grep -q "export-ignore: set" +}