feat(release): version stamping and tag-triggered release workflow
--version/-V on the CLI, version in /api/health via Server.SetVersion, Makefile/Dockerfile ldflags stamping from git describe, and a release workflow on v* tags: both binaries for linux/darwin (amd64+arm64) and windows/amd64 with SHA256SUMS attached to the Gitea release (idempotent — reuses an existing hand-written release and skips already-uploaded assets), plus version-tagged Docker images. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
391d849a5f
commit
94e41fe5b5
@@ -0,0 +1,132 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Builds release artifacts when a version tag (v*) is pushed:
|
||||||
|
# - cross-compiled CLI + server binaries attached to the Gitea release
|
||||||
|
# - version-tagged docker images for the CLI and web server
|
||||||
|
# The owner usually creates the Gitea release by hand with notes; this
|
||||||
|
# workflow attaches assets to it (creating a bare release only when none
|
||||||
|
# exists) and skips assets that are already attached, so re-runs are safe.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
binaries:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Build release binaries
|
||||||
|
env:
|
||||||
|
GOTOOLCHAIN: local
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
mkdir -p dist
|
||||||
|
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
|
||||||
|
GOOS="${target%/*}"
|
||||||
|
GOARCH="${target#*/}"
|
||||||
|
EXT=""
|
||||||
|
[ "$GOOS" = "windows" ] && EXT=".exe"
|
||||||
|
OUT="dist/${GOOS}_${GOARCH}"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.version=${TAG}" \
|
||||||
|
-o "${OUT}/exploredns${EXT}" ./cmd/exploredns
|
||||||
|
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \
|
||||||
|
go build -trimpath -ldflags "-s -w -X main.version=${TAG}" \
|
||||||
|
-o "${OUT}/exploredns-server${EXT}" ./cmd/server
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
(cd "$OUT" && zip -q "../exploredns_${TAG}_${GOOS}_${GOARCH}.zip" exploredns.exe exploredns-server.exe)
|
||||||
|
else
|
||||||
|
tar -czf "dist/exploredns_${TAG}_${GOOS}_${GOARCH}.tar.gz" -C "$OUT" exploredns exploredns-server
|
||||||
|
fi
|
||||||
|
rm -rf "$OUT"
|
||||||
|
done
|
||||||
|
(cd dist && sha256sum -- * > SHA256SUMS)
|
||||||
|
ls -l dist
|
||||||
|
|
||||||
|
- name: Attach assets to Gitea release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
API: ${{ github.server_url }}/api/v1
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
AUTH="Authorization: token ${TOKEN}"
|
||||||
|
|
||||||
|
# Look up the release for this tag; create a bare one only when
|
||||||
|
# none exists (the owner writes release notes by hand).
|
||||||
|
STATUS=$(curl -sS -o release.json -w '%{http_code}' -H "$AUTH" \
|
||||||
|
"${API}/repos/${REPO}/releases/tags/${TAG}")
|
||||||
|
if [ "$STATUS" = "404" ]; then
|
||||||
|
curl -sS -f -o release.json -H "$AUTH" -H 'Content-Type: application/json' \
|
||||||
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}" \
|
||||||
|
"${API}/repos/${REPO}/releases"
|
||||||
|
elif [ "$STATUS" != "200" ]; then
|
||||||
|
echo "release lookup failed with HTTP ${STATUS}" >&2
|
||||||
|
cat release.json >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
RELEASE_ID=$(jq -r '.id' release.json)
|
||||||
|
echo "release id: ${RELEASE_ID}"
|
||||||
|
|
||||||
|
# Existing asset names, so re-runs skip instead of failing.
|
||||||
|
curl -sS -f -H "$AUTH" \
|
||||||
|
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets" \
|
||||||
|
| jq -r '.[].name' > existing.txt
|
||||||
|
|
||||||
|
for f in dist/*; do
|
||||||
|
NAME=$(basename "$f")
|
||||||
|
if grep -Fxq "$NAME" existing.txt; then
|
||||||
|
echo "skip ${NAME} (already attached)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "upload ${NAME}"
|
||||||
|
curl -sS -f -o /dev/null -H "$AUTH" \
|
||||||
|
-F "attachment=@${f}" \
|
||||||
|
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${NAME}"
|
||||||
|
done
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.hansenits.com.au
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push CLI image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.cli
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ github.ref_name }}
|
||||||
|
tags: |
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-cli:latest
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-cli:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push web image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.web
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ github.ref_name }}
|
||||||
|
tags: |
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-web:latest
|
||||||
|
gitea.hansenits.com.au/hits/exploredns-web:${{ github.ref_name }}
|
||||||
+2
-1
@@ -8,7 +8,8 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/exploredns ./cmd/exploredns
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /out/exploredns ./cmd/exploredns
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,8 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /out/server ./cmd/server
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ SERVER_BINARY_NAME=exploredns-server
|
|||||||
BUILD_DIR=bin
|
BUILD_DIR=bin
|
||||||
GO=go
|
GO=go
|
||||||
GOFLAGS=-v
|
GOFLAGS=-v
|
||||||
|
VERSION?=$(shell git describe --tags --always 2>/dev/null || echo dev)
|
||||||
|
LDFLAGS=-ldflags "-X main.version=$(VERSION)"
|
||||||
|
|
||||||
.PHONY: build build-server build-all test lint clean cover deploy deploy-status
|
.PHONY: build build-server build-all test lint clean cover deploy deploy-status
|
||||||
|
|
||||||
build:
|
build:
|
||||||
$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/exploredns
|
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/exploredns
|
||||||
|
|
||||||
build-server:
|
build-server:
|
||||||
$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(SERVER_BINARY_NAME) ./cmd/server
|
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BUILD_DIR)/$(SERVER_BINARY_NAME) ./cmd/server
|
||||||
|
|
||||||
build-all: build build-server
|
build-all: build build-server
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import (
|
|||||||
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/traverse"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// version is stamped at build time via -ldflags "-X main.version=...".
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.DefaultConfig()
|
cfg := config.DefaultConfig()
|
||||||
|
|
||||||
@@ -42,6 +45,12 @@ func main() {
|
|||||||
flag.BoolVar(&dFlag, "debug", false, "Print debug diagnostics to stderr")
|
flag.BoolVar(&dFlag, "debug", false, "Print debug diagnostics to stderr")
|
||||||
flag.BoolVar(&ddFlag, "dd", false, "Like -d plus library-level debug")
|
flag.BoolVar(&ddFlag, "dd", false, "Like -d plus library-level debug")
|
||||||
|
|
||||||
|
// Version: long and short form share the same variable (mirrors
|
||||||
|
// dnstraverse's -V).
|
||||||
|
var showVersion bool
|
||||||
|
flag.BoolVar(&showVersion, "version", false, "Print version and exit")
|
||||||
|
flag.BoolVar(&showVersion, "V", false, "Print version and exit (shorthand)")
|
||||||
|
|
||||||
// Quiet: long and short form share the same variable.
|
// Quiet: long and short form share the same variable.
|
||||||
var quietVal bool
|
var quietVal bool
|
||||||
flag.BoolVar(&quietVal, "quiet", cfg.Quiet, "Suppress the header block")
|
flag.BoolVar(&quietVal, "quiet", cfg.Quiet, "Suppress the header block")
|
||||||
@@ -69,6 +78,11 @@ func main() {
|
|||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if showVersion {
|
||||||
|
fmt.Printf("exploredns %s\n", version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
|
|
||||||
cfg.QueryType = *queryType
|
cfg.QueryType = *queryType
|
||||||
|
|||||||
+5
-1
@@ -13,17 +13,21 @@ import (
|
|||||||
"gitea.hansenits.com.au/hits/ExploreDNS/web/api"
|
"gitea.hansenits.com.au/hits/ExploreDNS/web/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// version is stamped at build time via -ldflags "-X main.version=...".
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := flag.String("addr", ":8080", "listen address (host:port)")
|
addr := flag.String("addr", ":8080", "listen address (host:port)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
srv := api.NewServer(*addr)
|
srv := api.NewServer(*addr)
|
||||||
|
srv.SetVersion(version)
|
||||||
if err := srv.Start(); err != nil {
|
if err := srv.Start(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("ExploreDNS API server listening on %s", srv.Addr())
|
log.Printf("ExploreDNS API server %s listening on %s", version, srv.Addr())
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ func PrintUsage() {
|
|||||||
{"--show-results", "Show the results (default true)"},
|
{"--show-results", "Show the results (default true)"},
|
||||||
{"--show-summary-results", "Show the summary results (default true)"},
|
{"--show-summary-results", "Show the summary results (default true)"},
|
||||||
}},
|
}},
|
||||||
|
{"General Options", [][2]string{
|
||||||
|
{"--version, -V", "Print version and exit"},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, group := range flagGroups {
|
for _, group := range flagGroups {
|
||||||
|
|||||||
+14
-4
@@ -27,14 +27,23 @@ var staticFiles embed.FS
|
|||||||
|
|
||||||
// Server is the HTTP API server.
|
// Server is the HTTP API server.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
addr string
|
addr string
|
||||||
srv *http.Server
|
version string
|
||||||
cancel context.CancelFunc
|
srv *http.Server
|
||||||
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new Server that listens on addr (e.g. ":8080").
|
// NewServer creates a new Server that listens on addr (e.g. ":8080").
|
||||||
func NewServer(addr string) *Server {
|
func NewServer(addr string) *Server {
|
||||||
return &Server{addr: addr}
|
return &Server{addr: addr, version: "dev"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVersion records the build version reported by GET /api/health.
|
||||||
|
// Call before Start; empty values are ignored.
|
||||||
|
func (s *Server) SetVersion(v string) {
|
||||||
|
if v != "" {
|
||||||
|
s.version = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start builds the HTTP handler, begins listening, and returns when the
|
// Start builds the HTTP handler, begins listening, and returns when the
|
||||||
@@ -44,6 +53,7 @@ func (s *Server) Start() error {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
s.cancel = cancel
|
s.cancel = cancel
|
||||||
h := newHandler(ctx)
|
h := newHandler(ctx)
|
||||||
|
h.version = s.version
|
||||||
|
|
||||||
sub, err := fs.Sub(staticFiles, "static")
|
sub, err := fs.Sub(staticFiles, "static")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user