From a657e9f446e70a0e31ca1c176ea2ffde98bee3a3 Mon Sep 17 00:00:00 2001 From: Akshay Pant Date: Fri, 15 May 2026 16:54:42 +0530 Subject: [PATCH] feat(gitea): implement GetTaskURI for remote task resolution Enable Gitea/Forgejo provider to resolve remote taskRef URLs using the provider's authenticated API instead of returning "not supported". Supports branch, tag, and commit SHA URL formats. Signed-off-by: Akshay Pant Assisted-by: Claude Opus 4.6 --- pkg/provider/gitea/gitea.go | 63 +++++- pkg/provider/gitea/gitea_test.go | 181 ++++++++++++++++++ test/gitea_test.go | 106 ++++++++++ .../pipelinerun_remote_task_on_gitea.yaml | 24 +++ 4 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 test/testdata/pipelinerun_remote_task_on_gitea.yaml diff --git a/pkg/provider/gitea/gitea.go b/pkg/provider/gitea/gitea.go index 95a3ccf1f8..5ebadc76cf 100644 --- a/pkg/provider/gitea/gitea.go +++ b/pkg/provider/gitea/gitea.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "path" "regexp" "strconv" @@ -149,9 +150,65 @@ func (v *Provider) SetPacInfo(pacInfo *info.PacOpts) { v.pacInfo = pacInfo } -// GetTaskURI TODO: Implement ME. -func (v *Provider) GetTaskURI(_ context.Context, _ *info.Event, _ string) (bool, string, error) { - return false, "", nil +// splitGiteaURL parses a Gitea/Forgejo URL and returns org, repo, path, and ref. +func splitGiteaURL(uri string) (string, string, string, string, error) { + pURL, err := url.Parse(uri) + if err != nil { + return "", "", "", "", fmt.Errorf("URL %s is not a valid provider URL: %w", uri, err) + } + split := strings.Split(pURL.EscapedPath(), "/") + // minimum: /owner/repo/{src|raw}/{branch|tag|commit}/ref/filepath → 7 segments (split[0] is empty) + if len(split) < 7 { + return "", "", "", "", fmt.Errorf("URL %s does not seem to be a proper Gitea URL: not enough path segments", uri) + } + + spOrg := split[1] + spRepo := split[2] + + if split[3] != "src" && split[3] != "raw" { + return "", "", "", "", fmt.Errorf("cannot recognize URL as a Gitea URL to fetch: %s (expected 'src' or 'raw' in path)", uri) + } + if split[4] != "branch" && split[4] != "tag" && split[4] != "commit" { + return "", "", "", "", fmt.Errorf("cannot recognize URL as a Gitea URL to fetch: %s (expected 'branch', 'tag', or 'commit' in path)", uri) + } + + spRef := split[5] + spPath := strings.Join(split[6:], "/") + + if spRef, err = url.PathUnescape(spRef); err != nil { + return "", "", "", "", fmt.Errorf("cannot decode ref: %w", err) + } + if spPath, err = url.PathUnescape(spPath); err != nil { + return "", "", "", "", fmt.Errorf("cannot decode path: %w", err) + } + if spOrg, err = url.PathUnescape(spOrg); err != nil { + return "", "", "", "", fmt.Errorf("cannot decode org: %w", err) + } + if spRepo, err = url.PathUnescape(spRepo); err != nil { + return "", "", "", "", fmt.Errorf("cannot decode repo: %w", err) + } + + return spOrg, spRepo, spPath, spRef, nil +} + +func (v *Provider) GetTaskURI(_ context.Context, event *info.Event, uri string) (bool, string, error) { + if v.giteaClient == nil { + return false, "", fmt.Errorf("no gitea client has been initialized") + } + if ret := provider.CompareHostOfURLS(uri, event.URL); !ret { + return false, "", nil + } + + spOrg, spRepo, spPath, spRef, err := splitGiteaURL(uri) + if err != nil { + return false, "", err + } + + data, _, err := v.Client().GetFile(spOrg, spRepo, spRef, spPath) + if err != nil { + return false, "", err + } + return true, string(data), nil } func (v *Provider) SetLogger(logger *zap.SugaredLogger) { diff --git a/pkg/provider/gitea/gitea_test.go b/pkg/provider/gitea/gitea_test.go index cb1f0d7316..688ce8f521 100644 --- a/pkg/provider/gitea/gitea_test.go +++ b/pkg/provider/gitea/gitea_test.go @@ -1220,6 +1220,187 @@ func TestGetCommitInfoPRLookupPopulatesURLs(t *testing.T) { assert.Equal(t, "https://gitea.com/owner/repo", event.BaseURL, "BaseURL should be populated from PR lookup") } +func TestSplitGiteaURL(t *testing.T) { + tests := []struct { + name string + url string + wantOrg string + wantRepo string + wantRef string + wantPath string + wantErr bool + }{ + { + name: "src branch URL", + url: "https://gitea.example.com/owner/repo/src/branch/main/path/to/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "main", + wantPath: "path/to/task.yaml", + }, + { + name: "raw branch URL", + url: "https://gitea.example.com/owner/repo/raw/branch/main/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "main", + wantPath: "task.yaml", + }, + { + name: "src tag URL", + url: "https://gitea.example.com/owner/repo/src/tag/v1.0.0/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "v1.0.0", + wantPath: "task.yaml", + }, + { + name: "src commit URL", + url: "https://gitea.example.com/owner/repo/src/commit/abc123def/path/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "abc123def", + wantPath: "path/task.yaml", + }, + { + name: "URL encoded branch name", + url: "https://gitea.example.com/owner/repo/src/branch/feature%2Fbranch/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "feature/branch", + wantPath: "task.yaml", + }, + { + name: "raw commit URL", + url: "https://gitea.example.com/owner/repo/raw/commit/abc123def/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "abc123def", + wantPath: "task.yaml", + }, + { + name: "raw tag URL", + url: "https://gitea.example.com/owner/repo/raw/tag/v2.0/path/to/task.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "v2.0", + wantPath: "path/to/task.yaml", + }, + { + name: "URL encoded path", + url: "https://gitea.example.com/owner/repo/src/branch/main/path%2Fto%2Ftask.yaml", + wantOrg: "owner", + wantRepo: "repo", + wantRef: "main", + wantPath: "path/to/task.yaml", + }, + { + name: "too short URL", + url: "https://gitea.example.com/owner/repo", + wantErr: true, + }, + { + name: "invalid action segment", + url: "https://gitea.example.com/owner/repo/blob/branch/main/task.yaml", + wantErr: true, + }, + { + name: "invalid ref type", + url: "https://gitea.example.com/owner/repo/src/invalid/main/task.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + org, repo, path, ref, err := splitGiteaURL(tt.url) + if tt.wantErr { + assert.Assert(t, err != nil) + return + } + assert.NilError(t, err) + assert.Equal(t, tt.wantOrg, org) + assert.Equal(t, tt.wantRepo, repo) + assert.Equal(t, tt.wantRef, ref) + assert.Equal(t, tt.wantPath, path) + }) + } +} + +func TestGetTaskURI(t *testing.T) { + tests := []struct { + name string + eventURL string + uri string + wantRet string + wantFound bool + wantErr bool + fileExists bool + }{ + { + name: "fetch task from src branch URL", + eventURL: "https://gitea.example.com/owner/repo/pulls/1", + uri: "https://gitea.example.com/owner/repo/src/branch/main/task.yaml", + wantRet: "hello world", + wantFound: true, + fileExists: true, + }, + { + name: "fetch task from raw branch URL", + eventURL: "https://gitea.example.com/owner/repo/pulls/1", + uri: "https://gitea.example.com/owner/repo/raw/branch/main/task.yaml", + wantRet: "hello world", + wantFound: true, + fileExists: true, + }, + { + name: "fetch task from tag URL", + eventURL: "https://gitea.example.com/owner/repo/pulls/1", + uri: "https://gitea.example.com/owner/repo/src/tag/v1.0/task.yaml", + wantRet: "hello world", + wantFound: true, + fileExists: true, + }, + { + name: "different host returns not found", + eventURL: "https://gitea.example.com/owner/repo/pulls/1", + uri: "https://other.example.com/owner/repo/src/branch/main/task.yaml", + wantFound: false, + }, + { + name: "bad URI format", + eventURL: "https://gitea.example.com/owner/repo/pulls/1", + uri: "https://gitea.example.com/owner/repo", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeclient, mux, teardown := tgitea.Setup(t) + defer teardown() + + if tt.fileExists { + mux.HandleFunc("/repos/owner/repo/raw/", func(rw http.ResponseWriter, _ *http.Request) { + fmt.Fprint(rw, tt.wantRet) + }) + } + + p := &Provider{giteaClient: fakeclient} + event := info.NewEvent() + event.URL = tt.eventURL + + found, content, err := p.GetTaskURI(context.Background(), event, tt.uri) + if (err != nil) != tt.wantErr { + t.Errorf("GetTaskURI() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.wantFound, found) + if tt.wantFound { + assert.Equal(t, tt.wantRet, content) + } + }) + } +} + func TestGetCommitStatuses(t *testing.T) { tests := []struct { name string diff --git a/test/gitea_test.go b/test/gitea_test.go index e2d1b644cc..271dc1ce45 100644 --- a/test/gitea_test.go +++ b/test/gitea_test.go @@ -4,6 +4,7 @@ package test import ( "context" + "encoding/base64" "fmt" "os" "path/filepath" @@ -68,6 +69,97 @@ func TestGiteaPullRequestTaskAnnotations(t *testing.T) { defer f() } +// TestGiteaGetTaskURI verifies that remote tasks hosted on the same Gitea +// instance are fetched using the provider's authenticated GetTaskURI path +// rather than falling back to unauthenticated HTTP. +func TestGiteaGetTaskURI(t *testing.T) { + ctx := context.Background() + runcnx, opts, giteacnx, err := tgitea.Setup(ctx) + assert.NilError(t, err) + + remoteRepoName := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("remote-task-repo") + hookURL := os.Getenv("TEST_GITEA_SMEEURL") + webhookSecret := os.Getenv("TEST_EL_WEBHOOK_SECRET") + remoteRepo, err := tgitea.CreateGiteaRepo( + giteacnx.Client(), opts.Organization, + remoteRepoName, options.MainBranch, hookURL, webhookSecret, + false, runcnx.Clients.Log) + assert.NilError(t, err) + + defer func() { + if os.Getenv("TEST_NOCLEANUP") != "true" { + _, _ = giteacnx.Client().DeleteRepo(opts.Organization, remoteRepoName) + } + }() + + taskFiles := []struct { + remoteFile string + refType string + }{ + {"task-branch.yaml", "branch"}, + {"task-tag.yaml", "tag"}, + {"task-commit.yaml", "commit"}, + } + var commitSHA string + for _, tf := range taskFiles { + content := remoteTaskYAML(tf.refType) + fr, _, createErr := giteacnx.Client().CreateFile( + opts.Organization, remoteRepoName, tf.remoteFile, + forgejo.CreateFileOptions{ + Content: base64.StdEncoding.EncodeToString([]byte(content)), + FileOptions: forgejo.FileOptions{ + Message: "Add " + tf.remoteFile, + BranchName: options.MainBranch, + }, + }) + assert.NilError(t, createErr) + if tf.refType == "commit" { + commitSHA = fr.Commit.SHA + } + } + + tagName := "v0.0.1" + _, _, err = giteacnx.Client().CreateTag(opts.Organization, remoteRepoName, forgejo.CreateTagOption{ + TagName: tagName, + Target: options.MainBranch, + }) + assert.NilError(t, err) + + branchURL := fmt.Sprintf("%s/raw/branch/%s/task-branch.yaml", remoteRepo.HTMLURL, options.MainBranch) + tagURL := fmt.Sprintf("%s/src/tag/%s/task-tag.yaml", remoteRepo.HTMLURL, tagName) + commitURL := fmt.Sprintf("%s/raw/commit/%s/task-commit.yaml", remoteRepo.HTMLURL, commitSHA) + + runcnx.Clients.Log.Infof("Remote task URLs: branch=%s tag=%s commit=%s", branchURL, tagURL, commitURL) + + topts := &tgitea.TestOpts{ + Regexp: successRegexp, + TargetEvent: triggertype.PullRequest.String(), + YAMLFiles: map[string]string{ + ".tekton/pr.yaml": "testdata/pipelinerun_remote_task_on_gitea.yaml", + }, + CheckForStatus: "success", + ExtraArgs: map[string]string{ + "RemoteTaskBranchURL": branchURL, + "RemoteTaskTagURL": tagURL, + "RemoteTaskCommitURL": commitURL, + }, + ParamsRun: runcnx, + Opts: opts, + GiteaCNX: giteacnx, + } + _, f := tgitea.TestPR(t, topts) + defer f() + + for _, tf := range taskFiles { + err = twait.RegexpMatchingInPodLog(ctx, runcnx, topts.TargetNS, + fmt.Sprintf("tekton.dev/pipelineTask=task-from-%s", tf.refType), + "step-echo", + *regexp.MustCompile(fmt.Sprintf("Hello from %s ref", tf.refType)), + "", 2, nil) + assert.NilError(t, err, "task-from-%s did not produce expected log output", tf.refType) + } +} + func TestGiteaUseDisplayName(t *testing.T) { topts := &tgitea.TestOpts{ Regexp: regexp.MustCompile(`.*The Task name is Task.*`), @@ -1297,6 +1389,20 @@ func TestGiteaHubTaskNotFound(t *testing.T) { } } +func remoteTaskYAML(refType string) string { + return fmt.Sprintf(`apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: remote-task-gitea-%s +spec: + steps: + - name: echo + image: registry.access.redhat.com/ubi10/ubi-micro + script: | + echo "Hello from %s ref" +`, refType, refType) +} + // Local Variables: // compile-command: "go test -tags=e2e -v -run TestGiteaPush ." // End: diff --git a/test/testdata/pipelinerun_remote_task_on_gitea.yaml b/test/testdata/pipelinerun_remote_task_on_gitea.yaml new file mode 100644 index 0000000000..e3bf6e9102 --- /dev/null +++ b/test/testdata/pipelinerun_remote_task_on_gitea.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: "pipelinerun-remote-task-on-gitea" + annotations: + pipelinesascode.tekton.dev/target-namespace: "\\ .TargetNamespace //" + pipelinesascode.tekton.dev/on-target-branch: "[\\ .TargetBranch //]" + pipelinesascode.tekton.dev/on-event: "[\\ .TargetEvent //]" + pipelinesascode.tekton.dev/task: "[\\ .RemoteTaskBranchURL //]" + pipelinesascode.tekton.dev/task-1: "[\\ .RemoteTaskTagURL //]" + pipelinesascode.tekton.dev/task-2: "[\\ .RemoteTaskCommitURL //]" +spec: + pipelineSpec: + tasks: + - name: task-from-branch + taskRef: + name: remote-task-gitea-branch + - name: task-from-tag + taskRef: + name: remote-task-gitea-tag + - name: task-from-commit + taskRef: + name: remote-task-gitea-commit