Skip to content
Open
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
83 changes: 83 additions & 0 deletions cmd/publisher/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -64,6 +67,9 @@ func InitCommand() error {
model.CurrentSchemaURL, name, description, version, repoURL, repoSource, subfolder,
packageType, packageIdentifier, version, envVars,
)
if server.Repository != nil {
server.Repository.ID = detectRepoID(repoSource, repoURL)
}

// Write to file
jsonData, err := json.MarshalIndent(server, "", " ")
Expand Down Expand Up @@ -247,6 +253,83 @@ func buildGitHubServerName(repoURL, subfolder string) string {
return fmt.Sprintf("io.github.%s/%s", owner, repo)
}

// githubAPIBaseURL is a variable so the tests can point it at a stub server.
var githubAPIBaseURL = "https://api.github.com"

// parseGitHubOwnerRepo pulls owner/repo out of a github.com web URL, which is
// the only shape `detectRepoURL` produces for GitHub (it rewrites the SSH form
// and strips `.git`). Anything else — a different host, a bare path, a URL with
// extra segments — returns ok=false rather than a guess.
func parseGitHubOwnerRepo(repoURL string) (owner, repo string, ok bool) {
u, err := url.Parse(repoURL)
if err != nil {
return "", "", false
}
if strings.TrimPrefix(strings.ToLower(u.Hostname()), "www.") != "github.com" {
return "", "", false
}
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", false
}
return parts[0], strings.TrimSuffix(parts[1], ".git"), true
}

// detectRepoID resolves the forge's own identifier for the repository, which
// `repository.id` exists to carry.
//
// A URL is not a stable pointer: renaming or transferring a GitHub repo leaves
// the registered `repository.url` resolving only through GitHub's redirect,
// which the REST API follows but GraphQL does not, so the source link rots for
// consumers. The numeric ID does not move (#1484). Recording it at init time
// costs one request and makes the entry re-resolvable later even if the URL has
// gone stale.
//
// Deliberately best effort: `init` is otherwise fully offline, so being off the
// network, rate limited, or pointed at a private repo leaves the field empty
// instead of failing the command. `GITHUB_TOKEN` is used when present, mostly
// so a rate-limited developer still gets the ID.
func detectRepoID(repoSource, repoURL string) string {
if repoSource != MethodGitHub {
return ""
}
owner, repo, ok := parseGitHubOwnerRepo(repoURL)
if !ok {
return ""
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(
ctx, http.MethodGet,
fmt.Sprintf("%s/repos/%s/%s", githubAPIBaseURL, owner, repo), nil,
)
if err != nil {
return ""
}
req.Header.Set("Accept", "application/vnd.github+json")
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ""
}

var body struct {
ID int64 `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil || body.ID == 0 {
return ""
}
return strconv.FormatInt(body.ID, 10)
}

func detectDescription() string {
// Try to get from package.json
if data, err := os.ReadFile("package.json"); err == nil {
Expand Down
77 changes: 77 additions & 0 deletions cmd/publisher/commands/init_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package commands

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseGitHubOwnerRepo(t *testing.T) {
tests := []struct {
name string
repoURL string
owner string
repo string
ok bool
}{
{name: "plain https URL", repoURL: "https://github.com/acme/weather", owner: "acme", repo: "weather", ok: true},
{name: "www host", repoURL: "https://www.github.com/acme/weather", owner: "acme", repo: "weather", ok: true},
{name: "trailing slash", repoURL: "https://github.com/acme/weather/", owner: "acme", repo: "weather", ok: true},
{name: "dot-git suffix", repoURL: "https://github.com/acme/weather.git", owner: "acme", repo: "weather", ok: true},
{name: "gitlab is not github", repoURL: "https://gitlab.com/acme/weather"},
// A lookalike host must not be treated as GitHub — the ID we would fetch
// would belong to whatever that host decided to return.
{name: "lookalike host", repoURL: "https://github.com.evil.example/acme/weather"},
{name: "deep path is not a repo root", repoURL: "https://github.com/acme/weather/tree/main/src"},
{name: "owner only", repoURL: "https://github.com/acme"},
{name: "empty", repoURL: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
owner, repo, ok := parseGitHubOwnerRepo(tt.repoURL)
assert.Equal(t, tt.ok, ok)
assert.Equal(t, tt.owner, owner)
assert.Equal(t, tt.repo, repo)
})
}
}

func TestDetectRepoID(t *testing.T) {
t.Run("records the numeric id GitHub reports", func(t *testing.T) {
var gotPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
_, _ = w.Write([]byte(`{"id": 123456789, "full_name": "acme/weather"}`))
}))
defer srv.Close()
t.Setenv("GITHUB_TOKEN", "")

originalBaseURL := githubAPIBaseURL
githubAPIBaseURL = srv.URL
defer func() { githubAPIBaseURL = originalBaseURL }()

assert.Equal(t, "123456789", detectRepoID(MethodGitHub, "https://github.com/acme/weather"))
assert.Equal(t, "/repos/acme/weather", gotPath)
})

t.Run("stays empty rather than failing init when the lookup does not work", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
t.Setenv("GITHUB_TOKEN", "")

originalBaseURL := githubAPIBaseURL
githubAPIBaseURL = srv.URL
defer func() { githubAPIBaseURL = originalBaseURL }()

// 404 / private / rate limited.
assert.Empty(t, detectRepoID(MethodGitHub, "https://github.com/acme/weather"))
// Non-GitHub sources are never looked up, so no request is made at all.
assert.Empty(t, detectRepoID("gitlab", "https://gitlab.com/acme/weather"))
assert.Empty(t, detectRepoID(MethodGitHub, ""))
})
}
Loading