Skip to content

Commit 44eee29

Browse files
authored
Add calendar-versioned release automation (#41)
1 parent fd41452 commit 44eee29

8 files changed

Lines changed: 291 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ on:
44
push:
55
branches:
66
- main
7+
tags:
8+
- "*.*.*"
79
pull_request:
810
workflow_dispatch:
911

@@ -30,6 +32,20 @@ jobs:
3032
with:
3133
version-file: .tool-versions
3234

35+
goreleaser-check:
36+
name: GoReleaser Check
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Checkout code
40+
uses: actions/checkout@v6
41+
42+
- name: Run GoReleaser check
43+
uses: goreleaser/goreleaser-action@v6
44+
with:
45+
distribution: goreleaser
46+
version: "~> v2"
47+
args: check
48+
3349
test-unit:
3450
name: Unit Tests
3551
runs-on: ubuntu-latest
@@ -102,3 +118,42 @@ jobs:
102118
name: Integration Test Results (${{ matrix.os }})
103119
path: test-integration-results.xml
104120
reporter: java-junit
121+
122+
release:
123+
name: Build and Publish Release
124+
if: startsWith(github.ref, 'refs/tags/')
125+
needs:
126+
- lint
127+
- goreleaser-check
128+
- test-unit
129+
- test-integration
130+
runs-on: ubuntu-latest
131+
permissions:
132+
contents: write
133+
steps:
134+
- name: Checkout code
135+
uses: actions/checkout@v6
136+
with:
137+
fetch-depth: 0
138+
139+
- name: Set up Go
140+
uses: actions/setup-go@v6
141+
with:
142+
go-version-file: go.mod
143+
cache-dependency-path: go.sum
144+
145+
- name: Validate CalVer tag
146+
run: |
147+
if [[ ! "${GITHUB_REF_NAME}" =~ ^[0-9]{4}\.[1-9][0-9]*\.[0-9]+$ ]]; then
148+
echo "Tag '${GITHUB_REF_NAME}' does not match CalVer format YYYY.M.patch"
149+
exit 1
150+
fi
151+
152+
- name: Run GoReleaser
153+
uses: goreleaser/goreleaser-action@v6
154+
with:
155+
distribution: goreleaser
156+
version: "~> v2"
157+
args: release --clean
158+
env:
159+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
env:
35+
GIT_USER_NAME: ${{ env.git_user_name }}
36+
GIT_USER_EMAIL: ${{ env.git_user_email }}
37+
run: |
38+
git config user.name "${GIT_USER_NAME}"
39+
git config user.email "${GIT_USER_EMAIL}"
40+
41+
- name: Fetch tags
42+
run: git fetch --tags --force
43+
44+
- name: Compute next CalVer tag
45+
id: next_tag
46+
run: |
47+
set -euo pipefail
48+
year="$(date -u +%Y)"
49+
month="$(date -u +%-m)"
50+
prefix="${year}.${month}."
51+
52+
latest="$(git tag --list "${prefix}*" --sort=-v:refname | grep -E "^${year}\.${month}\.[0-9]+$" | head -n 1 || true)"
53+
if [[ -z "${latest}" ]]; then
54+
patch=0
55+
else
56+
patch="$(( ${latest##*.} + 1 ))"
57+
fi
58+
59+
tag="${year}.${month}.${patch}"
60+
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
61+
echo "Tag ${tag} already exists"
62+
exit 1
63+
fi
64+
65+
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
66+
67+
- name: Create and push tag
68+
run: |
69+
tag="${{ steps.next_tag.outputs.tag }}"
70+
git tag -a "${tag}" -m "Release ${tag}"
71+
git push origin "${tag}"
72+
73+
- name: Print created tag
74+
run: echo "Created tag ${{ steps.next_tag.outputs.tag }}"

.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: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,63 @@
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 uses the CI workflow plus one helper workflow:
30+
31+
1. `Create Release Tag` (`.github/workflows/create-release-tag.yml`)
32+
2. `LSTK CI` (`.github/workflows/ci.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 `LSTK CI`.
39+
4. In `LSTK CI`, the `release` job runs only for tag refs and publishes the GitHub release with GoReleaser.
40+
41+
## Published Artifacts
42+
Each release publishes binaries for:
43+
44+
- `linux/amd64`
45+
- `linux/arm64`
46+
- `darwin/amd64`
47+
- `darwin/arm64`
48+
- `windows/amd64`
49+
- `windows/arm64`
50+
51+
Archive formats:
52+
53+
- `tar.gz` for Linux and macOS
54+
- `zip` for Windows
55+
56+
Each release also includes `checksums.txt`.
57+
58+
## Local Dry Run
59+
To validate release packaging locally without publishing:
60+
61+
```bash
62+
goreleaser release --snapshot --clean
63+
```

cmd/root.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,19 @@ 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+
// Version should be side-effect free and must not create/read user config.
26+
if cmd.Name() == "version" {
27+
return nil
28+
}
2129
return config.Init()
2230
},
2331
Run: func(cmd *cobra.Command, args []string) {
@@ -35,6 +43,8 @@ var rootCmd = &cobra.Command{
3543
}
3644

3745
func init() {
46+
rootCmd.Version = version
47+
rootCmd.SetVersionTemplate(versionLine() + "\n")
3848
rootCmd.AddCommand(startCmd)
3949
}
4050

cmd/version.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
RunE: func(cmd *cobra.Command, args []string) error {
14+
_, err := fmt.Fprintln(cmd.OutOrStdout(), versionLine())
15+
return err
16+
},
17+
}
18+
19+
func init() {
20+
rootCmd.AddCommand(versionCmd)
21+
}
22+
23+
func versionLine() string {
24+
return fmt.Sprintf("lstk %s (%s, %s)", version, commit, buildDate)
25+
}

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)