From fcfb32525c2b2db12ff2dcf87f7cffe1a9c8fab4 Mon Sep 17 00:00:00 2001 From: Patrik Ragnarsson Date: Fri, 28 Nov 2025 00:09:01 +0100 Subject: [PATCH] Add `cloudamqp version` command Co-authored-by: Claude --- .github/workflows/README.md | 143 ++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 20 ++++- .github/workflows/release.yml | 31 +++++++- BUILD_VERSION.md | 119 ++++++++++++++++++++++++++++ Makefile | 35 +++++++-- cmd/root.go | 16 ++++ cmd/version.go | 35 +++++++++ 7 files changed, 389 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/README.md create mode 100644 BUILD_VERSION.md create mode 100644 cmd/version.go diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..6612c7b --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,143 @@ +# GitHub Actions Workflows + +This directory contains the CI/CD workflows for the CloudAMQP CLI. + +## Workflows + +### CI Workflow (`ci.yml`) + +Runs on every push to `main` or `develop` branches, and on pull requests. + +**What it does:** +- Tests with multiple Go versions (1.21, 1.22, 1.23) +- Runs format checks, vet, and tests +- Builds the binary with version information +- Uploads test coverage to Codecov +- Verifies the binary works with `--help` and `version` commands + +**Version Information:** +The CI build automatically includes: +- Version: From `git describe --tags --always --dirty` (or "dev" as fallback) +- Build Date: Current date in UTC (YYYY-MM-DD) +- Git Commit: Short commit hash + +### Release Workflow (`release.yml`) + +Triggers when a tag starting with `v` is pushed (e.g., `v1.0.0`), or can be run manually. + +**What it does:** +1. **Build Job**: Builds cross-platform binaries for: + - Linux (amd64, arm64) + - macOS (amd64, arm64) + - Windows (amd64, arm64) + +2. **Package Job**: Creates release archives: + - `.tar.gz` for Linux/macOS binaries + - `.zip` for Windows binaries + - Creates GitHub Release with all artifacts + +**Version Information:** +Release builds automatically include: +- Version: From the git tag (e.g., `v1.0.0`) +- Build Date: Current date in UTC (YYYY-MM-DD) +- Git Commit: Short commit hash + +The version info is embedded using Go's `-ldflags`: +```bash +-ldflags="-w -s -X cloudamqp-cli/cmd.Version=$VERSION -X cloudamqp-cli/cmd.BuildDate=$BUILD_DATE -X cloudamqp-cli/cmd.GitCommit=$GIT_COMMIT" +``` + +## Creating a Release + +To create a new release: + +1. **Create and push a tag:** + ```bash + git tag v1.0.0 + git push origin v1.0.0 + ``` + +2. The release workflow will automatically: + - Build binaries for all platforms + - Create archives + - Create a GitHub release with auto-generated release notes + - Upload all artifacts + +3. The released binaries will show proper version info: + ```bash + $ cloudamqp version + cloudamqp version v1.0.0 (2025-11-26) + https://github.com/cloudamqp/cli/releases/tag/v1.0.0 + ``` + +## Manual Workflow Trigger + +The release workflow can also be triggered manually from the GitHub Actions UI: +1. Go to Actions tab +2. Select "Build Release" workflow +3. Click "Run workflow" +4. Select branch and click "Run workflow" + +## Verification + +Both workflows include verification steps: +- **CI**: Tests the built binary with `--help` and `version` commands +- **Release**: Attempts to run `version` command on non-Windows binaries (may skip for cross-compiled binaries) + +## Caching + +Both workflows use Go module caching to speed up builds: +- Cache key includes Go version and `go.sum` hash +- Cached paths: `~/.cache/go-build` and `~/go/pkg/mod` + +## Build Flags + +**CI builds:** +- Standard build flags +- Version information included + +**Release builds:** +- `-a`: Force rebuilding of packages +- `-installsuffix cgo`: Use suffix for cgo-enabled builds +- `-ldflags="-w -s"`: Strip debug info and symbol tables (smaller binaries) +- Version information via `-X` flags +- `CGO_ENABLED=0`: Disable cgo for static binaries + +## Artifacts + +**CI Workflow:** +- Uploads test coverage to Codecov + +**Release Workflow:** +- Individual binaries (30 days retention) +- Release archives (90 days retention) +- GitHub Release assets (permanent) + +## Troubleshooting + +If version information is not showing correctly in releases: + +1. Check that tags are pushed: `git push origin --tags` +2. Verify tag format: Must start with `v` (e.g., `v1.0.0`) +3. Check workflow logs for version extraction step +4. Ensure no local modifications (avoid `-dirty` in version) + +## Local Testing + +To test version embedding locally: + +```bash +# Using Make (recommended) +make build + +# Manual +VERSION=v1.0.0 +BUILD_DATE=$(date -u +"%Y-%m-%d") +GIT_COMMIT=$(git rev-parse --short HEAD) + +go build -ldflags "\ + -X cloudamqp-cli/cmd.Version=$VERSION \ + -X cloudamqp-cli/cmd.BuildDate=$BUILD_DATE \ + -X cloudamqp-cli/cmd.GitCommit=$GIT_COMMIT" \ + -o cloudamqp . +``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbe030b..ccfd05a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,8 +53,24 @@ jobs: with: file: ./coverage.out + - name: Extract version information + id: version + run: | + VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") + BUILD_DATE=$(date -u +"%Y-%m-%d") + GIT_COMMIT=$(git rev-parse --short HEAD) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "build_date=$BUILD_DATE" >> $GITHUB_OUTPUT + echo "git_commit=$GIT_COMMIT" >> $GITHUB_OUTPUT + echo "Building with version: $VERSION (commit: $GIT_COMMIT, date: $BUILD_DATE)" + - name: Build - run: go build -v -o cloudamqp . + run: | + go build -v \ + -ldflags="-X cloudamqp-cli/cmd.Version=${{ steps.version.outputs.version }} -X cloudamqp-cli/cmd.BuildDate=${{ steps.version.outputs.build_date }} -X cloudamqp-cli/cmd.GitCommit=${{ steps.version.outputs.git_commit }}" \ + -o cloudamqp . - name: Test binary - run: ./cloudamqp --help \ No newline at end of file + run: | + ./cloudamqp --help + ./cloudamqp version \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c59b0f0..43fb134 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,13 +55,42 @@ jobs: - name: Download dependencies run: go mod download + - name: Extract version information + id: version + run: | + # Extract version from tag (remove 'v' prefix if present) + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + VERSION="${GITHUB_REF#refs/tags/}" + else + VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Get build date in UTC + BUILD_DATE=$(date -u +"%Y-%m-%d") + echo "build_date=$BUILD_DATE" >> $GITHUB_OUTPUT + + # Get short commit hash + GIT_COMMIT=$(git rev-parse --short HEAD) + echo "git_commit=$GIT_COMMIT" >> $GITHUB_OUTPUT + + echo "Building version: $VERSION (commit: $GIT_COMMIT, date: $BUILD_DATE)" + - name: Build binary env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} CGO_ENABLED: 0 run: | - go build -a -installsuffix cgo -ldflags="-w -s" -o cloudamqp-${{ matrix.name }}${{ matrix.ext }} . + go build -a -installsuffix cgo \ + -ldflags="-w -s -X cloudamqp-cli/cmd.Version=${{ steps.version.outputs.version }} -X cloudamqp-cli/cmd.BuildDate=${{ steps.version.outputs.build_date }} -X cloudamqp-cli/cmd.GitCommit=${{ steps.version.outputs.git_commit }}" \ + -o cloudamqp-${{ matrix.name }}${{ matrix.ext }} . + + - name: Verify version (Linux/macOS only) + if: matrix.goos != 'windows' + run: | + chmod +x cloudamqp-${{ matrix.name }}${{ matrix.ext }} + ./cloudamqp-${{ matrix.name }}${{ matrix.ext }} version || echo "Version verification skipped for cross-compiled binary" - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/BUILD_VERSION.md b/BUILD_VERSION.md new file mode 100644 index 0000000..027380a --- /dev/null +++ b/BUILD_VERSION.md @@ -0,0 +1,119 @@ +# Building with Version Information + +The `cloudamqp` CLI supports version information that can be set at build time using Go's `-ldflags`. + +## Version Variables + +The following variables are available in `cmd/version.go`: +- `Version` - The version number (e.g., "1.0.0") +- `BuildDate` - The build date (e.g., "2025-11-25") +- `GitCommit` - The git commit hash (optional) + +## Build Examples + +### Using Make (Recommended) + +The Makefile automatically extracts version information from git: + +```bash +# Build with automatic git version +make build +``` + +Output: +``` +$ ./cloudamqp version +cloudamqp version v0.1.0-8-g285bd2b (2025-11-26) +https://github.com/cloudamqp/cli/releases/tag/v0.1.0-8-g285bd2b +``` + +Show version information before building: +```bash +make version-info +``` + +Override version manually: +```bash +make build VERSION=1.0.0 BUILD_DATE=2025-11-25 +``` + +### Manual Build (without Make) + +#### Development Build +```bash +go build -o cloudamqp-cli +``` +Output: +``` +$ cloudamqp-cli version +cloudamqp version dev (development build) +``` + +#### Release Build +```bash +go build -ldflags "\ + -X cloudamqp-cli/cmd.Version=1.0.0 \ + -X cloudamqp-cli/cmd.BuildDate=2025-11-25 \ + -X cloudamqp-cli/cmd.GitCommit=abc123" \ + -o cloudamqp-cli +``` +Output: +``` +$ cloudamqp-cli version +cloudamqp version 1.0.0 (2025-11-25) +https://github.com/cloudamqp/cli/releases/tag/v1.0.0 +``` + +### Using Git Information +```bash +VERSION=$(git describe --tags --always --dirty) +BUILD_DATE=$(date -u +"%Y-%m-%d") +GIT_COMMIT=$(git rev-parse --short HEAD) + +go build -ldflags "\ + -X cloudamqp-cli/cmd.Version=${VERSION} \ + -X cloudamqp-cli/cmd.BuildDate=${BUILD_DATE} \ + -X cloudamqp-cli/cmd.GitCommit=${GIT_COMMIT}" \ + -o cloudamqp-cli +``` + +## Usage + +The version can be displayed in two ways: + +```bash +# Using the version command +cloudamqp version + +# Using the --version or -v flag +cloudamqp --version +cloudamqp -v +``` + +Both produce identical output, similar to GitHub CLI (`gh`). + +## CI/CD Integration + +### GitHub Actions Example +```yaml +- name: Build + run: | + VERSION=${GITHUB_REF#refs/tags/} + BUILD_DATE=$(date -u +"%Y-%m-%d") + GIT_COMMIT=${GITHUB_SHA::7} + + go build -ldflags "\ + -X cloudamqp-cli/cmd.Version=${VERSION} \ + -X cloudamqp-cli/cmd.BuildDate=${BUILD_DATE} \ + -X cloudamqp-cli/cmd.GitCommit=${GIT_COMMIT}" \ + -o cloudamqp-cli +``` + +### GoReleaser Example +```yaml +builds: + - ldflags: + - -X cloudamqp-cli/cmd.Version={{.Version}} + - -X cloudamqp-cli/cmd.BuildDate={{.Date}} + - -X cloudamqp-cli/cmd.GitCommit={{.ShortCommit}} +``` diff --git a/Makefile b/Makefile index 9a2b039..5503b0c 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,16 @@ BINARY_NAME=cloudamqp GO_BUILD_FLAGS=-v GO_TEST_FLAGS=-v -GO_LDFLAGS= + +# Version information (automatically extracted from git) +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +BUILD_DATE ?= $(shell date -u +"%Y-%m-%d") +GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") + +# Build with version information +GO_LDFLAGS=-X cloudamqp-cli/cmd.Version=$(VERSION) \ + -X cloudamqp-cli/cmd.BuildDate=$(BUILD_DATE) \ + -X cloudamqp-cli/cmd.GitCommit=$(GIT_COMMIT) # Default target .PHONY: all @@ -55,15 +64,23 @@ install: build # Build for multiple platforms .PHONY: build-all build-all: - GOOS=linux GOARCH=amd64 go build -o $(BINARY_NAME)-linux-amd64 . - GOOS=darwin GOARCH=amd64 go build -o $(BINARY_NAME)-darwin-amd64 . - GOOS=darwin GOARCH=arm64 go build -o $(BINARY_NAME)-darwin-arm64 . - GOOS=windows GOARCH=amd64 go build -o $(BINARY_NAME)-windows-amd64.exe . + GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o $(BINARY_NAME)-linux-amd64 . + GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o $(BINARY_NAME)-darwin-amd64 . + GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o $(BINARY_NAME)-darwin-arm64 . + GOOS=windows GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o $(BINARY_NAME)-windows-amd64.exe . # Development workflow .PHONY: dev dev: fmt vet test build +# Show version information that will be used +.PHONY: version-info +version-info: + @echo "Version Info:" + @echo " VERSION: $(VERSION)" + @echo " BUILD_DATE: $(BUILD_DATE)" + @echo " GIT_COMMIT: $(GIT_COMMIT)" + openapi.yaml: curl -O https://docs.cloudamqp.com/openapi.yaml @@ -74,7 +91,7 @@ openapi-instance.yaml: .PHONY: help help: @echo "Available targets:" - @echo " build - Build the binary" + @echo " build - Build the binary with version info" @echo " test - Run tests" @echo " integration-test - Run integration tests" @echo " clean - Clean build artifacts" @@ -82,6 +99,10 @@ help: @echo " vet - Vet code" @echo " deps - Install dependencies" @echo " install - Install the binary" - @echo " build-all - Build for multiple platforms" + @echo " build-all - Build for multiple platforms with version info" @echo " dev - Run development workflow (fmt, vet, test, build)" + @echo " version-info - Show version information that will be used in build" @echo " help - Show this help message" + @echo "" + @echo "Version information is automatically extracted from git." + @echo "Override with: make build VERSION=1.0.0 BUILD_DATE=2025-11-25" diff --git a/cmd/root.go b/cmd/root.go index 10c28bb..e4fe996 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,11 +1,23 @@ package cmd import ( + "fmt" + "strings" + "github.com/spf13/cobra" ) var apiKey string +func getVersionString() string { + if Version == "dev" { + return fmt.Sprintf("%s (development build)", Version) + } + // Strip leading 'v' if present to avoid double 'v' in URL + versionTag := strings.TrimPrefix(Version, "v") + return fmt.Sprintf("%s (%s)\nhttps://github.com/cloudamqp/cli/releases/tag/v%s", Version, BuildDate, versionTag) +} + var rootCmd = &cobra.Command{ Use: "cloudamqp", Short: "CloudAMQP CLI for managing instances and VPCs", @@ -19,6 +31,7 @@ The CLI will look for your API key in the following order: 3. If neither exists, you will be prompted to enter it Instance API keys are automatically saved when using 'instance get' command.`, + Version: getVersionString(), } func Execute() error { @@ -26,6 +39,9 @@ func Execute() error { } func init() { + // Set custom version template to match gh style + rootCmd.SetVersionTemplate("cloudamqp version {{.Version}}\n") + rootCmd.AddCommand(instanceCmd) rootCmd.AddCommand(vpcCmd) rootCmd.AddCommand(regionsCmd) diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..f727ffe --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +var ( + // Version information - these can be set at build time with -ldflags + Version = "dev" + BuildDate = "unknown" + GitCommit = "none" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Show cloudamqp version information", + Long: `Display the version number, build date, and release information for cloudamqp CLI.`, + Run: func(cmd *cobra.Command, args []string) { + if Version == "dev" { + fmt.Printf("cloudamqp version %s (development build)\n", Version) + } else { + fmt.Printf("cloudamqp version %s (%s)\n", Version, BuildDate) + // Strip leading 'v' if present to avoid double 'v' in URL + versionTag := strings.TrimPrefix(Version, "v") + fmt.Printf("https://github.com/cloudamqp/cli/releases/tag/v%s\n", versionTag) + } + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +}