Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions internal/cli/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ When using --pem-dir, additionally requires:
Region: region,
FunctionSourceDir: sourceDir,
DeployMode: deployMode,
Version: version,
Commit: commitSHA,
PublicMint: public,
}

Expand Down Expand Up @@ -1214,6 +1216,18 @@ func runMintStatus(ctx context.Context, printer *ui.Printer, project, region, or
printer.KeyValue("Project", project)
printer.KeyValue("Region", region)

// Query /health for version metadata.
if mintVersion, mintCommit, healthErr := queryMintHealth(ctx, discovery.URL); healthErr != nil {
printer.StepWarn(fmt.Sprintf("Could not query mint version: %v", healthErr))
} else {
if mintVersion != "" {
printer.KeyValue("Version", mintVersion)
}
if mintCommit != "" {
printer.KeyValue("Commit", mintCommit)
}
}

// Step 2a: Cloud Run revision info.
printer.StepStart("Querying Cloud Run revision state")
revInfo, revErr := provisioner.GetServiceRevisionInfo(ctx)
Expand Down Expand Up @@ -1409,3 +1423,33 @@ func runMintStatus(ctx context.Context, printer *ui.Printer, project, region, or

return nil
}

// queryMintHealth fetches the mint /health endpoint and extracts version
// metadata. Returns empty strings when the fields are absent. The health
// endpoint is unauthenticated so this works without OIDC credentials.
func queryMintHealth(ctx context.Context, mintURL string) (version, commit string, err error) {
healthURL := strings.TrimRight(mintURL, "/") + "/health"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
if err != nil {
return "", "", fmt.Errorf("creating health request: %w", err)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", "", fmt.Errorf("querying health: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusServiceUnavailable {
return "", "", fmt.Errorf("health returned HTTP %d", resp.StatusCode)
}

var body struct {
Version string `json:"version"`
Commit string `json:"commit"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return "", "", fmt.Errorf("decoding health response: %w", err)
}
return body.Version, body.Commit, nil
}
178 changes: 178 additions & 0 deletions internal/cli/mint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,58 @@ func TestRunMintEnrollRepo_InvalidFormat(t *testing.T) {
assert.Contains(t, err.Error(), "owner/repo")
}

func TestQueryMintHealth_WithVersion(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/health", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok","version":"0.27.0","commit":"abc123"}`)
}))
defer srv.Close()

ver, commit, err := queryMintHealth(context.Background(), srv.URL)
require.NoError(t, err)
assert.Equal(t, "0.27.0", ver)
assert.Equal(t, "abc123", commit)
}

func TestQueryMintHealth_NoVersion(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok"}`)
}))
defer srv.Close()

ver, commit, err := queryMintHealth(context.Background(), srv.URL)
require.NoError(t, err)
assert.Empty(t, ver)
assert.Empty(t, commit)
}

func TestQueryMintHealth_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()

_, _, err := queryMintHealth(context.Background(), srv.URL)
require.Error(t, err)
assert.Contains(t, err.Error(), "500")
}

func TestQueryMintHealth_ServiceUnavailable(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintln(w, `{"status":"unhealthy","version":"0.28.0","commit":"def456"}`)
}))
defer srv.Close()

ver, commit, err := queryMintHealth(context.Background(), srv.URL)
require.NoError(t, err)
assert.Equal(t, "0.28.0", ver)
assert.Equal(t, "def456", commit)
}

func TestRunMintStatus_Healthy(t *testing.T) {
withMintGCFClient(t, mintDiscoveryClient())
out := &strings.Builder{}
Expand All @@ -868,6 +920,132 @@ func TestRunMintStatus_Healthy(t *testing.T) {
assert.Contains(t, out.String(), "existing-org")
}

func TestRunMintStatus_WithHealthVersion(t *testing.T) {
// Spin up a health server that returns version metadata.
healthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok","version":"1.0.0","commit":"abc123"}`)
return
}
http.NotFound(w, r)
}))
defer healthSrv.Close()

client := gcf.NewFakeGCFClient(
gcf.WithFakeFunctionInfo(&gcf.FunctionInfo{
URI: healthSrv.URL,
EnvVars: map[string]string{
"ROLE_APP_IDS": `{"coder":"100"}`,
"ALLOWED_ORGS": "test-org",
},
}),
gcf.WithFakeTrafficEnvVars(map[string]string{
"ROLE_APP_IDS": `{"coder":"100"}`,
"ALLOWED_ORGS": "test-org",
}),
gcf.WithFakeRevisionInfo(&gcf.ServiceRevisionInfo{
TrafficRevisionShort: "fullsend-mint-00001",
TrafficPercent: 100,
TemplateMatchesTraffic: true,
TrafficEnvVars: map[string]string{
"ROLE_APP_IDS": `{"coder":"100"}`,
"ALLOWED_ORGS": "test-org",
},
RecentRevisions: []gcf.RevisionSummary{{
Name: "fullsend-mint-00001",
CreateTime: "2026-06-16T12:00:00Z",
Active: true,
}},
}),
gcf.WithFakeWIFProvider(&gcf.WIFProviderInfo{
AttributeCondition: "assertion.repository_owner in ['test-org']",
}),
gcf.WithFakeSecrets(map[string]bool{
"fullsend-coder-pem": true,
}),
)
withMintGCFClient(t, client)
out := &strings.Builder{}
printer := ui.New(out)
err := runMintStatus(context.Background(), printer, "my-project", "us-central1", "test-org")
require.NoError(t, err)
assert.Contains(t, out.String(), "Version")
assert.Contains(t, out.String(), "1.0.0")
assert.Contains(t, out.String(), "Commit")
assert.Contains(t, out.String(), "abc123")
}

