From 3e2e7f2f652e9c600022401f831e56da3edf1c68 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 8 Mar 2026 19:16:48 -0600 Subject: [PATCH] feat(monorel): add installer for monorepo release tool Adds releases.js, install.sh, install.ps1, and README.md for monorel, a Go monorepo release tool from therootcompany/golib. Filters monorepo releases by tools/monorel/ prefix and auto-installs prerequisites (git, gh, goreleaser). --- monorel/README.md | 165 ++++++++++++++++++++++++++++++++++++++++++++ monorel/install.ps1 | 48 +++++++++++++ monorel/install.sh | 57 +++++++++++++++ monorel/releases.js | 37 ++++++++++ 4 files changed, 307 insertions(+) create mode 100644 monorel/README.md create mode 100644 monorel/install.ps1 create mode 100644 monorel/install.sh create mode 100644 monorel/releases.js diff --git a/monorel/README.md b/monorel/README.md new file mode 100644 index 0000000..b1f2ca2 --- /dev/null +++ b/monorel/README.md @@ -0,0 +1,165 @@ +--- +title: monorel +homepage: https://github.com/therootcompany/golib/tree/main/tools/monorel +tagline: | + monorel: Monorepo Release Tool for Go binaries. +--- + +To update or switch versions, run `webi monorel@stable` (or `@v0.6`, `@beta`, +etc). + +### Files + +These are the files that are created and/or modified with this installer: + +```text +~/.config/envman/PATH.env +~/.local/bin/monorel +~/.local/opt/monorel-VERSION/bin/monorel +``` + +These are the files that monorel creates and/or modifies in your project: + +```text +/.goreleaser.yaml +.git/refs/tags//v* +``` + +## Cheat Sheet + +> `monorel` manages independently-versioned Go modules and releases in a single +> repository — initializing goreleaser configs, bumping versions, and publishing +> multi-arch releases. + +### How to use monorel + +```sh +# Generate .goreleaser.yaml for all modules +monorel init --recursive ./ + +# Tag the next patch version for all modules with new commits +monorel bump --recursive ./ + +# Build, package, and publish GitHub releases for all binaries of a module +monorel release --recursive ./tools/monorel +``` + +### How monorepo versioning works + +Each `go.mod` is an independently-versioned module. Tags use the module's path +as a prefix, and each module with binaries gets a `.goreleaser.yaml`: + +```text +./ +├── go.mod # v0.1.1 (library-only) +├── io/ +│ └── transform/ +│ └── gsheet2csv/ +│ ├── go.mod # io/transform/gsheet2csv/v1.0.5 +│ ├── .goreleaser.yaml +│ └── cmd/ +│ ├── gsheet2csv/ +│ ├── gsheet2env/ +│ └── gsheet2tsv/ +└── tools/ + └── monorel/ + ├── go.mod # tools/monorel/v1.0.0 + └── .goreleaser.yaml +``` + +### `monorel init` vs `goreleaser init` + +`goreleaser init` generates a config that assumes one module per repo and +derives names and versions from the git tag. That breaks in a monorepo with +prefixed tags. `monorel init` fixes this: + +| | `goreleaser init` | `monorel init` | +| ------------- | --------------------------- | --------------------------------------- | +| Project name | `{{ .ProjectName }}` | Hard-coded binary name | +| Version | Derived from git tag | `{{ .Env.VERSION }}` (plain semver) | +| Publishing | goreleaser's built-in | Disabled; uses `gh release` instead | +| Multiple bins | Manual config | Auto-discovered, shared via YAML anchor | +| Monorepo tags | (requires Pro subscription) | Prefix-aware (`cmd/foo/v1.2.3`) | + +### Generated `.goreleaser.yaml`: single binary + +For a module with one binary (like `tools/monorel/`), the generated config has a +single build entry: + +```yaml +builds: + - id: monorel + binary: monorel + env: + - CGO_ENABLED=0 + ldflags: + - >- + -s -w -X main.version={{.Env.VERSION}} -X main.commit={{.Commit}} -X + main.date={{.Date}} + goos: + - darwin + - linux + - windows + # ... and more + +archives: + - id: monorel + ids: [monorel] + # Hard-coded name instead of {{ .ProjectName }} — goreleaser derives + # ProjectName from the prefixed tag, which would produce messy filenames. + # {{ .Env.VERSION }} for the same reason — the raw tag version includes + # the module path prefix. + name_template: >- + monorel_{{ .Env.VERSION }}_{{ title .Os }}_{{ .Arch }} + +# goreleaser Pro would be needed to publish from a prefixed tag, +# so monorel disables goreleaser's publisher and uses 'gh release' instead. +release: + disable: true +``` + +### Generated `.goreleaser.yaml`: multiple binaries + +When a module has several commands under `cmd/`, monorel generates a build entry +per binary with shared settings via a YAML anchor: + +```yaml +builds: + - id: gsheet2csv + binary: gsheet2csv + main: ./cmd/gsheet2csv + <<: &build_defaults + env: + - CGO_ENABLED=0 + ldflags: + - >- + -s -w -X main.version={{.Env.VERSION}} + goos: + - darwin + - linux + - windows + # ... + - id: gsheet2env + binary: gsheet2env + main: ./cmd/gsheet2env + <<: *build_defaults + - id: gsheet2tsv + binary: gsheet2tsv + main: ./cmd/gsheet2tsv + <<: *build_defaults + +archives: + - id: gsheet2csv + # All binaries are bundled into one archive per platform. + ids: [gsheet2csv, gsheet2env, gsheet2tsv] + # Same hard-coded name and {{ .Env.VERSION }} to avoid prefixed tag leaking. + name_template: >- + gsheet2csv_{{ .Env.VERSION }}_{{ title .Os }}_{{ .Arch }} + +# Same as single binary — disable goreleaser's publisher for monorepo tags. +release: + disable: true +``` + +All three binaries share one version tag (`io/transform/gsheet2csv/v1.0.5`) and +one GitHub release. diff --git a/monorel/install.ps1 b/monorel/install.ps1 new file mode 100644 index 0000000..f4c2268 --- /dev/null +++ b/monorel/install.ps1 @@ -0,0 +1,48 @@ +#!/usr/bin/env pwsh + +$pkg_cmd_name = "monorel" + +$pkg_dst_cmd = "$Env:USERPROFILE\.local\bin\monorel.exe" +$pkg_dst = "$pkg_dst_cmd" + +$pkg_src_cmd = "$Env:USERPROFILE\.local\opt\monorel-v$Env:WEBI_VERSION\bin\monorel.exe" +$pkg_src_bin = "$Env:USERPROFILE\.local\opt\monorel-v$Env:WEBI_VERSION\bin" +$pkg_src_dir = "$Env:USERPROFILE\.local\opt\monorel-v$Env:WEBI_VERSION" +$pkg_src = "$pkg_src_cmd" + +New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null +$pkg_download = "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE" + +# Fetch archive +IF (!(Test-Path -Path "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE")) { + Write-Output "Downloading monorel from $Env:WEBI_PKG_URL to $pkg_download" + & curl.exe -A "$Env:WEBI_UA" -fsSL "$Env:WEBI_PKG_URL" -o "$pkg_download.part" + & Move-Item "$pkg_download.part" "$pkg_download" +} + +IF (!(Test-Path -Path "$pkg_src_cmd")) { + Write-Output "Installing monorel" + + # Enter tmp + Push-Location .local\tmp + + # Remove any leftover tmp cruft + Remove-Item -Path ".\monorel-v*" -Recurse -ErrorAction Ignore + Remove-Item -Path ".\monorel.exe" -Recurse -ErrorAction Ignore + + # Unpack archive + Write-Output "Unpacking $pkg_download" + & tar xf "$pkg_download" + + # Settle unpacked archive into place + Write-Output "Install Location: $pkg_src_cmd" + New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null + Move-Item -Path ".\monorel.exe" -Destination "$pkg_src_bin" + + # Exit tmp + Pop-Location +} + +Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'" +Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null +Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse diff --git a/monorel/install.sh b/monorel/install.sh new file mode 100644 index 0000000..c8b6066 --- /dev/null +++ b/monorel/install.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# shellcheck disable=SC2034 + +set -e +set -u + +__init_monorel() { + pkg_cmd_name="monorel" + + pkg_src_dir="$HOME/.local/opt/monorel-v$WEBI_VERSION" + pkg_src_cmd="$pkg_src_dir/bin/monorel" + pkg_src="$pkg_src_cmd" + + pkg_dst_cmd="$HOME/.local/bin/monorel" + pkg_dst="$pkg_dst_cmd" + + # pkg_install must be defined by every package + pkg_install() { + # ~/.local/opt/monorel-v0.6.5/bin + mkdir -p "$(dirname "$pkg_src_cmd")" + + # mv monorel ~/.local/opt/monorel-v0.6.5/bin/monorel + mv ./monorel "$pkg_src_cmd" + } + + pkg_post_install() { + b_old_path="${PATH}" + export PATH="$HOME/.local/bin:${PATH}" + + if ! command -v git > /dev/null; then + "$HOME/.local/bin/webi" git + fi + + if ! command -v gh > /dev/null; then + "$HOME/.local/bin/webi" gh + fi + + if ! command -v goreleaser > /dev/null; then + "$HOME/.local/bin/webi" goreleaser + fi + + export PATH="${b_old_path}" + } + + pkg_get_current_version() { + # 'monorel --version' has output in this format: + # monorel v0.6.6 ba674a6 (2026-03-08T23:24:03Z) + # This trims it down to just the version number: + # 0.6.6 + monorel --version 2> /dev/null | + head -n 1 | + cut -d' ' -f2 | + cut -c 2- + } +} + +__init_monorel diff --git a/monorel/releases.js b/monorel/releases.js new file mode 100644 index 0000000..c8e3012 --- /dev/null +++ b/monorel/releases.js @@ -0,0 +1,37 @@ +'use strict'; + +var github = require('../_common/github.js'); +var owner = 'therootcompany'; +var repo = 'golib'; + +let Releases = module.exports; + +Releases.latest = async function () { + let all = await github(null, owner, repo); + + // This is a monorepo — keep only monorel releases and strip the + // path prefix from the version so normalize.js sees plain semver. + all.releases = all.releases.filter(function (rel) { + return rel.version.startsWith('tools/monorel/'); + }); + all.releases.forEach(function (rel) { + rel.version = rel.version.replace(/^tools\/monorel\//, ''); + }); + + return all; +}; + +Releases.sample = async function () { + let normalize = require('../_webi/normalize.js'); + let all = await Releases.latest(); + all = normalize(all); + all.releases = all.releases.slice(0, 5); + return all; +}; + +if (module === require.main) { + (async function () { + let samples = await Releases.sample(); + console.info(JSON.stringify(samples, null, 2)); + })(); +}