diff --git a/docs/docs/30-administration/11-forges/10-overview.md b/docs/docs/30-administration/11-forges/10-overview.md index 70305aec2e6..597c5f689b1 100644 --- a/docs/docs/30-administration/11-forges/10-overview.md +++ b/docs/docs/30-administration/11-forges/10-overview.md @@ -8,7 +8,7 @@ | Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | | Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Event: Deploy | :white_check_mark: | :x: | :x: | :x: | -| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | +| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | ¹ for pull requests at least Gitea version 1.17 is required diff --git a/docs/docs/92-awesome.md b/docs/docs/92-awesome.md index 17f55ab7eea..9577e3c400c 100644 --- a/docs/docs/92-awesome.md +++ b/docs/docs/92-awesome.md @@ -40,6 +40,8 @@ If you have some missing resources, please feel free to [open a pull-request](ht - [Woodpecker CI @ Codeberg](https://www.sarkasti.eu/articles/post/woodpecker/) - [Deploy Docker/Compose using Woodpecker CI](https://hinty.io/vverenko/deploy-docker-compose-using-woodpecker-ci/) - [Installing Woodpecker CI in your personal homelab](https://pwa.io/articles/installing-woodpecker-in-your-homelab/) +- [Locally Cached Nix CI with Woodpecker](https://blog.kotatsu.dev/posts/2023-04-21-woodpecker-nix-caching/) +- [How to run Cypress auto-tests on Woodpecker CI and report results to Slack](https://devforth.io/blog/how-to-run-cypress-auto-tests-on-woodpecker-ci-and-report-results-to-slack/) ## Videos diff --git a/server/forge/bitbucket/bitbucket.go b/server/forge/bitbucket/bitbucket.go index 96c73aec894..a8e6ca3a873 100644 --- a/server/forge/bitbucket/bitbucket.go +++ b/server/forge/bitbucket/bitbucket.go @@ -20,17 +20,17 @@ import ( "fmt" "net/http" "net/url" + "path/filepath" "golang.org/x/oauth2" - shared_utils "github.com/woodpecker-ci/woodpecker/shared/utils" - "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/forge" "github.com/woodpecker-ci/woodpecker/server/forge/bitbucket/internal" "github.com/woodpecker-ci/woodpecker/server/forge/common" forge_types "github.com/woodpecker-ci/woodpecker/server/forge/types" "github.com/woodpecker-ci/woodpecker/server/model" + shared_utils "github.com/woodpecker-ci/woodpecker/shared/utils" ) // Bitbucket cloud endpoints. @@ -223,8 +223,52 @@ func (c *config) File(ctx context.Context, u *model.User, r *model.Repo, p *mode return []byte(*config), err } -func (c *config) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ string) ([]*forge_types.FileMeta, error) { - return nil, forge_types.ErrNotImplemented +// Dir fetches a folder from the bitbucket repository +func (c *config) Dir(ctx context.Context, u *model.User, r *model.Repo, p *model.Pipeline, f string) ([]*forge_types.FileMeta, error) { + var page *string + repoPathFiles := []*forge_types.FileMeta{} + client := c.newClient(ctx, u) + for { + filesResp, err := client.GetRepoFiles(r.Owner, r.Name, p.Commit, f, page) + if err != nil { + return nil, err + } + for _, file := range filesResp.Values { + _, filename := filepath.Split(file.Path) + repoFile := forge_types.FileMeta{ + Name: filename, + } + if file.Type == "commit_file" { + fileData, err := c.newClient(ctx, u).FindSource(r.Owner, r.Name, p.Commit, file.Path) + if err != nil { + return nil, err + } + if fileData != nil { + repoFile.Data = []byte(*fileData) + } + } + repoPathFiles = append(repoPathFiles, &repoFile) + } + + // Check for more results page + if filesResp.Next == nil { + break + } + nextPageURL, err := url.Parse(*filesResp.Next) + if err != nil { + return nil, err + } + params, err := url.ParseQuery(nextPageURL.RawQuery) + if err != nil { + return nil, err + } + nextPage := params.Get("page") + if len(nextPage) == 0 { + break + } + page = &nextPage + } + return repoPathFiles, nil } // Status creates a pipeline status for the Bitbucket commit. diff --git a/server/forge/bitbucket/bitbucket_test.go b/server/forge/bitbucket/bitbucket_test.go index c5a6d59e212..b616e0db8a4 100644 --- a/server/forge/bitbucket/bitbucket_test.go +++ b/server/forge/bitbucket/bitbucket_test.go @@ -178,6 +178,50 @@ func Test_bitbucket(t *testing.T) { }) }) + g.Describe("When requesting repo branch HEAD", func() { + g.It("Should return the details", func() { + branchHead, err := c.BranchHead(ctx, fakeUser, fakeRepo, "branch_name") + g.Assert(err).IsNil() + g.Assert(branchHead).Equal("branch_head_name") + }) + g.It("Should handle not found errors", func() { + _, err := c.BranchHead(ctx, fakeUser, fakeRepo, "branch_not_found") + g.Assert(err).IsNotNil() + }) + }) + + g.Describe("When requesting repo pull requests", func() { + listOpts := model.ListOptions{ + All: false, + Page: 1, + PerPage: 10, + } + g.It("Should return the details", func() { + repoPRs, err := c.PullRequests(ctx, fakeUser, fakeRepo, &listOpts) + g.Assert(err).IsNil() + g.Assert(repoPRs[0].Title).Equal("PRs title") + g.Assert(repoPRs[0].Index).Equal(int64(123)) + }) + g.It("Should handle not found errors", func() { + _, err := c.PullRequests(ctx, fakeUser, fakeRepoNotFound, &listOpts) + g.Assert(err).IsNotNil() + }) + }) + + g.Describe("When requesting repo directory contents", func() { + g.It("Should return the details", func() { + files, err := c.Dir(ctx, fakeUser, fakeRepo, fakePipeline, "/dir") + g.Assert(err).IsNil() + g.Assert(len(files)).Equal(3) + g.Assert(files[0].Name).Equal("README.md") + g.Assert(string(files[0].Data)).Equal("dummy payload") + }) + g.It("Should handle not found errors", func() { + _, err := c.Dir(ctx, fakeUser, fakeRepo, fakePipeline, "/dir_not_found") + g.Assert(err).IsNotNil() + }) + }) + g.Describe("When activating a repository", func() { g.It("Should error when malformed hook", func() { err := c.Activate(ctx, fakeUser, fakeRepo, "%gh&%ij") diff --git a/server/forge/bitbucket/fixtures/handler.go b/server/forge/bitbucket/fixtures/handler.go index 810a4168167..b200bb219d7 100644 --- a/server/forge/bitbucket/fixtures/handler.go +++ b/server/forge/bitbucket/fixtures/handler.go @@ -38,7 +38,8 @@ func Handler() http.Handler { e.GET("/2.0/repositories/:owner", getUserRepos) e.GET("/2.0/user/", getUser) e.GET("/2.0/user/permissions/repositories", getPermissions) - + e.GET("/2.0/repositories/:owner/:name/commits/:commit", getBranchHead) + e.GET("/2.0/repositories/:owner/:name/pullrequests", getPullRequests) return e } @@ -108,6 +109,10 @@ func getRepoHooks(c *gin.Context) { func getRepoFile(c *gin.Context) { switch c.Param("file") { + case "dir": + c.String(200, repoDirPayload) + case "dir_not_found/": + c.String(404, "") case "file_not_found": c.String(404, "") default: @@ -115,6 +120,24 @@ func getRepoFile(c *gin.Context) { } } +func getBranchHead(c *gin.Context) { + switch c.Param("commit") { + case "branch_name": + c.String(200, branchCommitsPayload) + default: + c.String(404, "") + } +} + +func getPullRequests(c *gin.Context) { + switch c.Param("name") { + case "repo_name": + c.String(200, pullRequestsPayload) + default: + c.String(404, "") + } +} + func createRepoStatus(c *gin.Context) { switch c.Param("name") { case "repo_not_found": @@ -223,6 +246,61 @@ const repoHookPayload = ` const repoFilePayload = "dummy payload" +const repoDirPayload = ` +{ + "pagelen": 10, + "page": 1, + "values": [ + { + "path": "README.md", + "type": "commit_file" + }, + { + "path": "test", + "type": "commit_directory" + }, + { + "path": ".gitignore", + "type": "commit_file" + } + ] +} +` + +const branchCommitsPayload = ` +{ + "values": [ + { + "hash": "branch_head_name" + }, + { + "hash": "random1" + }, + { + "hash": "random2" + } + ] +} +` + +const pullRequestsPayload = ` +{ + "values": [ + { + "id": 123, + "title": "PRs title" + }, + { + "id": 456, + "title": "Another PRs title" + } + ], + "pagelen": 10, + "size": 2, + "page": 1 +} +` + const userPayload = ` { "username": "superman", diff --git a/server/forge/bitbucket/internal/client.go b/server/forge/bitbucket/internal/client.go index 13fe875ed7f..35e309f6b22 100644 --- a/server/forge/bitbucket/internal/client.go +++ b/server/forge/bitbucket/internal/client.go @@ -48,6 +48,7 @@ const ( pathOrgPerms = "%s/2.0/workspaces/%s/permissions?%s" pathPullRequests = "%s/2.0/repositories/%s/%s/pullrequests" pathBranchCommits = "%s/2.0/repositories/%s/%s/commits/%s" + pathDir = "%s/2.0/repositories/%s/%s/src/%s%s" ) type Client struct { @@ -233,6 +234,16 @@ func (c *Client) GetWorkspace(name string) (*Workspace, error) { return out, err } +func (c *Client) GetRepoFiles(owner, name, revision, path string, page *string) (*DirResp, error) { + out := new(DirResp) + uri := fmt.Sprintf(pathDir, c.base, owner, name, revision, path) + if page != nil { + uri += "?page=" + *page + } + _, err := c.do(uri, get, nil, out) + return out, err +} + func (c *Client) do(rawurl, method string, in, out interface{}) (*string, error) { uri, err := url.Parse(rawurl) if err != nil { diff --git a/server/forge/bitbucket/internal/types.go b/server/forge/bitbucket/internal/types.go index ff2f77f013b..8bb4b10dd8f 100644 --- a/server/forge/bitbucket/internal/types.go +++ b/server/forge/bitbucket/internal/types.go @@ -283,3 +283,16 @@ type CommitsResp struct { type Commit struct { Hash string `json:"hash"` } + +type DirResp struct { + Page uint `json:"page"` + PageLen uint `json:"pagelen"` + Next *string `json:"next"` + Values []*Dir `json:"values"` +} + +type Dir struct { + Path string `json:"path"` + Type string `json:"type"` + Size uint `json:"size"` +} diff --git a/web/src/assets/locales/de.json b/web/src/assets/locales/de.json index 0ff8f135146..4d2c0ffba4d 100644 --- a/web/src/assets/locales/de.json +++ b/web/src/assets/locales/de.json @@ -161,45 +161,19 @@ "branches": "Branches", "build": { "actions": { - "cancel": "Abbrechen", - "cancel_success": "Pipeline abgebrochen", - "canceled": "Dieser Schritt wurde abgebrochen.", "log_auto_scroll": "Automatisches folgen", - "log_auto_scroll_off": "Schalte automatisches folgen aus", - "log_download": "Herunterladen", - "restart": "Neustarten", "restart_success": "Pipeline neu gestartet" }, - "config": "Konfiguration", - "created": "Erstellt", "event": { - "cron": "Cron", "deploy": "Deploy", - "manual": "Manuell", - "pr": "Pull-Request", - "push": "Push", "tag": "Tag" }, - "execution_error": "Ausführungsfehler", - "exit_code": "Exit-Code {exitCode}", - "files": "Geänderte Dateien ({files})", "loading": "Laden…", - "log_download_error": "Beim Herunterladen der Log-Datei ist ein Fehler aufgetreten.", - "no_files": "Es wurden keine Dateien geändert.", - "no_pipeline_steps": "Keine Schritte in der Pipeline vorhanden!", "no_pipelines": "Bisher wurden noch keine Pipelines gestartet.", - "pipeline": "Pipeline #{buildId}", - "pipelines_for": "Pipelines für den Branch \"{branch}\"", "protected": { - "approve": "Genehmigen", "approve_success": "Pipeline genehmigt", - "awaits": "Diese Pipeline wartet auf die Genehmigung durch einen Maintainer!", - "decline": "Ablehnen", - "decline_success": "Pipeline abgelehnt", "declined": "Diese Pipeline ist abgelehnt worden!" - }, - "step_not_started": "Dieser Schritt hat noch nicht begonnen.", - "tasks": "Vorgänge" + } }, "deploy_pipeline": { "enter_target": "Zielumgebung des Deployments", @@ -316,12 +290,10 @@ "badge": { "badge": "Abzeichen", "branch": "Branch", - "markdown": "Markdown", "type": "Syntax", "type_html": "HTML", "type_markdown": "Markdown", - "type_url": "URL", - "url_branch": "URL für bestimmten Branch" + "type_url": "URL" }, "crons": { "add": "Cron hinzufügen", diff --git a/web/src/assets/locales/fr.json b/web/src/assets/locales/fr.json index ff9de98221d..6c7d5ef2418 100644 --- a/web/src/assets/locales/fr.json +++ b/web/src/assets/locales/fr.json @@ -161,44 +161,19 @@ "branches": "Branches", "build": { "actions": { - "cancel": "Annuler", - "cancel_success": "Pipeline annulé", - "canceled": "Cette étape a été annulée.", "log_auto_scroll": "Automatiquement défiler vers le bas", - "log_auto_scroll_off": "Désactiver le défilement automatique", - "log_download": "Télécharger", - "restart": "Redémarrer", "restart_success": "Pipeline redémarré" }, - "config": "Configuration", - "created": "Crée", "event": { - "cron": "Tache périodique", - "deploy": "Déploiement", - "pr": "Pull Request", - "push": "Push", - "tag": "Tag" + "deploy": "Déploiement" }, "execution_error": "Erreur d'execution", - "exit_code": "Code de retour {exitCode}", - "files": "Fichiers changés ({files})", - "loading": "Chargement…", "log_download_error": "Il y a eu une erreur lors du téléchargement du fichier de journal", - "no_files": "Aucun fichier n'a été modifié.", - "no_pipeline_steps": "Aucune étape disponible !", - "no_pipelines": "Aucun pipeline n'a démarré pour le moment.", "pipeline": "Pipeline #{buildId}", - "pipelines_for": "Pipelines pour la branche \"{branch}\"", "protected": { - "approve": "Approuver", - "approve_success": "Pipeline approuvé", - "awaits": "Ce pipeline attends d'être approuvé par un mainteneur !", - "decline": "Refuser", - "decline_success": "Pipeline refusé", - "declined": "Le pipeline a été refusé !" + "awaits": "Ce pipeline attends d'être approuvé par un mainteneur !" }, - "step_not_started": "L'étape n'a pas démarré encore.", - "tasks": "Tâches" + "step_not_started": "L'étape n'a pas démarré encore." }, "deploy_pipeline": { "enter_target": "Environnement de déploiement ciblé", @@ -315,12 +290,10 @@ "badge": { "badge": "Badge", "branch": "Branche", - "markdown": "Markdown", "type": "Syntaxe", "type_html": "HTML", "type_markdown": "Markdown", - "type_url": "URL", - "url_branch": "URL pour la branche spécifique" + "type_url": "URL" }, "crons": { "add": "Ajouter une tâche planifiée", diff --git a/web/src/assets/locales/it.json b/web/src/assets/locales/it.json index c80845c5b6b..4799e6fcf91 100644 --- a/web/src/assets/locales/it.json +++ b/web/src/assets/locales/it.json @@ -47,11 +47,6 @@ "activity": "Attività", "add": "Aggiungi repository", "branches": "Branch", - "build": { - "actions": { - "cancel": "Annulla" - } - }, "deploy_pipeline": { "enter_target": "Ambiente di deploy destinazione", "title": "Azione l'evento di deploy per la pipeline corrente #{pipelineId}", diff --git a/web/src/assets/locales/lv.json b/web/src/assets/locales/lv.json index 2b288311489..0561d925c33 100644 --- a/web/src/assets/locales/lv.json +++ b/web/src/assets/locales/lv.json @@ -161,44 +161,19 @@ "branches": "Atzari", "build": { "actions": { - "cancel": "Atcelt", - "cancel_success": "Konvejerdarbs atcelts", - "canceled": "Šis solis tika atcelts.", "log_auto_scroll": "Automātiski ritināt", - "log_auto_scroll_off": "Atslēgt automātisko ritināšanu", - "log_download": "Lejupielādēt", - "restart": "Pārstartēt", "restart_success": "Konvejerdarbs pārstartēts" }, - "config": "Konfigurācija", - "created": "Izveidots", "event": { - "cron": "Plānotais darbs", - "deploy": "Uzstādīšana", - "pr": "Izmaiņu pieprasījums", - "push": "Iesūtīšana", - "tag": "Tags" + "deploy": "Uzstādīšana" }, "execution_error": "Uzdevuma izpildes kļūda", - "exit_code": "iziešanas kods {exitCode}", - "files": "Izmainītie faili ({files})", - "loading": "Notiek ielāde…", "log_download_error": "Veicot žurnālfaila lejupielādi notika kļūda", - "no_files": "Neviens fails nav mainīts.", - "no_pipeline_steps": "Konvejerdarbam nav neviena soļa!", - "no_pipelines": "Neviens konvejerdarbs vēl nav uzsākts.", "pipeline": "Konvejerdarbs #{buildId}", - "pipelines_for": "Konvejerdarbi atzaram \"{branch}\"", "protected": { - "approve": "Apstiprināt", - "approve_success": "Konvejerdarbs apstiprināts", - "awaits": "Šim konvejerdarbam ir nepieciešams apstiprinājums no atbildīgajām personām!", - "decline": "Noraidīt", - "decline_success": "Konvejerdarbs noraidīts", - "declined": "Šis konvejerdarbs tika noraidīts!" + "awaits": "Šim konvejerdarbam ir nepieciešams apstiprinājums no atbildīgajām personām!" }, - "step_not_started": "Šis solis vēl nav uzsākts.", - "tasks": "Uzdevumi" + "step_not_started": "Šis solis vēl nav uzsākts." }, "deploy_pipeline": { "enter_target": "Mērķa uzstādīšanas vide", @@ -315,12 +290,10 @@ "badge": { "badge": "Nozīmīte", "branch": "Atzars", - "markdown": "Markdown", "type": "Pieraksta veids", "type_html": "HTML", "type_markdown": "Markdown", - "type_url": "URL", - "url_branch": "URL konkrētam atzaram" + "type_url": "URL" }, "crons": { "add": "Pievienot plānoto darbu", diff --git a/web/src/assets/locales/zh-Hans.json b/web/src/assets/locales/zh-Hans.json index 823a3d59deb..ef8da8a6cf1 100644 --- a/web/src/assets/locales/zh-Hans.json +++ b/web/src/assets/locales/zh-Hans.json @@ -76,42 +76,18 @@ "branches": "分支", "build": { "actions": { - "cancel": "取消", - "cancel_success": "流水线已被取消", - "canceled": "该步骤已被取消。", "log_auto_scroll": "自动滚动", - "log_auto_scroll_off": "关闭自动滚动", - "log_download": "下载", - "restart": "重启", "restart_success": "流水线已重启" }, - "config": "配置", - "created": "已创建", "event": { - "cron": "计划任务", - "deploy": "部署", - "push": "推送", - "tag": "标签" + "deploy": "部署" }, - "execution_error": "执行错误", "exit_code": "退出代码 {exitCode}", - "files": "修改的文件({files})", - "loading": "载入中…", - "log_download_error": "下载日志文件中发生了错误", "no_files": "没有文件修改。", - "no_pipeline_steps": "没有流水线步骤可用!", - "no_pipelines": "没有流水线曾启动。", - "pipeline": "流水线 #{buildId}", "pipelines_for": "{branch} 分支的流水线", "protected": { - "approve": "同意", - "approve_success": "流水线已被许可", - "awaits": "该流水线正在等待维护者许可!", - "decline": "拒绝", - "decline_success": "流水线已被拒绝", - "declined": "该流水线已被拒绝!" + "decline": "拒绝" }, - "step_not_started": "该步骤还不曾启动过。", "tasks": "任务" }, "enable": { @@ -179,8 +155,7 @@ } }, "badge": { - "badge": "徽标", - "url_branch": "特定分支的 URL" + "badge": "徽标" }, "crons": { "add": "创建计划",