diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..aaacff7 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,31 @@ +name: Release + +on: + release: + types: + - published + +permissions: + contents: write + +jobs: + goreleaser: + name: GoReleaser + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version-file: go.mod + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..0518791 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,100 @@ +version: 2 + +project_name: sumup + +release: + prerelease: auto + +before: + hooks: + - go mod download + +builds: + - id: macos + main: ./cmd/sumup/main.go + binary: sumup + env: + - CGO_ENABLED=0 + goos: + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X github.com/sumup/sumup-cli/internal/buildinfo.Version={{ .Version }} -X github.com/sumup/sumup-cli/internal/buildinfo.Commit={{ .Commit }} -X github.com/sumup/sumup-cli/internal/buildinfo.Date={{ .Date }} + + - id: linux + main: ./cmd/sumup/main.go + binary: sumup + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X github.com/sumup/sumup-cli/internal/buildinfo.Version={{ .Version }} -X github.com/sumup/sumup-cli/internal/buildinfo.Commit={{ .Commit }} -X github.com/sumup/sumup-cli/internal/buildinfo.Date={{ .Date }} + + - id: windows + main: ./cmd/sumup/main.go + binary: sumup + env: + - CGO_ENABLED=0 + goos: + - windows + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X github.com/sumup/sumup-cli/internal/buildinfo.Version={{ .Version }} -X github.com/sumup/sumup-cli/internal/buildinfo.Commit={{ .Commit }} -X github.com/sumup/sumup-cli/internal/buildinfo.Date={{ .Date }} + + # Enable when signing credentials are configured in CI. + # hooks: + # post: + # - cmd: >- + # {{ if eq .Os "darwin" }}./scripts/sign-macos.sh '{{ .Path }}'{{ else }}echo{{ end }} + # output: true + # - cmd: >- + # {{ if eq .Os "windows" }}pwsh ./scripts/sign-windows.ps1 '{{ .Path }}'{{ else }}echo{{ end }} + # output: true + +archives: + - id: linux-archive + ids: [linux] + formats: [tar.gz] + name_template: >- + {{ .ProjectName }}_{{ .Version }}_linux_ + {{- if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }} + files: + - LICENSE + - README.md + - id: macos-archive + ids: [macos] + formats: [tar.gz] + name_template: >- + {{ .ProjectName }}_{{ .Version }}_macOS_ + {{- if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }} + files: + - LICENSE + - README.md + - id: windows-archive + ids: [windows] + formats: [zip] + name_template: >- + {{ .ProjectName }}_{{ .Version }}_windows_ + {{- if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }} + files: + - LICENSE + - README.md + +checksum: + name_template: checksums.txt + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' diff --git a/Makefile b/Makefile index 5f74f30..8c5717c 100644 --- a/Makefile +++ b/Makefile @@ -31,3 +31,15 @@ vulncheck-sarif: ## Check for Vulnerabilities .PHONY: install-tools install-tools: # Install development dependencies go install golang.org/x/vuln/cmd/govulncheck@latest + +VERSION ?= dev +COMMIT ?= $(shell git rev-parse --short HEAD) +DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +LDFLAGS := -s -w \ + -X github.com/sumup/sumup-cli/internal/buildinfo.Version=$(VERSION) \ + -X github.com/sumup/sumup-cli/internal/buildinfo.Commit=$(COMMIT) \ + -X github.com/sumup/sumup-cli/internal/buildinfo.Date=$(DATE) + +.PHONY: build +build: ## Build CLI binary with build metadata + go build -ldflags "$(LDFLAGS)" -o ./bin/sumup ./cmd/sumup diff --git a/cmd/sumup/main.go b/cmd/sumup/main.go index 3f14318..238143d 100644 --- a/cmd/sumup/main.go +++ b/cmd/sumup/main.go @@ -8,6 +8,7 @@ import ( "github.com/urfave/cli/v3" "github.com/sumup/sumup-cli/internal/app" + "github.com/sumup/sumup-cli/internal/buildinfo" "github.com/sumup/sumup-cli/internal/commands" "github.com/sumup/sumup-cli/internal/display/message" ) @@ -16,6 +17,7 @@ func main() { cliApp := &cli.Command{ Name: "sumup", Usage: "Command line tool for the SumUp API", + Version: buildinfo.Short(), EnableShellCompletion: true, Flags: []cli.Flag{ &cli.StringFlag{ diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go new file mode 100644 index 0000000..910658e --- /dev/null +++ b/internal/buildinfo/buildinfo.go @@ -0,0 +1,79 @@ +package buildinfo + +import ( + "fmt" + "runtime/debug" + "strings" +) + +var ( + // Version is the semantic version of the CLI. + Version = "dev" + // Commit is the VCS revision used for the build. + Commit = "unknown" + // Date is the build timestamp in UTC (RFC3339 format). + Date = "unknown" +) + +func info() (version, commit, date string, dirty bool) { + version = Version + commit = Commit + date = Date + + bi, ok := debug.ReadBuildInfo() + if !ok { + return version, commit, date, false + } + + if (version == "dev" || version == "(devel)") && bi.Main.Version != "" && bi.Main.Version != "(devel)" { + version = bi.Main.Version + } + + for _, setting := range bi.Settings { + switch setting.Key { + case "vcs.revision": + if commit == "unknown" && setting.Value != "" { + commit = setting.Value + } + case "vcs.time": + if date == "unknown" && setting.Value != "" { + date = setting.Value + } + case "vcs.modified": + dirty = setting.Value == "true" + } + } + + return version, commit, date, dirty +} + +// Short returns a concise version string suitable for --version output. +func Short() string { + version, commit, _, dirty := info() + + var parts []string + parts = append(parts, version) + if commit != "" && commit != "unknown" { + shortCommit := commit + if len(shortCommit) > 7 { + shortCommit = shortCommit[:7] + } + parts = append(parts, shortCommit) + } + if dirty { + parts = append(parts, "dirty") + } + + return strings.Join(parts, " ") +} + +// Long returns full build details. +func Long() string { + version, commit, date, _ := info() + return fmt.Sprintf( + "Version: %s\nCommit: %s\nDate: %s", + version, + commit, + date, + ) +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 5234969..766cc9c 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -14,6 +14,7 @@ import ( "github.com/sumup/sumup-cli/internal/commands/receipts" "github.com/sumup/sumup-cli/internal/commands/roles" "github.com/sumup/sumup-cli/internal/commands/transactions" + "github.com/sumup/sumup-cli/internal/commands/version" ) // All returns the list of resource commands exposed by the CLI. @@ -30,5 +31,6 @@ func All() []*cli.Command { receipts.NewCommand(), roles.NewCommand(), transactions.NewCommand(), + version.NewCommand(), } } diff --git a/internal/commands/version/version.go b/internal/commands/version/version.go new file mode 100644 index 0000000..e45567c --- /dev/null +++ b/internal/commands/version/version.go @@ -0,0 +1,22 @@ +package version + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v3" + + "github.com/sumup/sumup-cli/internal/buildinfo" +) + +// NewCommand returns the version command. +func NewCommand() *cli.Command { + return &cli.Command{ + Name: "version", + Usage: "Print CLI build information", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println(buildinfo.Long()) + return nil + }, + } +} diff --git a/scripts/sign-macos.sh b/scripts/sign-macos.sh new file mode 100755 index 0000000..bb21267 --- /dev/null +++ b/scripts/sign-macos.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +binary_path="${1:-}" +if [[ -z "$binary_path" ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# Placeholder script for future macOS signing and notarization. +# Expected env vars once enabled: +# - APPLE_CERT_BASE64 +# - APPLE_CERT_PASSWORD +# - APPLE_TEAM_ID +# - APPLE_ID +# - APPLE_APP_SPECIFIC_PASSWORD +# +# Example (to enable later): +# security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain +# echo "$APPLE_CERT_BASE64" | base64 --decode > cert.p12 +# security import cert.p12 -k build.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign +# codesign --force --timestamp --options runtime --sign "Developer ID Application: ..." "$binary_path" + +echo "macOS signing is currently disabled. Skipping: $binary_path" diff --git a/scripts/sign-windows.ps1 b/scripts/sign-windows.ps1 new file mode 100644 index 0000000..9afa5a4 --- /dev/null +++ b/scripts/sign-windows.ps1 @@ -0,0 +1,17 @@ +param( + [Parameter(Mandatory = $true)] + [string]$BinaryPath +) + +$ErrorActionPreference = "Stop" + +# Placeholder script for future Windows Authenticode signing. +# Expected env vars once enabled: +# - WINDOWS_CERT_BASE64 +# - WINDOWS_CERT_PASSWORD +# +# Example (to enable later): +# [IO.File]::WriteAllBytes("cert.pfx", [Convert]::FromBase64String($env:WINDOWS_CERT_BASE64)) +# signtool sign /f cert.pfx /p $env:WINDOWS_CERT_PASSWORD /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 $BinaryPath + +Write-Output "Windows signing is currently disabled. Skipping: $BinaryPath"