Closes the gap that let issue #2395 ship: redeploy-fleet workflows reported ssm_status=Success based on SSM RPC return code alone, while EC2 tenants silently kept serving the previous :latest digest because docker compose up without an explicit pull is a no-op when the local tag already exists. Wire: - new buildinfo package exposes GitSHA, set at link time via -ldflags from the GIT_SHA build-arg (default "dev" so test runs without ldflags fail closed against an unset deploy) - router exposes GET /buildinfo returning {git_sha} — public, no auth, cheap enough to curl from CI for every tenant - both Dockerfiles thread GIT_SHA into the Go build - publish-workspace-server-image.yml passes GIT_SHA=github.sha for both images - redeploy-tenants-on-main.yml + redeploy-tenants-on-staging.yml curl each tenant's /buildinfo after the redeploy SSM RPC and fail the workflow on digest mismatch; staging treats both :latest and :staging-latest as moving tags; verification is skipped only when an operator pinned a specific tag via workflow_dispatch Tests: - TestGitSHA_DefaultDevSentinel pins the dev default - TestBuildInfoEndpoint_ReturnsGitSHA pins the wire shape that the workflow's jq lookup depends on Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
Go
82 lines
2.9 KiB
Go
package buildinfo_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Molecule-AI/molecule-monorepo/platform/internal/buildinfo"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// TestGitSHA_DefaultDevSentinel pins the contract that an unset
|
|
// GIT_SHA at build time reads as "dev", NOT as an empty string. The
|
|
// redeploy verification step compares the deployed /buildinfo against
|
|
// the workflow's expected SHA — if GitSHA were "" by default, a
|
|
// misconfigured deploy would round-trip "" successfully if the
|
|
// expected SHA were also somehow ""; "dev" guarantees the comparison
|
|
// always fails closed for an unset deploy.
|
|
//
|
|
// Linker tests can't directly exercise -ldflags injection from inside
|
|
// `go test`, but they can pin the default the linker overrides.
|
|
func TestGitSHA_DefaultDevSentinel(t *testing.T) {
|
|
if buildinfo.GitSHA != "dev" {
|
|
t.Errorf("GitSHA default = %q, want %q (CI ldflags override expected to set this; tests run without ldflags so this should be the dev sentinel)", buildinfo.GitSHA, "dev")
|
|
}
|
|
}
|
|
|
|
// TestBuildInfoEndpoint_ReturnsGitSHA pins the wire shape of the
|
|
// /buildinfo response. The redeploy verification step reads
|
|
// `.git_sha` from this JSON; renaming the field would silently break
|
|
// every tenant verification (the jq lookup would return null + the
|
|
// step would interpret it as "tenant unreachable" and fail closed,
|
|
// which is correct but noisy).
|
|
//
|
|
// Test routes the handler against an httptest server rather than
|
|
// constructing a router.Setup() — that constructor takes a Hub +
|
|
// Broadcaster + Provisioner + WorkspaceHandler + ChannelMgr, and
|
|
// /buildinfo doesn't depend on any of them. Using a minimal gin
|
|
// engine here keeps the test fast and isolated to the contract under
|
|
// test.
|
|
func TestBuildInfoEndpoint_ReturnsGitSHA(t *testing.T) {
|
|
// Stash + restore so other tests that read GitSHA see a stable
|
|
// value. The package-level var is mutable by design (-ldflags),
|
|
// so test isolation requires explicit save/restore.
|
|
prev := buildinfo.GitSHA
|
|
t.Cleanup(func() { buildinfo.GitSHA = prev })
|
|
buildinfo.GitSHA = "abc1234deadbeef"
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
r.GET("/buildinfo", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"git_sha": buildinfo.GitSHA})
|
|
})
|
|
|
|
srv := httptest.NewServer(r)
|
|
t.Cleanup(srv.Close)
|
|
|
|
resp, err := http.Get(srv.URL + "/buildinfo")
|
|
if err != nil {
|
|
t.Fatalf("GET /buildinfo: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = resp.Body.Close() })
|
|
|
|
if resp.StatusCode != 200 {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
|
|
var body map[string]string
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
|
|
got, ok := body["git_sha"]
|
|
if !ok {
|
|
t.Fatalf("response missing git_sha field — would break the redeploy verification jq lookup. Body: %+v", body)
|
|
}
|
|
if got != "abc1234deadbeef" {
|
|
t.Errorf("git_sha = %q, want %q", got, "abc1234deadbeef")
|
|
}
|
|
}
|