fix(provisioning): Gitea plural /git/refs/heads path (Refs TBD-C18c — unblocks journey step 14 → Org CR creation) (#1712)
Third layer of C18. The provisioning Git Data API client (used for both
upstream github.com AND in-cluster Gitea on Sovereign clusters) called
the GitHub-only singular `/git/ref/heads/<branch>`. Gitea 1.22 only
implements the plural `/git/refs/heads/<branch>` and ALWAYS returns an
array.
Before (failing on Gitea):
curl -sS .../api/v1/repos/openova/openova/git/ref/heads/main
→ 404
After (Gitea + GitHub both work):
curl -sS .../api/v1/repos/openova/openova/git/refs/heads/main
→ 200 [{"object":{"sha":"c3f4799…"}}]
Without this fix, customer journey step 14 ("Committing manifests to
Git") fails on every Sovereign cluster, blocking downstream Org CR
creation. See journey v3 report.
This is the THIRD layer of C18:
- C18 — chart 0.1.30 step 09 gitea-token-mint (TBD-C18)
- C18b — secret ownership (PR fix-c18b-secret-ownership)
- C18c — THIS — plural refs path
Change:
- client.go getRef: GET /git/refs/heads/<branch> (was /git/ref/heads/)
- Unmarshal tries array first (Gitea + GitHub prefix), falls back to
single object (GitHub exact-ref), preserving upstream-GitHub behavior.
- New client_getref_test.go: asserts plural path, single-object decode,
empty-array error path.
Tests:
go test ./core/services/provisioning/github/... → ok
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a6fd044c98
commit
7411995b6a
@ -356,19 +356,42 @@ func (c *Client) doRequest(ctx context.Context, method, url string, body any) ([
|
||||
}
|
||||
|
||||
func (c *Client) getRef(ctx context.Context, branch string) (string, error) {
|
||||
body, err := c.doRequest(ctx, http.MethodGet, c.apiURL("/git/ref/heads/"+branch), nil)
|
||||
// TBD-C18c (2026-05-18): Use plural /git/refs/heads/<branch>.
|
||||
//
|
||||
// GitHub supports BOTH /git/ref/<ref> (singular, returns one object) and
|
||||
// /git/refs/<ref> (plural, prefix-match — returns one object for an exact
|
||||
// ref, array for a prefix). Gitea 1.22 ONLY implements the plural form
|
||||
// and ALWAYS returns an array even for an exact `heads/<branch>` match
|
||||
// (https://docs.gitea.com/api#tag/repository/operation/repoGetGitRefs).
|
||||
//
|
||||
// The previous singular path 404'd on Gitea, blocking customer journey
|
||||
// step 14 ("Committing manifests to Git") and downstream Org CR creation
|
||||
// during day-2 install onto a Sovereign cluster's in-cluster Gitea.
|
||||
//
|
||||
// We try the array shape first (Gitea + GitHub prefix-style) and fall
|
||||
// back to a single-object decode (GitHub exact-ref shape — kept for
|
||||
// forward compatibility against api.github.com behavior).
|
||||
body, err := c.doRequest(ctx, http.MethodGet, c.apiURL("/git/refs/heads/"+branch), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp struct {
|
||||
type refResp struct {
|
||||
Object struct {
|
||||
SHA string `json:"sha"`
|
||||
} `json:"object"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return "", err
|
||||
var arr []refResp
|
||||
if err := json.Unmarshal(body, &arr); err == nil {
|
||||
if len(arr) == 0 {
|
||||
return "", fmt.Errorf("getRef: empty refs array for heads/%s", branch)
|
||||
}
|
||||
return arr[0].Object.SHA, nil
|
||||
}
|
||||
return resp.Object.SHA, nil
|
||||
var single refResp
|
||||
if err := json.Unmarshal(body, &single); err != nil {
|
||||
return "", fmt.Errorf("getRef: response neither array nor object: %w", err)
|
||||
}
|
||||
return single.Object.SHA, nil
|
||||
}
|
||||
|
||||
func (c *Client) getCommitTree(ctx context.Context, commitSHA string) (string, error) {
|
||||
|
||||
81
core/services/provisioning/github/client_getref_test.go
Normal file
81
core/services/provisioning/github/client_getref_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGetRef_UsesPluralRefsPath asserts the client requests the Gitea-compatible
|
||||
// plural /git/refs/heads/<branch> path, NOT the GitHub-only singular
|
||||
// /git/ref/heads/<branch> path. The singular path 404s on Gitea 1.22, blocking
|
||||
// customer journey step 14 ("Committing manifests to Git") and downstream Org CR
|
||||
// creation. See TBD-C18c (2026-05-18).
|
||||
func TestGetRef_UsesPluralRefsPath(t *testing.T) {
|
||||
var gotPath string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Gitea always returns an array for /git/refs/heads/<branch>.
|
||||
_, _ = w.Write([]byte(`[{"object":{"sha":"c3f4799deadbeef0000000000000000deadbeef"}}]`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClientWithAPIURL("token", "owner", "repo", srv.URL)
|
||||
sha, err := c.getRef(context.Background(), "main")
|
||||
if err != nil {
|
||||
t.Fatalf("getRef returned error: %v", err)
|
||||
}
|
||||
if sha != "c3f4799deadbeef0000000000000000deadbeef" {
|
||||
t.Fatalf("getRef sha = %q, want c3f4799deadbeef0000000000000000deadbeef", sha)
|
||||
}
|
||||
|
||||
wantSuffix := "/git/refs/heads/main"
|
||||
if !strings.HasSuffix(gotPath, wantSuffix) {
|
||||
t.Fatalf("request path = %q, want suffix %q (plural /git/refs/, NOT singular /git/ref/)", gotPath, wantSuffix)
|
||||
}
|
||||
// Defence-in-depth: assert we did NOT regress to the GitHub-only singular form.
|
||||
if strings.Contains(gotPath, "/git/ref/heads/") {
|
||||
t.Fatalf("request path = %q contains GitHub-only singular /git/ref/heads/ — regression of TBD-C18c", gotPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetRef_AcceptsSingleObjectResponse covers GitHub's exact-ref response
|
||||
// shape (object, not array). Kept for forward compatibility — even though the
|
||||
// Sovereign path always hits Gitea today, the client is also used against
|
||||
// upstream github.com from the contabo path.
|
||||
func TestGetRef_AcceptsSingleObjectResponse(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"object":{"sha":"abc1230000000000000000000000000000abc123"}}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClientWithAPIURL("token", "owner", "repo", srv.URL)
|
||||
sha, err := c.getRef(context.Background(), "main")
|
||||
if err != nil {
|
||||
t.Fatalf("getRef returned error: %v", err)
|
||||
}
|
||||
if sha != "abc1230000000000000000000000000000abc123" {
|
||||
t.Fatalf("getRef sha = %q, want abc1230000000000000000000000000000abc123", sha)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetRef_EmptyArrayReturnsError guards against an upstream returning an
|
||||
// empty refs array (would otherwise silently produce sha="" and propagate into
|
||||
// a malformed updateRef call).
|
||||
func TestGetRef_EmptyArrayReturnsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClientWithAPIURL("token", "owner", "repo", srv.URL)
|
||||
_, err := c.getRef(context.Background(), "main")
|
||||
if err == nil {
|
||||
t.Fatalf("getRef returned nil error on empty refs array; want error")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user