refactor: decouple update package from forge-specific gitlab config
This commit is contained in:
parent
5642581b9b
commit
605ef93fd6
5 changed files with 116 additions and 86 deletions
|
|
@ -5,7 +5,11 @@ Bibliotheque Go pour construire des binaires MCP avec :
|
||||||
- resolution de profils CLI
|
- resolution de profils CLI
|
||||||
- stockage JSON de configuration dans `os.UserConfigDir()`
|
- stockage JSON de configuration dans `os.UserConfigDir()`
|
||||||
- stockage de secrets dans le wallet natif selon l'OS
|
- stockage de secrets dans le wallet natif selon l'OS
|
||||||
- pipeline d'auto-update via GitLab Releases
|
- pipeline d'auto-update via endpoint de release configurable
|
||||||
|
|
||||||
|
Le package `update` ne deduit pas la forge ni l'authentification.
|
||||||
|
L'application cliente fournit l'URL de release, le header d'auth eventuel et,
|
||||||
|
si besoin, les variables d'environnement a consulter.
|
||||||
|
|
||||||
Packages exposes :
|
Packages exposes :
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func TestSaveAndLoadRoundTrip(t *testing.T) {
|
||||||
CurrentProfile: "prod",
|
CurrentProfile: "prod",
|
||||||
Profiles: map[string]testProfile{
|
Profiles: map[string]testProfile{
|
||||||
"prod": {
|
"prod": {
|
||||||
BaseURL: "https://graylog.example.com",
|
BaseURL: "https://api.example.com",
|
||||||
StreamID: "stream-1",
|
StreamID: "stream-1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -72,7 +72,7 @@ func TestSaveAndLoadRoundTrip(t *testing.T) {
|
||||||
if cfg.CurrentProfile != "prod" {
|
if cfg.CurrentProfile != "prod" {
|
||||||
t.Fatalf("CurrentProfile = %q, want prod", cfg.CurrentProfile)
|
t.Fatalf("CurrentProfile = %q, want prod", cfg.CurrentProfile)
|
||||||
}
|
}
|
||||||
if cfg.Profiles["prod"].BaseURL != "https://graylog.example.com" {
|
if cfg.Profiles["prod"].BaseURL != "https://api.example.com" {
|
||||||
t.Fatalf("BaseURL = %q", cfg.Profiles["prod"].BaseURL)
|
t.Fatalf("BaseURL = %q", cfg.Profiles["prod"].BaseURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,4 +1,4 @@
|
||||||
module gitlab.lundimatin.app/artificial-intelligence-ia/claude/mcp-framework
|
module gitea.lclr.dev/AI/mcp-framework
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,18 @@ type Options struct {
|
||||||
LatestReleaseURL string
|
LatestReleaseURL string
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
BinaryName string
|
BinaryName string
|
||||||
ReleaseSource GitLabSource
|
ReleaseSource ReleaseSource
|
||||||
GOOS string
|
GOOS string
|
||||||
GOARCH string
|
GOARCH string
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitLabSource struct {
|
type ReleaseSource struct {
|
||||||
BaseURL string
|
Name string
|
||||||
ProjectPath string
|
BaseURL string
|
||||||
Token string
|
LatestReleaseURL string
|
||||||
TokenHeader string
|
Token string
|
||||||
TokenEnvNames []string
|
TokenHeader string
|
||||||
|
TokenEnvNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
|
|
@ -70,7 +71,15 @@ func Run(ctx context.Context, opts Options) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
source := normalizeSource(opts.ReleaseSource)
|
source := normalizeSource(opts.ReleaseSource)
|
||||||
auth := ResolveGitLabAuth(source.Token, source)
|
auth := ResolveAuth(source.Token, source)
|
||||||
|
|
||||||
|
releaseURL := opts.LatestReleaseURL
|
||||||
|
if strings.TrimSpace(releaseURL) == "" {
|
||||||
|
releaseURL = strings.TrimSpace(source.LatestReleaseURL)
|
||||||
|
}
|
||||||
|
if releaseURL == "" {
|
||||||
|
return errors.New("latest release URL must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
targetPath, err := ResolveUpdateTarget(opts.ExecutablePath)
|
targetPath, err := ResolveUpdateTarget(opts.ExecutablePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -82,11 +91,6 @@ func Run(ctx context.Context, opts Options) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
releaseURL := opts.LatestReleaseURL
|
|
||||||
if strings.TrimSpace(releaseURL) == "" {
|
|
||||||
releaseURL = LatestReleaseAPIURL(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := FetchLatestRelease(ctx, opts.Client, releaseURL, auth, source)
|
release, err := FetchLatestRelease(ctx, opts.Client, releaseURL, auth, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -115,7 +119,7 @@ func Run(ctx context.Context, opts Options) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveGitLabAuth(explicitToken string, source GitLabSource) Auth {
|
func ResolveAuth(explicitToken string, source ReleaseSource) Auth {
|
||||||
source = normalizeSource(source)
|
source = normalizeSource(source)
|
||||||
|
|
||||||
if token := strings.TrimSpace(explicitToken); token != "" {
|
if token := strings.TrimSpace(explicitToken); token != "" {
|
||||||
|
|
@ -131,15 +135,6 @@ func ResolveGitLabAuth(explicitToken string, source GitLabSource) Auth {
|
||||||
return Auth{}
|
return Auth{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LatestReleaseAPIURL(source GitLabSource) string {
|
|
||||||
source = normalizeSource(source)
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s/api/v4/projects/%s/releases/permalink/latest",
|
|
||||||
source.BaseURL,
|
|
||||||
url.PathEscape(source.ProjectPath),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResolveUpdateTarget(explicitPath string) (string, error) {
|
func ResolveUpdateTarget(explicitPath string) (string, error) {
|
||||||
targetPath := strings.TrimSpace(explicitPath)
|
targetPath := strings.TrimSpace(explicitPath)
|
||||||
if targetPath == "" {
|
if targetPath == "" {
|
||||||
|
|
@ -177,7 +172,7 @@ func AssetName(binaryName, goos, goarch string) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchLatestRelease(ctx context.Context, client *http.Client, releaseURL string, auth Auth, source GitLabSource) (Release, error) {
|
func FetchLatestRelease(ctx context.Context, client *http.Client, releaseURL string, auth Auth, source ReleaseSource) (Release, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releaseURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Release{}, fmt.Errorf("build latest release request: %w", err)
|
return Release{}, fmt.Errorf("build latest release request: %w", err)
|
||||||
|
|
@ -240,7 +235,7 @@ func (r Release) AssetURL(assetName, releaseURL string) (string, error) {
|
||||||
return "", fmt.Errorf("latest release does not contain asset %q", assetName)
|
return "", fmt.Errorf("latest release does not contain asset %q", assetName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DownloadReleaseAsset(ctx context.Context, client *http.Client, assetURL, targetPath string, auth Auth, source GitLabSource) (string, error) {
|
func DownloadReleaseAsset(ctx context.Context, client *http.Client, assetURL, targetPath string, auth Auth, source ReleaseSource) (string, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, assetURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, assetURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("build artifact download request: %w", err)
|
return "", fmt.Errorf("build artifact download request: %w", err)
|
||||||
|
|
@ -307,12 +302,11 @@ func ReplaceExecutable(downloadPath, targetPath string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeSource(source GitLabSource) GitLabSource {
|
func normalizeSource(source ReleaseSource) ReleaseSource {
|
||||||
if source.TokenHeader == "" {
|
source.Name = strings.TrimSpace(source.Name)
|
||||||
source.TokenHeader = "PRIVATE-TOKEN"
|
|
||||||
}
|
|
||||||
source.BaseURL = strings.TrimRight(strings.TrimSpace(source.BaseURL), "/")
|
source.BaseURL = strings.TrimRight(strings.TrimSpace(source.BaseURL), "/")
|
||||||
source.ProjectPath = strings.TrimSpace(source.ProjectPath)
|
source.LatestReleaseURL = strings.TrimSpace(source.LatestReleaseURL)
|
||||||
|
source.TokenHeader = strings.TrimSpace(source.TokenHeader)
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +329,7 @@ func (a Auth) apply(req *http.Request) {
|
||||||
req.Header.Set(a.Header, a.Token)
|
req.Header.Set(a.Header, a.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Auth) maybeHint(statusCode int, body []byte, source GitLabSource) error {
|
func (a Auth) maybeHint(statusCode int, body []byte, source ReleaseSource) error {
|
||||||
source = normalizeSource(source)
|
source = normalizeSource(source)
|
||||||
if strings.TrimSpace(a.Token) != "" || len(source.TokenEnvNames) == 0 {
|
if strings.TrimSpace(a.Token) != "" || len(source.TokenEnvNames) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -349,22 +343,34 @@ func (a Auth) maybeHint(statusCode int, body []byte, source GitLabSource) error
|
||||||
|
|
||||||
message := strings.ToLower(strings.TrimSpace(string(body)))
|
message := strings.ToLower(strings.TrimSpace(string(body)))
|
||||||
if !strings.Contains(message, "project not found") &&
|
if !strings.Contains(message, "project not found") &&
|
||||||
|
!strings.Contains(message, "not found") &&
|
||||||
!strings.Contains(message, "unauthorized") &&
|
!strings.Contains(message, "unauthorized") &&
|
||||||
!strings.Contains(message, "forbidden") {
|
!strings.Contains(message, "forbidden") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target := source.BaseURL
|
||||||
|
if target == "" {
|
||||||
|
target = "release endpoint"
|
||||||
|
}
|
||||||
|
name := source.Name
|
||||||
|
if name == "" {
|
||||||
|
name = "release"
|
||||||
|
}
|
||||||
|
|
||||||
if len(source.TokenEnvNames) == 1 {
|
if len(source.TokenEnvNames) == 1 {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"GitLab release access requires authentication on %s; set %s and retry",
|
"%s access requires authentication on %s; set %s and retry",
|
||||||
source.BaseURL,
|
name,
|
||||||
|
target,
|
||||||
source.TokenEnvNames[0],
|
source.TokenEnvNames[0],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"GitLab release access requires authentication on %s; set %s (or %s) and retry",
|
"%s access requires authentication on %s; set %s (or %s) and retry",
|
||||||
source.BaseURL,
|
name,
|
||||||
|
target,
|
||||||
source.TokenEnvNames[0],
|
source.TokenEnvNames[0],
|
||||||
source.TokenEnvNames[1],
|
source.TokenEnvNames[1],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -78,52 +78,54 @@ func TestReleaseAssetURLResolvesRelativeLinks(t *testing.T) {
|
||||||
{Name: "graylog-mcp-linux-amd64", URL: "/downloads/graylog-mcp-linux-amd64"},
|
{Name: "graylog-mcp-linux-amd64", URL: "/downloads/graylog-mcp-linux-amd64"},
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := release.AssetURL("graylog-mcp-linux-amd64", "https://gitlab.example.com/api/v4/projects/1/releases/permalink/latest")
|
got, err := release.AssetURL("graylog-mcp-linux-amd64", "https://releases.example.com/latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("AssetURL: %v", err)
|
t.Fatalf("AssetURL: %v", err)
|
||||||
}
|
}
|
||||||
if got != "https://gitlab.example.com/downloads/graylog-mcp-linux-amd64" {
|
if got != "https://releases.example.com/downloads/graylog-mcp-linux-amd64" {
|
||||||
t.Fatalf("got %q", got)
|
t.Fatalf("got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveGitLabAuthPrefersExplicitToken(t *testing.T) {
|
func TestResolveAuthPrefersExplicitToken(t *testing.T) {
|
||||||
t.Setenv("GITLAB_TOKEN", "env-token")
|
t.Setenv("RELEASE_TOKEN", "env-token")
|
||||||
|
|
||||||
auth := ResolveGitLabAuth("explicit-token", GitLabSource{
|
auth := ResolveAuth("explicit-token", ReleaseSource{
|
||||||
BaseURL: "https://gitlab.example.com",
|
Name: "release endpoint",
|
||||||
ProjectPath: "group/project",
|
BaseURL: "https://releases.example.com",
|
||||||
TokenEnvNames: []string{"GITLAB_TOKEN", "GITLAB_PRIVATE_TOKEN"},
|
TokenHeader: "X-Release-Token",
|
||||||
|
TokenEnvNames: []string{"RELEASE_TOKEN", "RELEASE_PRIVATE_TOKEN"},
|
||||||
})
|
})
|
||||||
if auth.Header != "PRIVATE-TOKEN" {
|
if auth.Header != "X-Release-Token" {
|
||||||
t.Fatalf("header = %q, want PRIVATE-TOKEN", auth.Header)
|
t.Fatalf("header = %q, want X-Release-Token", auth.Header)
|
||||||
}
|
}
|
||||||
if auth.Token != "explicit-token" {
|
if auth.Token != "explicit-token" {
|
||||||
t.Fatalf("token = %q, want explicit token", auth.Token)
|
t.Fatalf("token = %q, want explicit token", auth.Token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveGitLabAuthReadsEnvironment(t *testing.T) {
|
func TestResolveAuthReadsEnvironment(t *testing.T) {
|
||||||
t.Setenv("GITLAB_PRIVATE_TOKEN", "env-token")
|
t.Setenv("RELEASE_PRIVATE_TOKEN", "env-token")
|
||||||
|
|
||||||
auth := ResolveGitLabAuth("", GitLabSource{
|
auth := ResolveAuth("", ReleaseSource{
|
||||||
BaseURL: "https://gitlab.example.com",
|
Name: "release endpoint",
|
||||||
ProjectPath: "group/project",
|
BaseURL: "https://releases.example.com",
|
||||||
TokenEnvNames: []string{"GITLAB_TOKEN", "GITLAB_PRIVATE_TOKEN"},
|
TokenHeader: "X-Release-Token",
|
||||||
|
TokenEnvNames: []string{"RELEASE_TOKEN", "RELEASE_PRIVATE_TOKEN"},
|
||||||
})
|
})
|
||||||
if auth.Header != "PRIVATE-TOKEN" {
|
if auth.Header != "X-Release-Token" {
|
||||||
t.Fatalf("header = %q, want PRIVATE-TOKEN", auth.Header)
|
t.Fatalf("header = %q, want X-Release-Token", auth.Header)
|
||||||
}
|
}
|
||||||
if auth.Token != "env-token" {
|
if auth.Token != "env-token" {
|
||||||
t.Fatalf("token = %q, want env token", auth.Token)
|
t.Fatalf("token = %q, want env token", auth.Token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchLatestReleaseAddsGitLabAuthHeader(t *testing.T) {
|
func TestFetchLatestReleaseAddsConfiguredAuthHeader(t *testing.T) {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
if got := r.Header.Get("PRIVATE-TOKEN"); got != "secret-token" {
|
if got := r.Header.Get("X-Release-Token"); got != "secret-token" {
|
||||||
t.Fatalf("PRIVATE-TOKEN = %q, want secret-token", got)
|
t.Fatalf("X-Release-Token = %q, want secret-token", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(Release{TagName: "v1.2.3"})
|
payload, err := json.Marshal(Release{TagName: "v1.2.3"})
|
||||||
|
|
@ -141,9 +143,9 @@ func TestFetchLatestReleaseAddsGitLabAuthHeader(t *testing.T) {
|
||||||
release, err := FetchLatestRelease(
|
release, err := FetchLatestRelease(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
client,
|
client,
|
||||||
"https://gitlab.example.com/latest",
|
"https://releases.example.com/latest",
|
||||||
Auth{Header: "PRIVATE-TOKEN", Token: "secret-token"},
|
Auth{Header: "X-Release-Token", Token: "secret-token"},
|
||||||
GitLabSource{BaseURL: "https://gitlab.example.com", ProjectPath: "group/project"},
|
ReleaseSource{Name: "release endpoint", BaseURL: "https://releases.example.com"},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("FetchLatestRelease: %v", err)
|
t.Fatalf("FetchLatestRelease: %v", err)
|
||||||
|
|
@ -153,7 +155,7 @@ func TestFetchLatestReleaseAddsGitLabAuthHeader(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchLatestReleaseHintsWhenGitLabAuthIsMissing(t *testing.T) {
|
func TestFetchLatestReleaseHintsWhenAuthIsMissing(t *testing.T) {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
|
|
@ -167,18 +169,18 @@ func TestFetchLatestReleaseHintsWhenGitLabAuthIsMissing(t *testing.T) {
|
||||||
_, err := FetchLatestRelease(
|
_, err := FetchLatestRelease(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
client,
|
client,
|
||||||
"https://gitlab.example.com/latest",
|
"https://releases.example.com/latest",
|
||||||
Auth{},
|
Auth{},
|
||||||
GitLabSource{
|
ReleaseSource{
|
||||||
BaseURL: "https://gitlab.example.com",
|
Name: "release endpoint",
|
||||||
ProjectPath: "group/project",
|
BaseURL: "https://releases.example.com",
|
||||||
TokenEnvNames: []string{"GITLAB_TOKEN", "GITLAB_PRIVATE_TOKEN"},
|
TokenEnvNames: []string{"RELEASE_TOKEN", "RELEASE_PRIVATE_TOKEN"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error")
|
t.Fatal("expected error")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "GITLAB_TOKEN") {
|
if !strings.Contains(err.Error(), "RELEASE_TOKEN") {
|
||||||
t.Fatalf("error = %v, want token hint", err)
|
t.Fatalf("error = %v, want token hint", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,10 +205,10 @@ func TestRunReplacesExecutableWithLatestArtifact(t *testing.T) {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
switch r.URL.String() {
|
switch r.URL.String() {
|
||||||
case "https://gitlab.example.com/latest":
|
case "https://releases.example.com/latest":
|
||||||
release := Release{TagName: "v1.2.3"}
|
release := Release{TagName: "v1.2.3"}
|
||||||
release.Assets.Links = []ReleaseLink{
|
release.Assets.Links = []ReleaseLink{
|
||||||
{Name: assetName, URL: "https://gitlab.example.com/artifact"},
|
{Name: assetName, URL: "https://releases.example.com/artifact"},
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(release)
|
payload, err := json.Marshal(release)
|
||||||
|
|
@ -218,7 +220,7 @@ func TestRunReplacesExecutableWithLatestArtifact(t *testing.T) {
|
||||||
Header: make(http.Header),
|
Header: make(http.Header),
|
||||||
Body: io.NopCloser(bytes.NewReader(payload)),
|
Body: io.NopCloser(bytes.NewReader(payload)),
|
||||||
}, nil
|
}, nil
|
||||||
case "https://gitlab.example.com/artifact":
|
case "https://releases.example.com/artifact":
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
Header: make(http.Header),
|
Header: make(http.Header),
|
||||||
|
|
@ -250,13 +252,13 @@ func TestRunReplacesExecutableWithLatestArtifact(t *testing.T) {
|
||||||
Client: client,
|
Client: client,
|
||||||
CurrentVersion: "v1.2.2",
|
CurrentVersion: "v1.2.2",
|
||||||
ExecutablePath: link,
|
ExecutablePath: link,
|
||||||
LatestReleaseURL: "https://gitlab.example.com/latest",
|
LatestReleaseURL: "https://releases.example.com/latest",
|
||||||
Stdout: &stdout,
|
Stdout: &stdout,
|
||||||
BinaryName: "graylog-mcp",
|
BinaryName: "graylog-mcp",
|
||||||
ReleaseSource: GitLabSource{
|
ReleaseSource: ReleaseSource{
|
||||||
BaseURL: "https://gitlab.example.com",
|
Name: "release endpoint",
|
||||||
ProjectPath: "group/project",
|
BaseURL: "https://releases.example.com",
|
||||||
TokenEnvNames: []string{"GITLAB_TOKEN", "GITLAB_PRIVATE_TOKEN"},
|
TokenEnvNames: []string{"RELEASE_TOKEN", "RELEASE_PRIVATE_TOKEN"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -290,10 +292,10 @@ func TestRunSkipsWhenAlreadyOnLatestRelease(t *testing.T) {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
switch r.URL.String() {
|
switch r.URL.String() {
|
||||||
case "https://gitlab.example.com/latest":
|
case "https://releases.example.com/latest":
|
||||||
release := Release{TagName: "v1.2.3"}
|
release := Release{TagName: "v1.2.3"}
|
||||||
release.Assets.Links = []ReleaseLink{
|
release.Assets.Links = []ReleaseLink{
|
||||||
{Name: assetName, URL: "https://gitlab.example.com/artifact"},
|
{Name: assetName, URL: "https://releases.example.com/artifact"},
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(release)
|
payload, err := json.Marshal(release)
|
||||||
|
|
@ -305,7 +307,7 @@ func TestRunSkipsWhenAlreadyOnLatestRelease(t *testing.T) {
|
||||||
Header: make(http.Header),
|
Header: make(http.Header),
|
||||||
Body: io.NopCloser(bytes.NewReader(payload)),
|
Body: io.NopCloser(bytes.NewReader(payload)),
|
||||||
}, nil
|
}, nil
|
||||||
case "https://gitlab.example.com/artifact":
|
case "https://releases.example.com/artifact":
|
||||||
downloaded = true
|
downloaded = true
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: http.StatusOK,
|
StatusCode: http.StatusOK,
|
||||||
|
|
@ -333,13 +335,13 @@ func TestRunSkipsWhenAlreadyOnLatestRelease(t *testing.T) {
|
||||||
Client: client,
|
Client: client,
|
||||||
CurrentVersion: "v1.2.3",
|
CurrentVersion: "v1.2.3",
|
||||||
ExecutablePath: target,
|
ExecutablePath: target,
|
||||||
LatestReleaseURL: "https://gitlab.example.com/latest",
|
LatestReleaseURL: "https://releases.example.com/latest",
|
||||||
Stdout: &stdout,
|
Stdout: &stdout,
|
||||||
BinaryName: "graylog-mcp",
|
BinaryName: "graylog-mcp",
|
||||||
ReleaseSource: GitLabSource{
|
ReleaseSource: ReleaseSource{
|
||||||
BaseURL: "https://gitlab.example.com",
|
Name: "release endpoint",
|
||||||
ProjectPath: "group/project",
|
BaseURL: "https://releases.example.com",
|
||||||
TokenEnvNames: []string{"GITLAB_TOKEN", "GITLAB_PRIVATE_TOKEN"},
|
TokenEnvNames: []string{"RELEASE_TOKEN", "RELEASE_PRIVATE_TOKEN"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -361,3 +363,21 @@ func TestRunSkipsWhenAlreadyOnLatestRelease(t *testing.T) {
|
||||||
t.Fatalf("stdout = %q, want up-to-date message", stdout.String())
|
t.Fatalf("stdout = %q, want up-to-date message", stdout.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunRequiresLatestReleaseURL(t *testing.T) {
|
||||||
|
target := filepath.Join(t.TempDir(), "graylog-mcp")
|
||||||
|
if err := os.WriteFile(target, []byte("current-binary"), 0o755); err != nil {
|
||||||
|
t.Fatalf("WriteFile target: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Run(context.Background(), Options{
|
||||||
|
BinaryName: "graylog-mcp",
|
||||||
|
ExecutablePath: target,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "latest release URL") {
|
||||||
|
t.Fatalf("error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue