From ed94ce1e6966200706f381e6828dbb4b60d649d5 Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Mon, 11 May 2026 06:21:02 +0000 Subject: [PATCH] fix(platform): /github-installation-token returns 501 on missing config (#388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When GITHUB_APP_ID/INSTALLATION_ID/PRIVATE_KEY_FILE are unset (Gitea- canonical deployment or suspended GitHub App org), generateAppInstallation Token() returns "required" — a permanent configuration error, not a transient one. Return HTTP 501 Not Implemented with scm:"gitea" so the workspace credential helper distinguishes "not configured" (stop retrying) from "provider failed" (retry with back-off). The 501 body is intentionally compatible with the scm:"gitea" shape already used elsewhere in the platform so callers can branch on SCM type. --- .../internal/handlers/github_token.go | 13 ++++++++++- .../internal/handlers/github_token_test.go | 22 +++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/workspace-server/internal/handlers/github_token.go b/workspace-server/internal/handlers/github_token.go index ce9492a9..0337916d 100644 --- a/workspace-server/internal/handlers/github_token.go +++ b/workspace-server/internal/handlers/github_token.go @@ -49,6 +49,7 @@ import ( "net/http" "os" "strconv" + "strings" "time" "github.com/Molecule-AI/molecule-monorepo/platform/pkg/provisionhook" @@ -98,7 +99,17 @@ func (h *GitHubTokenHandler) GetInstallationToken(c *gin.Context) { token, expiresAt, err := generateAppInstallationToken() if err != nil { log.Printf("[github] fallback token generation failed: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "token refresh failed"}) + // #388: GITHUB_APP_ID/INSTALLATION_ID unset → Gitea-canonical deployment + // or suspended org. Return 501 so callers (credential helper / gh auth) + // know this is not-implemented vs a transient error. + if strings.Contains(err.Error(), "required") { + c.JSON(http.StatusNotImplemented, gin.H{ + "error": "GitHub integration not configured", + "scm": "gitea", + }) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "token refresh failed"}) + } return } c.JSON(http.StatusOK, gin.H{"token": token, "expires_at": expiresAt}) diff --git a/workspace-server/internal/handlers/github_token_test.go b/workspace-server/internal/handlers/github_token_test.go index 01076c81..f4b16ca8 100644 --- a/workspace-server/internal/handlers/github_token_test.go +++ b/workspace-server/internal/handlers/github_token_test.go @@ -78,11 +78,12 @@ func TestGitHubToken_NilRegistry(t *testing.T) { // Post-#960/#1101 the handler now falls back to direct env-based App // token generation (GITHUB_APP_ID / INSTALLATION_ID / PRIVATE_KEY_FILE) // when no registered provider matches. In the test environment those -// env vars are unset, so the fallback fails with 500 "token refresh -// failed" — a clean retryable signal for the workspace credential -// helper. Previously this path returned 404; the new 500 matches the -// ProviderError shape so callers don't have to branch on "missing -// provider" vs "provider failed". +// env vars are unset, so the fallback fails with 501 "not implemented" +// with scm:"gitea" — signals a Gitea-canonical or suspended-org +// deployment where GitHub integration is not configured (#388). +// Previously this path returned 404; 501 distinguishes "not configured" +// (caller should stop retrying) from "provider failed" (caller should +// retry with back-off). func TestGitHubToken_NoTokenProvider(t *testing.T) { reg := provisionhook.NewRegistry() reg.Register(&mockMutatorOnly{name: "other-plugin"}) @@ -91,12 +92,15 @@ func TestGitHubToken_NoTokenProvider(t *testing.T) { h.GetInstallationToken(c) - if w.Code != http.StatusInternalServerError { - t.Fatalf("expected 500 (env-based fallback fails with unset GITHUB_APP_* vars), got %d: %s", + if w.Code != http.StatusNotImplemented { + t.Fatalf("expected 501 (env-based fallback fails with unset GITHUB_APP_* vars), got %d: %s", w.Code, w.Body.String()) } - if !strings.Contains(w.Body.String(), "token refresh failed") { - t.Errorf("expected body to contain 'token refresh failed', got: %s", w.Body.String()) + if !strings.Contains(w.Body.String(), "GitHub integration not configured") { + t.Errorf("expected body to contain 'GitHub integration not configured', got: %s", w.Body.String()) + } + if !strings.Contains(w.Body.String(), `"scm":"gitea"`) { + t.Errorf("expected body to contain 'scm:gitea', got: %s", w.Body.String()) } }