Skip to content

Commit 247b484

Browse files
committed
Add calendar-versioned release automation
1 parent fd41452 commit 247b484

9 files changed

Lines changed: 293 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ jobs:
3030
with:
3131
version-file: .tool-versions
3232

33+
goreleaser-check:
34+
name: GoReleaser Check
35+
runs-on: ubuntu-latest
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v6
39+
40+
- name: Run GoReleaser check
41+
uses: goreleaser/goreleaser-action@v6
42+
with:
43+
distribution: goreleaser
44+
version: "~> v2"
45+
args: check
46+
3347
test-unit:
3448
name: Unit Tests
3549
runs-on: ubuntu-latest
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Create Release Tag
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
release_ref:
7+
description: Git ref to tag
8+
required: true
9+
default: main
10+
11+
permissions:
12+
contents: write
13+
14+
env:
15+
git_user_name: localstack[bot]
16+
git_user_email: localstack-bot@users.noreply.github.com
17+
18+
concurrency:
19+
group: create-release-tag
20+
cancel-in-progress: false
21+
22+
jobs:
23+
create-tag:
24+
name: Create next CalVer tag
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v6
29+
with:
30+
ref: ${{ inputs.release_ref }}
31+
fetch-depth: 0
32+
33+
- name: Configure Git user
34+
run: |
35+
git config user.name "${git_user_name}"
36+
git config user.email "${git_user_email}"
37+
38+
- name: Fetch tags
39+
run: git fetch --tags --force
40+
41+
- name: Compute next CalVer tag
42+
id: next_tag
43+
run: |
44+
set -euo pipefail
45+
year="$(date -u +%Y)"
46+
month="$(date -u +%-m)"
47+
prefix="${year}.${month}."
48+
49+
latest="$(git tag --list "${prefix}*" --sort=-v:refname | grep -E "^${year}\.${month}\.[0-9]+$" | head -n 1 || true)"
50+
if [[ -z "${latest}" ]]; then
51+
patch=0
52+
else
53+
patch="$(( ${latest##*.} + 1 ))"
54+
fi
55+
56+
tag="${year}.${month}.${patch}"
57+
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
58+
echo "Tag ${tag} already exists"
59+
exit 1
60+
fi
61+
62+
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
63+
64+
- name: Create and push tag
65+
run: |
66+
tag="${{ steps.next_tag.outputs.tag }}"
67+
git tag -a "${tag}" -m "Release ${tag}"
68+
git push origin "${tag}"
69+
70+
- name: Print created tag
71+
run: echo "Created tag ${{ steps.next_tag.outputs.tag }}"

.github/workflows/release.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "*.*.*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Build and Publish Release
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v6
18+
with:
19+
fetch-depth: 0
20+
21+
- name: Set up Go
22+
uses: actions/setup-go@v6
23+
with:
24+
go-version-file: go.mod
25+
cache-dependency-path: go.sum
26+
27+
- name: Validate CalVer tag
28+
if: github.ref_type == 'tag'
29+
run: |
30+
if [[ ! "${GITHUB_REF_NAME}" =~ ^[0-9]{4}\.[1-9][0-9]*\.[0-9]+$ ]]; then
31+
echo "Tag '${GITHUB_REF_NAME}' does not match CalVer format YYYY.M.patch"
32+
exit 1
33+
fi
34+
35+
- name: Run unit tests
36+
run: go test ./...
37+
38+
- name: Run GoReleaser
39+
uses: goreleaser/goreleaser-action@v6
40+
with:
41+
distribution: goreleaser
42+
version: "~> v2"
43+
args: release --clean
44+
env:
45+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
bin/
2+
dist/
23
*.test
34

4-
.idea
5+
.idea

.goreleaser.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
version: 2
2+
3+
project_name: lstk
4+
5+
builds:
6+
- id: lstk
7+
main: ./main.go
8+
binary: lstk
9+
env:
10+
- CGO_ENABLED=0
11+
goos:
12+
- linux
13+
- darwin
14+
- windows
15+
goarch:
16+
- amd64
17+
- arm64
18+
flags:
19+
- -trimpath
20+
ldflags:
21+
- -s -w -X github.com/localstack/lstk/cmd.version={{ .Version }} -X github.com/localstack/lstk/cmd.commit={{ .Commit }} -X github.com/localstack/lstk/cmd.buildDate={{ .Date }}
22+
23+
archives:
24+
- id: lstk
25+
ids:
26+
- lstk
27+
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
28+
formats:
29+
- tar.gz
30+
format_overrides:
31+
- goos: windows
32+
formats:
33+
- zip
34+
35+
checksum:
36+
name_template: checksums.txt
37+
38+
changelog:
39+
use: github-native