func TestQueryMintHealth_InvalidJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `not-valid-json`)
}))
defer srv.Close()

_, _, err := queryMintHealth(context.Background(), srv.URL)
require.Error(t, err)
assert.Contains(t, err.Error(), "decoding health response")
}

func TestQueryMintHealth_ConnectionRefused(t *testing.T) {
// Use a URL pointing to a port that nothing is listening on.
_, _, err := queryMintHealth(context.Background(), "http://127.0.0.1:1")
require.Error(t, err)
assert.Contains(t, err.Error(), "querying health")
}

func TestQueryMintHealth_BadURL(t *testing.T) {
// A URL with an invalid scheme triggers the request-creation error path.
_, _, err := queryMintHealth(context.Background(), "://bad-url")
require.Error(t, err)
assert.Contains(t, err.Error(), "creating health request")
}

func TestRunMintStatus_HealthError(t *testing.T) {
// When the health endpoint is unreachable, runMintStatus should still
// succeed and print a warning instead of failing.
client := gcf.NewFakeGCFClient(
gcf.WithFakeFunctionInfo(&gcf.FunctionInfo{
URI: "http://127.0.0.1:1", // unreachable
EnvVars: map[string]string{
"ROLE_APP_IDS": `{"coder":"100"}`,
"ALLOWED_ORGS": "test-org",
},
}),
gcf.WithFakeTrafficEnvVars(map[string]string{
"ROLE_APP_IDS": `{"coder":"100"}`,
"ALLOWED_ORGS": "test-org",
}),
gcf.WithFakeRevisionInfo(&gcf.ServiceRevisionInfo{
TrafficRevisionShort: "fullsend-mint-00001",
TrafficPercent: 100,
TemplateMatchesTraffic: true,
TrafficEnvVars: map[string]string{
"ROLE_APP_IDS": `{"coder":"100"}`,
"ALLOWED_ORGS": "test-org",
},
RecentRevisions: []gcf.RevisionSummary{{
Name: "fullsend-mint-00001",
CreateTime: "2026-06-16T12:00:00Z",
Active: true,
}},
}),
gcf.WithFakeWIFProvider(&gcf.WIFProviderInfo{
AttributeCondition: "assertion.repository_owner in ['test-org']",
}),
gcf.WithFakeSecrets(map[string]bool{
"fullsend-coder-pem": true,
}),
)
withMintGCFClient(t, client)
out := &strings.Builder{}
printer := ui.New(out)
err := runMintStatus(context.Background(), printer, "my-project", "us-central1", "test-org")
require.NoError(t, err)
assert.Contains(t, out.String(), "Could not query mint version")
}

func TestRunMintStatus_NotInstalled(t *testing.T) {
withMintGCFClient(t, gcf.NewFakeGCFClient())
out := &strings.Builder{}
Expand Down
21 changes: 16 additions & 5 deletions internal/dispatch/gcf/mintsrc/mintcore/handler.go.embed
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ type mintResponse struct {

// statusResponse is returned by the /v1/status diagnostic endpoint.
type statusResponse struct {
Org string `json:"org"`
Roles []string `json:"roles"`
Org string `json:"org"`
Roles []string `json:"roles"`
Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"`
}

// Handler holds dependencies for the token mint HTTP server.
Expand Down Expand Up @@ -307,8 +309,15 @@ func (h *Handler) handleHealth(w http.ResponseWriter) {
})
return
}
resp := map[string]string{"status": "ok"}
if Version != "" {
resp["version"] = Version
}
if Commit != "" {
resp["commit"] = Commit
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{"status":"ok"}`)
json.NewEncoder(w).Encode(resp)
}

func (h *Handler) handleStatus(w http.ResponseWriter, claims *Claims) {
Expand All @@ -319,8 +328,10 @@ func (h *Handler) handleStatus(w http.ResponseWriter, claims *Claims) {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(statusResponse{
Org: org,
Roles: roles,
Org: org,
Roles: roles,
Version: Version,
Commit: Commit,
}); err != nil {
log.Printf("encoding status response: %v", err)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/dispatch/gcf/mintsrc/mintcore/version.go.embed
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package mintcore

// Version and Commit are stamped into the Cloud Function source at
// deployment time by the provisioner. In development (local dev, tests)
// they default to empty strings.
var (
Version string
Commit string
)
Loading
Loading