mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
ci: release chain — shared workflows, semver wording, source-only goreleaser, version stamping
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
name: Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
- "!main"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-checks:
|
||||||
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||||
|
with:
|
||||||
|
go-version: "1.25.x"
|
||||||
|
coverage-threshold: 75
|
||||||
|
secrets: inherit
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "**.go"
|
||||||
|
- "go.mod"
|
||||||
|
- "go.sum"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||||
|
with:
|
||||||
|
go-version: "1.25.x"
|
||||||
|
secrets: inherit
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
# Traefik plugins are source-only - no binary builds
|
||||||
|
# Traefik loads plugins via Yaegi interpreter at runtime
|
||||||
|
builds:
|
||||||
|
- skip: true
|
||||||
|
|
||||||
|
# Create source archive for GitHub releases
|
||||||
|
archives:
|
||||||
|
- formats: [tar.gz]
|
||||||
|
name_template: "{{ .ProjectName }}_v{{ .Version }}_source"
|
||||||
|
files:
|
||||||
|
- "*.go"
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
- .traefik.yml
|
||||||
|
- LICENSE*
|
||||||
|
- README*
|
||||||
|
# Exclude test files and non-shipped dirs from release archive
|
||||||
|
- "!**/*_test.go"
|
||||||
|
- "!testdata/**"
|
||||||
|
- "!tools/**"
|
||||||
|
- "!integration/**"
|
||||||
|
- "!docs/**"
|
||||||
|
- "!examples/**"
|
||||||
|
- "!vendor/**"
|
||||||
|
- "!.superpowers/**"
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "{{ .ProjectName }}_v{{ .Version }}_checksums.txt"
|
||||||
|
algorithm: sha256
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
- "^Merge"
|
||||||
|
- "^WIP"
|
||||||
|
- "^chore:"
|
||||||
|
|
||||||
|
release:
|
||||||
|
github:
|
||||||
|
owner: lukaszraczylo
|
||||||
|
name: traefik-llmgateway
|
||||||
|
name_template: "v{{ .Version }}"
|
||||||
|
draft: false
|
||||||
|
prerelease: auto
|
||||||
|
|
||||||
|
signs:
|
||||||
|
- cmd: cosign
|
||||||
|
signature: "${artifact}.sigstore.json"
|
||||||
|
args:
|
||||||
|
- sign-blob
|
||||||
|
- "--bundle=${signature}"
|
||||||
|
- "${artifact}"
|
||||||
|
- "--yes"
|
||||||
|
artifacts: checksum
|
||||||
|
output: true
|
||||||
@@ -8,14 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pluginVersion is the gateway's own build/plugin version string, surfaced
|
|
||||||
// read-only in GET /admin/api/overview (spec §4, v0.2). Bumped alongside
|
|
||||||
// the module's own release process — not read from anywhere else in the
|
|
||||||
// package, since Yaegi interpretation has no build-info mechanism to pull
|
|
||||||
// it from automatically. Keep in sync with the release tag: bump this
|
|
||||||
// alongside `git tag`, not before or after it.
|
|
||||||
const pluginVersion = "0.2.0"
|
|
||||||
|
|
||||||
// The three routes the read-only admin dashboard registers (spec §4,
|
// The three routes the read-only admin dashboard registers (spec §4,
|
||||||
// v0.2), matched only when adminEnabled(g.cfg) — see llmgateway.go's
|
// v0.2), matched only when adminEnabled(g.cfg) — see llmgateway.go's
|
||||||
// ServeHTTP dispatch.
|
// ServeHTTP dispatch.
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
version: 1
|
||||||
|
force:
|
||||||
|
existing: true
|
||||||
|
wording:
|
||||||
|
patch:
|
||||||
|
- patch-release
|
||||||
|
minor:
|
||||||
|
- minor-release
|
||||||
|
major:
|
||||||
|
- breaking
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package traefikllmgateway
|
||||||
|
|
||||||
|
// pluginVersion is the released version of this plugin. It is stamped at
|
||||||
|
// release time by ./workflow-prepare.sh (invoked by the shared go-release
|
||||||
|
// workflow before GoReleaser builds and tags), which rewrites the string
|
||||||
|
// below to the computed semver.
|
||||||
|
//
|
||||||
|
// Traefik runs this plugin under Yaegi, where the version cannot be
|
||||||
|
// resolved from build info at runtime (debug.ReadBuildInfo sees Traefik's
|
||||||
|
// build graph, not the interpreted plugin). This build-stamped constant is
|
||||||
|
// therefore the single source of truth for the version surfaced read-only
|
||||||
|
// in GET /admin/api/overview (spec §4, v0.2). Local/dev/test builds carry
|
||||||
|
// the "0.0.0-dev" sentinel below, since nothing has stamped them. Not read
|
||||||
|
// from anywhere else in the package.
|
||||||
|
const pluginVersion = "0.0.0-dev"
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# workflow-prepare.sh — stamp the release version into version.go at build time.
|
||||||
|
#
|
||||||
|
# The shared go-release workflow (lukaszraczylo/shared-actions go-release.yaml)
|
||||||
|
# runs this script, if present, from the repository root BEFORE GoReleaser
|
||||||
|
# builds and tags. Traefik runs this plugin under Yaegi, where the version
|
||||||
|
# cannot be resolved from build info at runtime, so the released semver must be
|
||||||
|
# baked into source here.
|
||||||
|
#
|
||||||
|
# Version source — first non-empty wins:
|
||||||
|
# $VERSION $VERSION_TAG $SEMVER $NEW_VERSION $RELEASE_VERSION
|
||||||
|
# A leading "v"/"V" is stripped.
|
||||||
|
#
|
||||||
|
# NOTE: go-release.yaml @main does not yet pass the computed version into this
|
||||||
|
# step's environment. Add it to the "Run workflow prepare script" step, e.g.:
|
||||||
|
# env:
|
||||||
|
# VERSION: ${{ needs.version.outputs.version }} # bare, no leading v
|
||||||
|
#
|
||||||
|
# The shared workflow runs this script in its test, version AND release jobs,
|
||||||
|
# but only the release job has a computed version. So a missing version is a
|
||||||
|
# no-op (leave the dev sentinel) — NOT a hard failure, otherwise the test/version
|
||||||
|
# jobs would break. A malformed version that IS provided is a hard error. Wire
|
||||||
|
# the env only on the release job's prepare step (see header note above).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
FILE="version.go"
|
||||||
|
CONST="pluginVersion"
|
||||||
|
|
||||||
|
VER="${VERSION:-${VERSION_TAG:-${SEMVER:-${NEW_VERSION:-${RELEASE_VERSION:-}}}}}"
|
||||||
|
VER="${VER#v}"
|
||||||
|
VER="${VER#V}"
|
||||||
|
|
||||||
|
if [ -z "$VER" ]; then
|
||||||
|
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
|
||||||
|
echo "workflow-prepare: WARNING no version provided; leaving ${FILE} at the dev placeholder. If this is the release build, set 'env: VERSION: \${{ needs.version.outputs.version }}' on the release job's prepare step — otherwise the release ships 0.0.0-dev and the admin overview reports it." >&2
|
||||||
|
else
|
||||||
|
echo "workflow-prepare: no version provided; leaving dev placeholder in ${FILE} (local build)"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Accept MAJOR[.MINOR[.PATCH]] with optional -prerelease / +build (semver-ish).
|
||||||
|
if ! printf '%s' "$VER" | grep -Eq '^[0-9]+(\.[0-9]+){0,2}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
|
||||||
|
echo "workflow-prepare: ERROR version '${VER}' is not semver-shaped" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "workflow-prepare: ERROR ${FILE} not found (run from repository root)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rewrite only the value of ${CONST}, anchored on the constant name.
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
sed -E "s/(${CONST}[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1${VER}\2/" "$FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$FILE"
|
||||||
|
|
||||||
|
if ! grep -Eq "${CONST}[[:space:]]*=[[:space:]]*\"${VER}\"" "$FILE"; then
|
||||||
|
echo "workflow-prepare: ERROR failed to stamp version into ${FILE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v gofmt >/dev/null 2>&1 && gofmt -w "$FILE"
|
||||||
|
echo "workflow-prepare: stamped ${CONST} = \"${VER}\" in ${FILE}"
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package traefikllmgateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// copyWorkflowPrepareFixture copies the repository's workflow-prepare.sh and
|
||||||
|
// version.go into a fresh temp directory and returns that directory. The
|
||||||
|
// script hardcodes "version.go" relative to its own working directory (see
|
||||||
|
// workflow-prepare.sh's FILE var), so the test runs it against a throwaway
|
||||||
|
// copy — never against the tracked version.go, which must keep carrying the
|
||||||
|
// "0.0.0-dev" sentinel in this package's own source.
|
||||||
|
func copyWorkflowPrepareFixture(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
for _, name := range []string{"workflow-prepare.sh", "version.go"} {
|
||||||
|
src, err := os.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", name, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), src, 0o600); err != nil { //nolint:gosec // G703: name ranges over a fixed literal slice above, not external input; dir is t.TempDir()
|
||||||
|
t.Fatalf("write %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// runWorkflowPrepare runs `bash workflow-prepare.sh` with dir as the working
|
||||||
|
// directory (the fixture from copyWorkflowPrepareFixture) and env appended
|
||||||
|
// as extra VAR=value entries. Go's exec.Cmd documents that a later duplicate
|
||||||
|
// key in Env wins, so entries here override any same-named var inherited
|
||||||
|
// from the test process.
|
||||||
|
func runWorkflowPrepare(t *testing.T, dir string, env ...string) (stdout, stderr string, exitCode int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := exec.Command("bash", "workflow-prepare.sh")
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Env = append(os.Environ(), env...)
|
||||||
|
|
||||||
|
var out, errOut strings.Builder
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &errOut
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
exitErr, ok := err.(*exec.ExitError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("run workflow-prepare.sh: %v", err)
|
||||||
|
}
|
||||||
|
return out.String(), errOut.String(), exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String(), errOut.String(), 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFixtureVersion(t *testing.T, dir string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
got, err := os.ReadFile(filepath.Join(dir, "version.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read stamped version.go: %v", err)
|
||||||
|
}
|
||||||
|
return string(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkflowPrepare(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("good: stamps VERSION into the const", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir := copyWorkflowPrepareFixture(t)
|
||||||
|
|
||||||
|
stdout, stderr, code := runWorkflowPrepare(t, dir, "VERSION=9.9.9")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit = %d, want 0; stdout=%s stderr=%s", code, stdout, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFixtureVersion(t, dir)
|
||||||
|
if !strings.Contains(got, `const pluginVersion = "9.9.9"`) {
|
||||||
|
t.Errorf("version.go does not contain the stamped const, got:\n%s", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, `const pluginVersion = "0.0.0-dev"`) {
|
||||||
|
t.Errorf("version.go const still carries the dev sentinel after stamping:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("good: strips a leading v from VERSION", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir := copyWorkflowPrepareFixture(t)
|
||||||
|
|
||||||
|
_, stderr, code := runWorkflowPrepare(t, dir, "VERSION=v1.2.3")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit = %d, want 0; stderr=%s", code, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFixtureVersion(t, dir)
|
||||||
|
if !strings.Contains(got, `pluginVersion = "1.2.3"`) {
|
||||||
|
t.Errorf("leading 'v' not stripped, got:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("edge: no version env leaves the sentinel untouched", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir := copyWorkflowPrepareFixture(t)
|
||||||
|
|
||||||
|
_, _, code := runWorkflowPrepare(t, dir,
|
||||||
|
"VERSION=", "VERSION_TAG=", "SEMVER=", "NEW_VERSION=", "RELEASE_VERSION=", "GITHUB_ACTIONS=")
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("exit = %d, want 0 (no-op) when no version env is set", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFixtureVersion(t, dir)
|
||||||
|
if !strings.Contains(got, `pluginVersion = "0.0.0-dev"`) {
|
||||||
|
t.Errorf("version.go mutated on a no-op run, got:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad: malformed VERSION exits non-zero and leaves the file untouched", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dir := copyWorkflowPrepareFixture(t)
|
||||||
|
|
||||||
|
_, stderr, code := runWorkflowPrepare(t, dir, "VERSION=not-a-version")
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatalf("exit = 0, want non-zero for malformed VERSION")
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr, "not semver-shaped") {
|
||||||
|
t.Errorf("stderr missing the semver error, got: %s", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := readFixtureVersion(t, dir)
|
||||||
|
if !strings.Contains(got, `pluginVersion = "0.0.0-dev"`) {
|
||||||
|
t.Errorf("version.go was mutated despite the malformed VERSION, got:\n%s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user