README.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,62 @@
11
# lstk
2-
Localstack's new CLI 🌟 (v2)
2+
Localstack's new CLI (v2).
3+
4+
## Versioning
5+
`lstk` uses calendar versioning in a SemVer-compatible format:
6+
7+
- `YYYY.M.patch`
8+
- Example (current format): `2026.2.0`
9+
10+
Release tags are the source of truth for published versions.
11+
12+
## Version Output
13+
The CLI exposes version info through:
14+
15+
- `lstk version`
16+
- `lstk --version`
17+
18+
Output format:
19+
20+
- `lstk <version> (<commit>, <buildDate>)`
21+
22+
At local development time (without ldflags), defaults are:
23+
24+
- `version=dev`
25+
- `commit=none`
26+
- `buildDate=unknown`
27+
28+
## Releasing with GoReleaser
29+
Release automation is split into two workflows:
30+
31+
1. `Create Release Tag` (`.github/workflows/create-release-tag.yml`)
32+
2. `Release` (`.github/workflows/release.yml`)
33+
34+
How it works:
35+
36+
1. Manually run `Create Release Tag` from GitHub Actions (default ref: `main`).
37+
2. The workflow computes and pushes the next CalVer tag for the current UTC month.
38+
3. Pushing that tag triggers `Release`, which runs tests and publishes a GitHub Release using GoReleaser.
39+
40+
## Published Artifacts
41+
Each release publishes binaries for:
42+
43+
- `linux/amd64`
44+
- `linux/arm64`
45+
- `darwin/amd64`
46+
- `darwin/arm64`
47+
- `windows/amd64`
48+
- `windows/arm64`
49+
50+
Archive formats:
51+
52+
- `tar.gz` for Linux and macOS
53+
- `zip` for Windows
54+
55+
Each release also includes `checksums.txt`.
56+
57+
## Local Dry Run
58+
To validate release packaging locally without publishing:
59+
60+
```bash
61+
goreleaser release --snapshot --clean
62+
```

cmd/root.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,22 @@ import (
1313
"github.com/spf13/cobra"
1414
)
1515

16+
var version = "dev"
17+
var commit = "none"
18+
var buildDate = "unknown"
19+
1620
var rootCmd = &cobra.Command{
1721
Use: "lstk",
1822
Short: "LocalStack CLI",
1923
Long: "lstk is the command-line interface for LocalStack.",
2024
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
25+
if cmd.Name() == "version" {
26+
return nil
27+
}
28+
29+
if showVersion, err := cmd.Flags().GetBool("version"); err == nil && showVersion {
30+
return nil
31+
}
2132
return config.Init()
2233
},
2334
Run: func(cmd *cobra.Command, args []string) {
@@ -35,6 +46,8 @@ var rootCmd = &cobra.Command{
3546
}
3647

3748
func init() {
49+
rootCmd.Version = version
50+
rootCmd.SetVersionTemplate(versionLine() + "\n")
3851
rootCmd.AddCommand(startCmd)
3952
}
4053

cmd/version.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var versionCmd = &cobra.Command{
10+
Use: "version",
11+
Short: "Print the lstk version",
12+
Long: "Print version information for the lstk binary.",
13+
Run: func(cmd *cobra.Command, args []string) {
14+
fmt.Fprintln(cmd.OutOrStdout(), versionLine())
15+
},
16+
}
17+
18+
func init() {
19+
rootCmd.AddCommand(versionCmd)
20+
}
21+
22+
func versionLine() string {
23+
return fmt.Sprintf("lstk %s (%s, %s)", version, commit, buildDate)
24+
}

cmd/version_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestVersionLine(t *testing.T) {
6+
originalVersion := version
7+
originalCommit := commit
8+
originalBuildDate := buildDate
9+
t.Cleanup(func() {
10+
version = originalVersion
11+
commit = originalCommit
12+
buildDate = originalBuildDate
13+
})
14+
15+
version = "2026.2.0"
16+
commit = "abc1234"
17+
buildDate = "2026-02-17T15:04:05Z"
18+
19+
got := versionLine()
20+
want := "lstk 2026.2.0 (abc1234, 2026-02-17T15:04:05Z)"
21+
if got != want {
22+
t.Fatalf("versionLine() = %q, want %q", got, want)
23+
}
24+
}

0 commit comments

Comments
 (0)