mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-03-16 17:54:07 +01:00
Native forgejo support (#3684)
Co-authored-by: Robert Kaussow <xoxys@rknet.org> Co-authored-by: John Olheiser <john.olheiser@gmail.com>
This commit is contained in:
210
server/forge/forgejo/fixtures/handler.go
Normal file
210
server/forge/forgejo/fixtures/handler.go
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fixtures
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Handler returns an http.Handler that is capable of handling a variety of mock
|
||||
// Forgejo requests and returning mock responses.
|
||||
func Handler() http.Handler {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
e := gin.New()
|
||||
e.GET("/api/v1/repos/:owner/:name", getRepo)
|
||||
e.GET("/api/v1/repositories/:id", getRepoByID)
|
||||
e.GET("/api/v1/repos/:owner/:name/raw/:file", getRepoFile)
|
||||
e.POST("/api/v1/repos/:owner/:name/hooks", createRepoHook)
|
||||
e.GET("/api/v1/repos/:owner/:name/hooks", listRepoHooks)
|
||||
e.DELETE("/api/v1/repos/:owner/:name/hooks/:id", deleteRepoHook)
|
||||
e.POST("/api/v1/repos/:owner/:name/statuses/:commit", createRepoCommitStatus)
|
||||
e.GET("/api/v1/repos/:owner/:name/pulls/:index/files", getPRFiles)
|
||||
e.GET("/api/v1/user/repos", getUserRepos)
|
||||
e.GET("/api/v1/version", getVersion)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func listRepoHooks(c *gin.Context) {
|
||||
page := c.Query("page")
|
||||
if page != "" && page != "1" {
|
||||
c.String(http.StatusOK, "[]")
|
||||
} else {
|
||||
c.String(http.StatusOK, listRepoHookPayloads)
|
||||
}
|
||||
}
|
||||
|
||||
func getRepo(c *gin.Context) {
|
||||
switch c.Param("name") {
|
||||
case "repo_not_found":
|
||||
c.String(http.StatusNotFound, "")
|
||||
default:
|
||||
c.String(http.StatusOK, repoPayload)
|
||||
}
|
||||
}
|
||||
|
||||
func getRepoByID(c *gin.Context) {
|
||||
switch c.Param("id") {
|
||||
case "repo_not_found":
|
||||
c.String(http.StatusNotFound, "")
|
||||
default:
|
||||
c.String(http.StatusOK, repoPayload)
|
||||
}
|
||||
}
|
||||
|
||||
func createRepoCommitStatus(c *gin.Context) {
|
||||
if c.Param("commit") == "v1.0.0" || c.Param("commit") == "9ecad50" {
|
||||
c.String(http.StatusOK, repoPayload)
|
||||
}
|
||||
c.String(http.StatusNotFound, "")
|
||||
}
|
||||
|
||||
func getRepoFile(c *gin.Context) {
|
||||
file := c.Param("file")
|
||||
ref := c.Query("ref")
|
||||
|
||||
if file == "file_not_found" {
|
||||
c.String(http.StatusNotFound, "")
|
||||
}
|
||||
if ref == "v1.0.0" || ref == "9ecad50" {
|
||||
c.String(http.StatusOK, repoFilePayload)
|
||||
}
|
||||
c.String(http.StatusNotFound, "")
|
||||
}
|
||||
|
||||
func createRepoHook(c *gin.Context) {
|
||||
in := struct {
|
||||
Type string `json:"type"`
|
||||
Conf struct {
|
||||
Type string `json:"content_type"`
|
||||
URL string `json:"url"`
|
||||
} `json:"config"`
|
||||
}{}
|
||||
_ = c.BindJSON(&in)
|
||||
if in.Type != "forgejo" ||
|
||||
in.Conf.Type != "json" ||
|
||||
in.Conf.URL != "http://localhost" {
|
||||
c.String(http.StatusInternalServerError, "")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "{}")
|
||||
}
|
||||
|
||||
func deleteRepoHook(c *gin.Context) {
|
||||
c.String(http.StatusOK, "{}")
|
||||
}
|
||||
|
||||
func getUserRepos(c *gin.Context) {
|
||||
switch c.Request.Header.Get("Authorization") {
|
||||
case "token repos_not_found":
|
||||
c.String(http.StatusNotFound, "")
|
||||
default:
|
||||
page := c.Query("page")
|
||||
if page != "" && page != "1" {
|
||||
c.String(http.StatusOK, "[]")
|
||||
} else {
|
||||
c.String(http.StatusOK, userRepoPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getVersion(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, map[string]any{"version": "1.18.0"})
|
||||
}
|
||||
|
||||
func getPRFiles(c *gin.Context) {
|
||||
page := c.Query("page")
|
||||
if page == "1" {
|
||||
c.String(http.StatusOK, prFilesPayload)
|
||||
} else {
|
||||
c.String(http.StatusOK, "[]")
|
||||
}
|
||||
}
|
||||
|
||||
const listRepoHookPayloads = `
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"type": "forgejo",
|
||||
"config": {
|
||||
"content_type": "json",
|
||||
"url": "http:\/\/localhost\/hook?access_token=1234567890"
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
const repoPayload = `
|
||||
{
|
||||
"id": 5,
|
||||
"owner": {
|
||||
"login": "test_name",
|
||||
"email": "octocat@github.com",
|
||||
"avatar_url": "https:\/\/secure.gravatar.com\/avatar\/8c58a0be77ee441bb8f8595b7f1b4e87"
|
||||
},
|
||||
"full_name": "test_name\/repo_name",
|
||||
"private": true,
|
||||
"html_url": "http:\/\/localhost\/test_name\/repo_name",
|
||||
"clone_url": "http:\/\/localhost\/test_name\/repo_name.git",
|
||||
"permissions": {
|
||||
"admin": true,
|
||||
"push": true,
|
||||
"pull": true
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const repoFilePayload = `{ platform: linux/amd64 }`
|
||||
|
||||
const userRepoPayload = `
|
||||
[
|
||||
{
|
||||
"id": 5,
|
||||
"owner": {
|
||||
"login": "test_name",
|
||||
"email": "octocat@github.com",
|
||||
"avatar_url": "https:\/\/secure.gravatar.com\/avatar\/8c58a0be77ee441bb8f8595b7f1b4e87"
|
||||
},
|
||||
"full_name": "test_name\/repo_name",
|
||||
"private": true,
|
||||
"html_url": "http:\/\/localhost\/test_name\/repo_name",
|
||||
"clone_url": "http:\/\/localhost\/test_name\/repo_name.git",
|
||||
"permissions": {
|
||||
"admin": true,
|
||||
"push": true,
|
||||
"pull": true
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
const prFilesPayload = `
|
||||
[
|
||||
{
|
||||
"filename": "README.md",
|
||||
"status": "changed",
|
||||
"additions": 2,
|
||||
"deletions": 0,
|
||||
"changes": 2,
|
||||
"html_url": "http://localhost/username/repo/src/commit/e79e4b0e8d9dd6f72b70e776c3317db7c19ca0fd/README.md",
|
||||
"contents_url": "http://localhost:3000/api/v1/repos/username/repo/contents/README.md?ref=e79e4b0e8d9dd6f72b70e776c3317db7c19ca0fd",
|
||||
"raw_url": "http://localhost/username/repo/raw/commit/e79e4b0e8d9dd6f72b70e776c3317db7c19ca0fd/README.md"
|
||||
}
|
||||
]
|
||||
`
|
||||
1483
server/forge/forgejo/fixtures/hooks.go
Normal file
1483
server/forge/forgejo/fixtures/hooks.go
Normal file
File diff suppressed because it is too large
Load Diff
706
server/forge/forgejo/forgejo.go
Normal file
706
server/forge/forgejo/forgejo.go
Normal file
@@ -0,0 +1,706 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/common"
|
||||
forge_types "go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||
shared_utils "go.woodpecker-ci.org/woodpecker/v2/shared/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
authorizeTokenURL = "%s/login/oauth/authorize"
|
||||
accessTokenURL = "%s/login/oauth/access_token"
|
||||
defaultPageSize = 50
|
||||
forgejoDevVersion = "v7.0.2"
|
||||
)
|
||||
|
||||
type Forgejo struct {
|
||||
url string
|
||||
oauth2URL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
SkipVerify bool
|
||||
pageSize int
|
||||
}
|
||||
|
||||
// Opts defines configuration options.
|
||||
type Opts struct {
|
||||
URL string // Forgejo server url.
|
||||
OAuth2URL string // User-facing Forgejo server url for OAuth2.
|
||||
Client string // OAuth2 Client ID
|
||||
Secret string // OAuth2 Client Secret
|
||||
SkipVerify bool // Skip ssl verification.
|
||||
}
|
||||
|
||||
// New returns a Forge implementation that integrates with Forgejo,
|
||||
// an open source Git service written in Go. See https://forgejo.org/
|
||||
func New(opts Opts) (forge.Forge, error) {
|
||||
if opts.OAuth2URL == "" {
|
||||
opts.OAuth2URL = opts.URL
|
||||
}
|
||||
|
||||
return &Forgejo{
|
||||
url: opts.URL,
|
||||
oauth2URL: opts.OAuth2URL,
|
||||
ClientID: opts.Client,
|
||||
ClientSecret: opts.Secret,
|
||||
SkipVerify: opts.SkipVerify,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Name returns the string name of this driver.
|
||||
func (c *Forgejo) Name() string {
|
||||
return "forgejo"
|
||||
}
|
||||
|
||||
// URL returns the root url of a configured forge.
|
||||
func (c *Forgejo) URL() string {
|
||||
return c.url
|
||||
}
|
||||
|
||||
func (c *Forgejo) oauth2Config(ctx context.Context) (*oauth2.Config, context.Context) {
|
||||
return &oauth2.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf(authorizeTokenURL, c.oauth2URL),
|
||||
TokenURL: fmt.Sprintf(accessTokenURL, c.oauth2URL),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
||||
},
|
||||
|
||||
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipVerify},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}})
|
||||
}
|
||||
|
||||
// Login authenticates an account with Forgejo using basic authentication. The
|
||||
// Forgejo account details are returned when the user is successfully authenticated.
|
||||
func (c *Forgejo) Login(ctx context.Context, req *forge_types.OAuthRequest) (*model.User, string, error) {
|
||||
config, oauth2Ctx := c.oauth2Config(ctx)
|
||||
redirectURL := config.AuthCodeURL("woodpecker")
|
||||
|
||||
// check the OAuth errors
|
||||
if req.Error != "" {
|
||||
return nil, redirectURL, &forge_types.AuthError{
|
||||
Err: req.Error,
|
||||
Description: req.ErrorDescription,
|
||||
URI: req.ErrorURI,
|
||||
}
|
||||
}
|
||||
|
||||
// check the OAuth code
|
||||
if len(req.Code) == 0 {
|
||||
return nil, redirectURL, nil
|
||||
}
|
||||
|
||||
token, err := config.Exchange(oauth2Ctx, req.Code)
|
||||
if err != nil {
|
||||
return nil, redirectURL, err
|
||||
}
|
||||
|
||||
client, err := c.newClientToken(ctx, token.AccessToken)
|
||||
if err != nil {
|
||||
return nil, redirectURL, err
|
||||
}
|
||||
account, _, err := client.GetMyUserInfo()
|
||||
if err != nil {
|
||||
return nil, redirectURL, err
|
||||
}
|
||||
|
||||
return &model.User{
|
||||
Token: token.AccessToken,
|
||||
Secret: token.RefreshToken,
|
||||
Expiry: token.Expiry.UTC().Unix(),
|
||||
Login: account.UserName,
|
||||
Email: account.Email,
|
||||
ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(account.ID)),
|
||||
Avatar: expandAvatar(c.url, account.AvatarURL),
|
||||
}, redirectURL, nil
|
||||
}
|
||||
|
||||
// Auth uses the Forgejo oauth2 access token and refresh token to authenticate
|
||||
// a session and return the Forgejo account login.
|
||||
func (c *Forgejo) Auth(ctx context.Context, token, _ string) (string, error) {
|
||||
client, err := c.newClientToken(ctx, token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
user, _, err := client.GetMyUserInfo()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return user.UserName, nil
|
||||
}
|
||||
|
||||
// Refresh refreshes the Forgejo oauth2 access token. If the token is
|
||||
// refreshed, the user is updated and a true value is returned.
|
||||
func (c *Forgejo) Refresh(ctx context.Context, user *model.User) (bool, error) {
|
||||
config, oauth2Ctx := c.oauth2Config(ctx)
|
||||
config.RedirectURL = ""
|
||||
|
||||
source := config.TokenSource(oauth2Ctx, &oauth2.Token{
|
||||
AccessToken: user.Token,
|
||||
RefreshToken: user.Secret,
|
||||
Expiry: time.Unix(user.Expiry, 0),
|
||||
})
|
||||
|
||||
token, err := source.Token()
|
||||
if err != nil || len(token.AccessToken) == 0 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
user.Token = token.AccessToken
|
||||
user.Secret = token.RefreshToken
|
||||
user.Expiry = token.Expiry.UTC().Unix()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Teams is supported by the Forgejo driver.
|
||||
func (c *Forgejo) Teams(ctx context.Context, u *model.User) ([]*model.Team, error) {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shared_utils.Paginate(func(page int) ([]*model.Team, error) {
|
||||
orgs, _, err := client.ListMyOrgs(
|
||||
forgejo.ListOrgsOptions{
|
||||
ListOptions: forgejo.ListOptions{
|
||||
Page: page,
|
||||
PageSize: c.perPage(ctx),
|
||||
},
|
||||
},
|
||||
)
|
||||
teams := make([]*model.Team, 0, len(orgs))
|
||||
for _, org := range orgs {
|
||||
teams = append(teams, toTeam(org, c.url))
|
||||
}
|
||||
return teams, err
|
||||
})
|
||||
}
|
||||
|
||||
// TeamPerm is not supported by the Forgejo driver.
|
||||
func (c *Forgejo) TeamPerm(_ *model.User, _ string) (*model.Perm, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Repo returns the Forgejo repository.
|
||||
func (c *Forgejo) Repo(ctx context.Context, u *model.User, remoteID model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if remoteID.IsValid() {
|
||||
intID, err := strconv.ParseInt(string(remoteID), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo, _, err := client.GetRepoByID(intID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toRepo(repo), nil
|
||||
}
|
||||
|
||||
repo, _, err := client.GetRepo(owner, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toRepo(repo), nil
|
||||
}
|
||||
|
||||
// Repos returns a list of all repositories for the Forgejo account, including
|
||||
// organization repositories.
|
||||
func (c *Forgejo) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repos, err := shared_utils.Paginate(func(page int) ([]*forgejo.Repository, error) {
|
||||
repos, _, err := client.ListMyRepos(
|
||||
forgejo.ListReposOptions{
|
||||
ListOptions: forgejo.ListOptions{
|
||||
Page: page,
|
||||
PageSize: c.perPage(ctx),
|
||||
},
|
||||
},
|
||||
)
|
||||
return repos, err
|
||||
})
|
||||
|
||||
result := make([]*model.Repo, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
if repo.Archived {
|
||||
continue
|
||||
}
|
||||
result = append(result, toRepo(repo))
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// File fetches the file from the Forgejo repository and returns its contents.
|
||||
func (c *Forgejo) File(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, f string) ([]byte, error) {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, resp, err := client.GetFile(r.Owner, r.Name, b.Commit, f)
|
||||
if err != nil && resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||
return nil, errors.Join(err, &forge_types.ErrConfigNotFound{Configs: []string{f}})
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (c *Forgejo) Dir(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, f string) ([]*forge_types.FileMeta, error) {
|
||||
var configs []*forge_types.FileMeta
|
||||
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// List files in repository. Path from root
|
||||
tree, _, err := client.GetTrees(r.Owner, r.Name, b.Commit, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f = path.Clean(f) // We clean path and remove trailing slash
|
||||
f += "/" + "*" // construct pattern for match i.e. file in subdir
|
||||
for _, e := range tree.Entries {
|
||||
// Filter path matching pattern and type file (blob)
|
||||
if m, _ := filepath.Match(f, e.Path); m && e.Type == "blob" {
|
||||
data, err := c.File(ctx, u, r, b, e.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, &forge_types.ErrConfigNotFound{}) {
|
||||
return nil, fmt.Errorf("git tree reported existence of file but we got: %s", err.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("multi-pipeline cannot get %s: %w", e.Path, err)
|
||||
}
|
||||
|
||||
configs = append(configs, &forge_types.FileMeta{
|
||||
Name: e.Path,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// Status is supported by the Forgejo driver.
|
||||
func (c *Forgejo) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, workflow *model.Workflow) error {
|
||||
client, err := c.newClientToken(ctx, user.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, err = client.CreateStatus(
|
||||
repo.Owner,
|
||||
repo.Name,
|
||||
pipeline.Commit,
|
||||
forgejo.CreateStatusOption{
|
||||
State: getStatus(workflow.State),
|
||||
TargetURL: common.GetPipelineStatusURL(repo, pipeline, workflow),
|
||||
Description: common.GetPipelineStatusDescription(workflow.State),
|
||||
Context: common.GetPipelineStatusContext(repo, pipeline, workflow),
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Netrc returns a netrc file capable of authenticating Forgejo requests and
|
||||
// cloning Forgejo repositories. The netrc will use the global machine account
|
||||
// when configured.
|
||||
func (c *Forgejo) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
|
||||
login := ""
|
||||
token := ""
|
||||
|
||||
if u != nil {
|
||||
login = u.Login
|
||||
token = u.Token
|
||||
}
|
||||
|
||||
host, err := common.ExtractHostFromCloneURL(r.Clone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Netrc{
|
||||
Login: login,
|
||||
Password: token,
|
||||
Machine: host,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Activate activates the repository by registering post-commit hooks with
|
||||
// the Forgejo repository.
|
||||
func (c *Forgejo) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
|
||||
config := map[string]string{
|
||||
"url": link,
|
||||
"secret": r.Hash,
|
||||
"content_type": "json",
|
||||
}
|
||||
hook := forgejo.CreateHookOption{
|
||||
Type: forgejo.HookTypeForgejo,
|
||||
Config: config,
|
||||
Events: []string{"push", "create", "pull_request"},
|
||||
Active: true,
|
||||
}
|
||||
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, response, err := client.CreateRepoHook(r.Owner, r.Name, hook)
|
||||
if err != nil {
|
||||
if response != nil {
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return fmt.Errorf("could not find repository")
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
return fmt.Errorf("could not find repository, repository was probably renamed")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivate deactivates the repository be removing repository push hooks from
|
||||
// the Forgejo repository.
|
||||
func (c *Forgejo) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hooks, err := shared_utils.Paginate(func(page int) ([]*forgejo.Hook, error) {
|
||||
hooks, _, err := client.ListRepoHooks(r.Owner, r.Name, forgejo.ListHooksOptions{
|
||||
ListOptions: forgejo.ListOptions{
|
||||
Page: page,
|
||||
PageSize: c.perPage(ctx),
|
||||
},
|
||||
})
|
||||
return hooks, err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hook := matchingHooks(hooks, link)
|
||||
if hook != nil {
|
||||
_, err := client.DeleteRepoHook(r.Owner, r.Name, hook.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Branches returns the names of all branches for the named repository.
|
||||
func (c *Forgejo) Branches(ctx context.Context, u *model.User, r *model.Repo, p *model.ListOptions) ([]string, error) {
|
||||
token := common.UserToken(ctx, r, u)
|
||||
client, err := c.newClientToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branches, _, err := client.ListRepoBranches(r.Owner, r.Name,
|
||||
forgejo.ListRepoBranchesOptions{ListOptions: forgejo.ListOptions{Page: p.Page, PageSize: p.PerPage}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]string, len(branches))
|
||||
for i := range branches {
|
||||
result[i] = branches[i].Name
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// BranchHead returns the sha of the head (latest commit) of the specified branch.
|
||||
func (c *Forgejo) BranchHead(ctx context.Context, u *model.User, r *model.Repo, branch string) (*model.Commit, error) {
|
||||
token := common.UserToken(ctx, r, u)
|
||||
client, err := c.newClientToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, _, err := client.GetRepoBranch(r.Owner, r.Name, branch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Commit{
|
||||
SHA: b.Commit.ID,
|
||||
ForgeURL: b.Commit.URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Forgejo) PullRequests(ctx context.Context, u *model.User, r *model.Repo, p *model.ListOptions) ([]*model.PullRequest, error) {
|
||||
token := common.UserToken(ctx, r, u)
|
||||
client, err := c.newClientToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pullRequests, _, err := client.ListRepoPullRequests(r.Owner, r.Name, forgejo.ListPullRequestsOptions{
|
||||
ListOptions: forgejo.ListOptions{Page: p.Page, PageSize: p.PerPage},
|
||||
State: forgejo.StateOpen,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*model.PullRequest, len(pullRequests))
|
||||
for i := range pullRequests {
|
||||
result[i] = &model.PullRequest{
|
||||
Index: model.ForgeRemoteID(strconv.Itoa(int(pullRequests[i].Index))),
|
||||
Title: pullRequests[i].Title,
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Hook parses the incoming Forgejo hook and returns the Repository and Pipeline
|
||||
// details. If the hook is unsupported nil values are returned.
|
||||
func (c *Forgejo) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) {
|
||||
repo, pipeline, err := parseHook(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if pipeline != nil && pipeline.Event == model.EventRelease && pipeline.Commit == "" {
|
||||
tagName := strings.Split(pipeline.Ref, "/")[2]
|
||||
sha, err := c.getTagCommitSHA(ctx, repo, tagName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pipeline.Commit = sha
|
||||
}
|
||||
|
||||
if pipeline != nil && (pipeline.Event == model.EventPull || pipeline.Event == model.EventPullClosed) && len(pipeline.ChangedFiles) == 0 {
|
||||
index, err := strconv.ParseInt(strings.Split(pipeline.Ref, "/")[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pipeline.ChangedFiles, err = c.getChangedFilesForPR(ctx, repo, index)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("could not get changed files for PR %s#%d", repo.FullName, index)
|
||||
}
|
||||
}
|
||||
|
||||
return repo, pipeline, nil
|
||||
}
|
||||
|
||||
// OrgMembership returns if user is member of organization and if user
|
||||
// is admin/owner in this organization.
|
||||
func (c *Forgejo) OrgMembership(ctx context.Context, u *model.User, owner string) (*model.OrgPerm, error) {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
member, _, err := client.CheckOrgMembership(owner, u.Login)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !member {
|
||||
return &model.OrgPerm{}, nil
|
||||
}
|
||||
|
||||
perm, _, err := client.GetOrgPermissions(owner, u.Login)
|
||||
if err != nil {
|
||||
return &model.OrgPerm{Member: member}, err
|
||||
}
|
||||
|
||||
return &model.OrgPerm{Member: member, Admin: perm.IsAdmin || perm.IsOwner}, nil
|
||||
}
|
||||
|
||||
func (c *Forgejo) Org(ctx context.Context, u *model.User, owner string) (*model.Org, error) {
|
||||
client, err := c.newClientToken(ctx, u.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
org, _, orgErr := client.GetOrg(owner)
|
||||
if orgErr == nil && org != nil {
|
||||
return &model.Org{
|
||||
Name: org.UserName,
|
||||
Private: forgejo.VisibleType(org.Visibility) != forgejo.VisibleTypePublic,
|
||||
}, nil
|
||||
}
|
||||
|
||||
user, _, err := client.GetUserInfo(owner)
|
||||
if err != nil {
|
||||
if orgErr != nil {
|
||||
err = errors.Join(orgErr, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &model.Org{
|
||||
Name: user.UserName,
|
||||
IsUser: true,
|
||||
Private: user.Visibility != forgejo.VisibleTypePublic,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newClientToken returns a Forgejo client with token.
|
||||
func (c *Forgejo) newClientToken(ctx context.Context, token string) (*forgejo.Client, error) {
|
||||
httpClient := &http.Client{}
|
||||
if c.SkipVerify {
|
||||
httpClient.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
}
|
||||
client, err := forgejo.NewClient(c.url, forgejo.SetToken(token), forgejo.SetHTTPClient(httpClient), forgejo.SetContext(ctx))
|
||||
if err != nil &&
|
||||
(errors.Is(err, &forgejo.ErrUnknownVersion{}) || strings.Contains(err.Error(), "Malformed version")) {
|
||||
// we guess it's a dev forgejo version
|
||||
log.Error().Err(err).Msgf("could not detect forgejo version, assume dev version %s", forgejoDevVersion)
|
||||
client, err = forgejo.NewClient(c.url, forgejo.SetForgejoVersion(forgejoDevVersion), forgejo.SetToken(token), forgejo.SetHTTPClient(httpClient), forgejo.SetContext(ctx))
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
// getStatus is a helper function that converts a Woodpecker
|
||||
// status to a Forgejo status.
|
||||
func getStatus(status model.StatusValue) forgejo.StatusState {
|
||||
switch status {
|
||||
case model.StatusPending, model.StatusBlocked:
|
||||
return forgejo.StatusPending
|
||||
case model.StatusRunning:
|
||||
return forgejo.StatusPending
|
||||
case model.StatusSuccess:
|
||||
return forgejo.StatusSuccess
|
||||
case model.StatusFailure:
|
||||
return forgejo.StatusFailure
|
||||
case model.StatusKilled:
|
||||
return forgejo.StatusFailure
|
||||
case model.StatusDeclined:
|
||||
return forgejo.StatusWarning
|
||||
case model.StatusError:
|
||||
return forgejo.StatusError
|
||||
default:
|
||||
return forgejo.StatusFailure
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Forgejo) getChangedFilesForPR(ctx context.Context, repo *model.Repo, index int64) ([]string, error) {
|
||||
_store, ok := store.TryFromContext(ctx)
|
||||
if !ok {
|
||||
log.Error().Msg("could not get store from context")
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
repo, err := _store.GetRepoNameFallback(repo.ForgeRemoteID, repo.FullName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := _store.GetUser(repo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := c.newClientToken(ctx, user.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shared_utils.Paginate(func(page int) ([]string, error) {
|
||||
forgejoFiles, _, err := client.ListPullRequestFiles(repo.Owner, repo.Name, index,
|
||||
forgejo.ListPullRequestFilesOptions{ListOptions: forgejo.ListOptions{Page: page}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, file := range forgejoFiles {
|
||||
files = append(files, file.Filename)
|
||||
}
|
||||
return files, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Forgejo) getTagCommitSHA(ctx context.Context, repo *model.Repo, tagName string) (string, error) {
|
||||
_store, ok := store.TryFromContext(ctx)
|
||||
if !ok {
|
||||
log.Error().Msg("could not get store from context")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
repo, err := _store.GetRepoNameFallback(repo.ForgeRemoteID, repo.FullName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
user, err := _store.GetUser(repo.UserID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client, err := c.newClientToken(ctx, user.Token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tag, _, err := client.GetTag(repo.Owner, repo.Name, tagName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tag.Commit.SHA, nil
|
||||
}
|
||||
|
||||
func (c *Forgejo) perPage(ctx context.Context) int {
|
||||
if c.pageSize == 0 {
|
||||
client, err := c.newClientToken(ctx, "")
|
||||
if err != nil {
|
||||
return defaultPageSize
|
||||
}
|
||||
|
||||
api, _, err := client.GetGlobalAPISettings()
|
||||
if err != nil {
|
||||
return defaultPageSize
|
||||
}
|
||||
c.pageSize = api.MaxResponseItems
|
||||
}
|
||||
return c.pageSize
|
||||
}
|
||||
199
server/forge/forgejo/forgejo_test.go
Normal file
199
server/forge/forgejo/forgejo_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/franela/goblin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/forgejo/fixtures"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||
mocks_store "go.woodpecker-ci.org/woodpecker/v2/server/store/mocks"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
|
||||
)
|
||||
|
||||
func Test_forgejo(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
s := httptest.NewServer(fixtures.Handler())
|
||||
c, _ := New(Opts{
|
||||
URL: s.URL,
|
||||
SkipVerify: true,
|
||||
})
|
||||
|
||||
mockStore := mocks_store.NewStore(t)
|
||||
ctx := store.InjectToContext(context.Background(), mockStore)
|
||||
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Forgejo", func() {
|
||||
g.After(func() {
|
||||
s.Close()
|
||||
})
|
||||
|
||||
g.Describe("Creating a forge", func() {
|
||||
g.It("Should return client with specified options", func() {
|
||||
forge, _ := New(Opts{
|
||||
URL: "http://localhost:8080",
|
||||
SkipVerify: true,
|
||||
})
|
||||
|
||||
f, _ := forge.(*Forgejo)
|
||||
g.Assert(f.url).Equal("http://localhost:8080")
|
||||
g.Assert(f.SkipVerify).Equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("Generating a netrc file", func() {
|
||||
g.It("Should return a netrc with the user token", func() {
|
||||
forge, _ := New(Opts{})
|
||||
netrc, _ := forge.Netrc(fakeUser, fakeRepo)
|
||||
g.Assert(netrc.Machine).Equal("forgejo.org")
|
||||
g.Assert(netrc.Login).Equal(fakeUser.Login)
|
||||
g.Assert(netrc.Password).Equal(fakeUser.Token)
|
||||
})
|
||||
g.It("Should return a netrc with the machine account", func() {
|
||||
forge, _ := New(Opts{})
|
||||
netrc, _ := forge.Netrc(nil, fakeRepo)
|
||||
g.Assert(netrc.Machine).Equal("forgejo.org")
|
||||
g.Assert(netrc.Login).Equal("")
|
||||
g.Assert(netrc.Password).Equal("")
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("Requesting a repository", func() {
|
||||
g.It("Should return the repository details", func() {
|
||||
repo, err := c.Repo(ctx, fakeUser, fakeRepo.ForgeRemoteID, fakeRepo.Owner, fakeRepo.Name)
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(repo.Owner).Equal(fakeRepo.Owner)
|
||||
g.Assert(repo.Name).Equal(fakeRepo.Name)
|
||||
g.Assert(repo.FullName).Equal(fakeRepo.Owner + "/" + fakeRepo.Name)
|
||||
g.Assert(repo.IsSCMPrivate).IsTrue()
|
||||
g.Assert(repo.Clone).Equal("http://localhost/test_name/repo_name.git")
|
||||
g.Assert(repo.ForgeURL).Equal("http://localhost/test_name/repo_name")
|
||||
})
|
||||
g.It("Should handle a not found error", func() {
|
||||
_, err := c.Repo(ctx, fakeUser, "0", fakeRepoNotFound.Owner, fakeRepoNotFound.Name)
|
||||
g.Assert(err).IsNotNil()
|
||||
})
|
||||
})
|
||||
|
||||
g.Describe("Requesting a repository list", func() {
|
||||
g.It("Should return the repository list", func() {
|
||||
repos, err := c.Repos(ctx, fakeUser)
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(repos[0].ForgeRemoteID).Equal(fakeRepo.ForgeRemoteID)
|
||||
g.Assert(repos[0].Owner).Equal(fakeRepo.Owner)
|
||||
g.Assert(repos[0].Name).Equal(fakeRepo.Name)
|
||||
g.Assert(repos[0].FullName).Equal(fakeRepo.Owner + "/" + fakeRepo.Name)
|
||||
})
|
||||
g.It("Should handle a not found error", func() {
|
||||
_, err := c.Repos(ctx, fakeUserNoRepos)
|
||||
g.Assert(err).IsNotNil()
|
||||
})
|
||||
})
|
||||
|
||||
g.It("Should register repository hooks", func() {
|
||||
err := c.Activate(ctx, fakeUser, fakeRepo, "http://localhost")
|
||||
g.Assert(err).IsNil()
|
||||
})
|
||||
|
||||
g.It("Should remove repository hooks", func() {
|
||||
err := c.Deactivate(ctx, fakeUser, fakeRepo, "http://localhost")
|
||||
g.Assert(err).IsNil()
|
||||
})
|
||||
|
||||
g.It("Should return a repository file", func() {
|
||||
raw, err := c.File(ctx, fakeUser, fakeRepo, fakePipeline, ".woodpecker.yml")
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(string(raw)).Equal("{ platform: linux/amd64 }")
|
||||
})
|
||||
|
||||
g.It("Should return nil from send pipeline status", func() {
|
||||
err := c.Status(ctx, fakeUser, fakeRepo, fakePipeline, fakeWorkflow)
|
||||
g.Assert(err).IsNil()
|
||||
})
|
||||
|
||||
g.Describe("Given an authentication request", func() {
|
||||
g.It("Should redirect to login form")
|
||||
g.It("Should create an access token")
|
||||
g.It("Should handle an access token error")
|
||||
g.It("Should return the authenticated user")
|
||||
})
|
||||
|
||||
g.Describe("Given a repository hook", func() {
|
||||
g.It("Should skip non-push events")
|
||||
g.It("Should return push details")
|
||||
g.It("Should handle a parsing error")
|
||||
})
|
||||
|
||||
g.It("Given a PR hook", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPullRequest)
|
||||
req, _ := http.NewRequest("POST", "/hook", buf)
|
||||
req.Header = http.Header{}
|
||||
req.Header.Set(hookEvent, hookPullRequest)
|
||||
mockStore.On("GetRepoNameFallback", mock.Anything, mock.Anything).Return(fakeRepo, nil)
|
||||
mockStore.On("GetUser", mock.Anything).Return(fakeUser, nil)
|
||||
r, b, err := c.Hook(ctx, req)
|
||||
g.Assert(r).IsNotNil()
|
||||
g.Assert(b).IsNotNil()
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(b.Event).Equal(model.EventPull)
|
||||
g.Assert(utils.EqualSliceValues(b.ChangedFiles, []string{"README.md"})).IsTrue()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
fakeUser = &model.User{
|
||||
Login: "someuser",
|
||||
Token: "cfcd2084",
|
||||
}
|
||||
|
||||
fakeUserNoRepos = &model.User{
|
||||
Login: "someuser",
|
||||
Token: "repos_not_found",
|
||||
}
|
||||
|
||||
fakeRepo = &model.Repo{
|
||||
Clone: "http://forgejo.org/test_name/repo_name.git",
|
||||
ForgeRemoteID: "5",
|
||||
Owner: "test_name",
|
||||
Name: "repo_name",
|
||||
FullName: "test_name/repo_name",
|
||||
}
|
||||
|
||||
fakeRepoNotFound = &model.Repo{
|
||||
Owner: "test_name",
|
||||
Name: "repo_not_found",
|
||||
FullName: "test_name/repo_not_found",
|
||||
}
|
||||
|
||||
fakePipeline = &model.Pipeline{
|
||||
Commit: "9ecad50",
|
||||
}
|
||||
|
||||
fakeWorkflow = &model.Workflow{
|
||||
Name: "test",
|
||||
State: model.StatusSuccess,
|
||||
}
|
||||
)
|
||||
277
server/forge/forgejo/helper.go
Normal file
277
server/forge/forgejo/helper.go
Normal file
@@ -0,0 +1,277 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
|
||||
)
|
||||
|
||||
// toRepo converts a Forgejo repository to a Woodpecker repository.
|
||||
func toRepo(from *forgejo.Repository) *model.Repo {
|
||||
name := strings.Split(from.FullName, "/")[1]
|
||||
avatar := expandAvatar(
|
||||
from.HTMLURL,
|
||||
from.Owner.AvatarURL,
|
||||
)
|
||||
return &model.Repo{
|
||||
ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)),
|
||||
SCMKind: model.RepoGit,
|
||||
Name: name,
|
||||
Owner: from.Owner.UserName,
|
||||
FullName: from.FullName,
|
||||
Avatar: avatar,
|
||||
ForgeURL: from.HTMLURL,
|
||||
IsSCMPrivate: from.Private || from.Owner.Visibility != forgejo.VisibleTypePublic,
|
||||
Clone: from.CloneURL,
|
||||
CloneSSH: from.SSHURL,
|
||||
Branch: from.DefaultBranch,
|
||||
Perm: toPerm(from.Permissions),
|
||||
PREnabled: from.HasPullRequests,
|
||||
}
|
||||
}
|
||||
|
||||
// toPerm converts a Forgejo permission to a Woodpecker permission.
|
||||
func toPerm(from *forgejo.Permission) *model.Perm {
|
||||
return &model.Perm{
|
||||
Pull: from.Pull,
|
||||
Push: from.Push,
|
||||
Admin: from.Admin,
|
||||
}
|
||||
}
|
||||
|
||||
// toTeam converts a Forgejo team to a Woodpecker team.
|
||||
func toTeam(from *forgejo.Organization, link string) *model.Team {
|
||||
return &model.Team{
|
||||
Login: from.UserName,
|
||||
Avatar: expandAvatar(link, from.AvatarURL),
|
||||
}
|
||||
}
|
||||
|
||||
// pipelineFromPush extracts the Pipeline data from a Forgejo push hook.
|
||||
func pipelineFromPush(hook *pushHook) *model.Pipeline {
|
||||
avatar := expandAvatar(
|
||||
hook.Repo.HTMLURL,
|
||||
fixMalformedAvatar(hook.Sender.AvatarURL),
|
||||
)
|
||||
|
||||
var message string
|
||||
link := hook.Compare
|
||||
if len(hook.Commits) > 0 {
|
||||
message = hook.Commits[0].Message
|
||||
if len(hook.Commits) == 1 {
|
||||
link = hook.Commits[0].URL
|
||||
}
|
||||
} else {
|
||||
message = hook.HeadCommit.Message
|
||||
link = hook.HeadCommit.URL
|
||||
}
|
||||
|
||||
return &model.Pipeline{
|
||||
Event: model.EventPush,
|
||||
Commit: hook.After,
|
||||
Ref: hook.Ref,
|
||||
ForgeURL: link,
|
||||
Branch: strings.TrimPrefix(hook.Ref, "refs/heads/"),
|
||||
Message: message,
|
||||
Avatar: avatar,
|
||||
Author: hook.Sender.UserName,
|
||||
Email: hook.Sender.Email,
|
||||
Timestamp: time.Now().UTC().Unix(),
|
||||
Sender: hook.Sender.UserName,
|
||||
ChangedFiles: getChangedFilesFromPushHook(hook),
|
||||
}
|
||||
}
|
||||
|
||||
func getChangedFilesFromPushHook(hook *pushHook) []string {
|
||||
// assume a capacity of 4 changed files per commit
|
||||
files := make([]string, 0, len(hook.Commits)*4)
|
||||
for _, c := range hook.Commits {
|
||||
files = append(files, c.Added...)
|
||||
files = append(files, c.Removed...)
|
||||
files = append(files, c.Modified...)
|
||||
}
|
||||
|
||||
files = append(files, hook.HeadCommit.Added...)
|
||||
files = append(files, hook.HeadCommit.Removed...)
|
||||
files = append(files, hook.HeadCommit.Modified...)
|
||||
|
||||
return utils.DeduplicateStrings(files)
|
||||
}
|
||||
|
||||
// pipelineFromTag extracts the Pipeline data from a Forgejo tag hook.
|
||||
func pipelineFromTag(hook *pushHook) *model.Pipeline {
|
||||
avatar := expandAvatar(
|
||||
hook.Repo.HTMLURL,
|
||||
fixMalformedAvatar(hook.Sender.AvatarURL),
|
||||
)
|
||||
ref := strings.TrimPrefix(hook.Ref, "refs/tags/")
|
||||
|
||||
return &model.Pipeline{
|
||||
Event: model.EventTag,
|
||||
Commit: hook.Sha,
|
||||
Ref: fmt.Sprintf("refs/tags/%s", ref),
|
||||
ForgeURL: fmt.Sprintf("%s/src/tag/%s", hook.Repo.HTMLURL, ref),
|
||||
Message: fmt.Sprintf("created tag %s", ref),
|
||||
Avatar: avatar,
|
||||
Author: hook.Sender.UserName,
|
||||
Sender: hook.Sender.UserName,
|
||||
Email: hook.Sender.Email,
|
||||
Timestamp: time.Now().UTC().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// pipelineFromPullRequest extracts the Pipeline data from a Forgejo pull_request hook.
|
||||
func pipelineFromPullRequest(hook *pullRequestHook) *model.Pipeline {
|
||||
avatar := expandAvatar(
|
||||
hook.Repo.HTMLURL,
|
||||
fixMalformedAvatar(hook.PullRequest.Poster.AvatarURL),
|
||||
)
|
||||
|
||||
event := model.EventPull
|
||||
if hook.Action == actionClose {
|
||||
event = model.EventPullClosed
|
||||
}
|
||||
|
||||
pipeline := &model.Pipeline{
|
||||
Event: event,
|
||||
Commit: hook.PullRequest.Head.Sha,
|
||||
ForgeURL: hook.PullRequest.HTMLURL,
|
||||
Ref: fmt.Sprintf("refs/pull/%d/head", hook.Number),
|
||||
Branch: hook.PullRequest.Base.Ref,
|
||||
Message: hook.PullRequest.Title,
|
||||
Author: hook.PullRequest.Poster.UserName,
|
||||
Avatar: avatar,
|
||||
Sender: hook.Sender.UserName,
|
||||
Email: hook.Sender.Email,
|
||||
Title: hook.PullRequest.Title,
|
||||
Refspec: fmt.Sprintf("%s:%s",
|
||||
hook.PullRequest.Head.Ref,
|
||||
hook.PullRequest.Base.Ref,
|
||||
),
|
||||
PullRequestLabels: convertLabels(hook.PullRequest.Labels),
|
||||
}
|
||||
|
||||
return pipeline
|
||||
}
|
||||
|
||||
func pipelineFromRelease(hook *releaseHook) *model.Pipeline {
|
||||
avatar := expandAvatar(
|
||||
hook.Repo.HTMLURL,
|
||||
fixMalformedAvatar(hook.Sender.AvatarURL),
|
||||
)
|
||||
|
||||
return &model.Pipeline{
|
||||
Event: model.EventRelease,
|
||||
Ref: fmt.Sprintf("refs/tags/%s", hook.Release.TagName),
|
||||
ForgeURL: hook.Release.HTMLURL,
|
||||
Branch: hook.Release.Target,
|
||||
Message: fmt.Sprintf("created release %s", hook.Release.Title),
|
||||
Avatar: avatar,
|
||||
Author: hook.Sender.UserName,
|
||||
Sender: hook.Sender.UserName,
|
||||
Email: hook.Sender.Email,
|
||||
IsPrerelease: hook.Release.IsPrerelease,
|
||||
}
|
||||
}
|
||||
|
||||
// helper function that parses a push hook from a read closer.
|
||||
func parsePush(r io.Reader) (*pushHook, error) {
|
||||
push := new(pushHook)
|
||||
err := json.NewDecoder(r).Decode(push)
|
||||
return push, err
|
||||
}
|
||||
|
||||
func parsePullRequest(r io.Reader) (*pullRequestHook, error) {
|
||||
pr := new(pullRequestHook)
|
||||
err := json.NewDecoder(r).Decode(pr)
|
||||
return pr, err
|
||||
}
|
||||
|
||||
func parseRelease(r io.Reader) (*releaseHook, error) {
|
||||
pr := new(releaseHook)
|
||||
err := json.NewDecoder(r).Decode(pr)
|
||||
return pr, err
|
||||
}
|
||||
|
||||
// fixMalformedAvatar is a helper function that fixes an avatar url if malformed
|
||||
// (currently a known bug with forgejo).
|
||||
func fixMalformedAvatar(url string) string {
|
||||
index := strings.Index(url, "///")
|
||||
if index != -1 {
|
||||
return url[index+1:]
|
||||
}
|
||||
index = strings.Index(url, "//avatars/")
|
||||
if index != -1 {
|
||||
return strings.ReplaceAll(url, "//avatars/", "/avatars/")
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// expandAvatar is a helper function that converts a relative avatar URL to the
|
||||
// absolute url.
|
||||
func expandAvatar(repo, rawurl string) string {
|
||||
aurl, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return rawurl
|
||||
}
|
||||
if aurl.IsAbs() {
|
||||
// Url is already absolute
|
||||
return aurl.String()
|
||||
}
|
||||
|
||||
// Resolve to base
|
||||
burl, err := url.Parse(repo)
|
||||
if err != nil {
|
||||
return rawurl
|
||||
}
|
||||
aurl = burl.ResolveReference(aurl)
|
||||
|
||||
return aurl.String()
|
||||
}
|
||||
|
||||
// helper function to return matching hooks.
|
||||
func matchingHooks(hooks []*forgejo.Hook, rawurl string) *forgejo.Hook {
|
||||
link, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, hook := range hooks {
|
||||
if val, ok := hook.Config["url"]; ok {
|
||||
hookurl, err := url.Parse(val)
|
||||
if err == nil && hookurl.Host == link.Host {
|
||||
return hook
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertLabels(from []*forgejo.Label) []string {
|
||||
labels := make([]string, len(from))
|
||||
for i, label := range from {
|
||||
labels[i] = label.Name
|
||||
}
|
||||
return labels
|
||||
}
|
||||
273
server/forge/forgejo/helper_test.go
Normal file
273
server/forge/forgejo/helper_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo"
|
||||
"github.com/franela/goblin"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/forgejo/fixtures"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
|
||||
)
|
||||
|
||||
func Test_parse(t *testing.T) {
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Forgejo", func() {
|
||||
g.It("Should parse push hook payload", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPush)
|
||||
hook, err := parsePush(buf)
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(hook.Ref).Equal("refs/heads/main")
|
||||
g.Assert(hook.After).Equal("ef98532add3b2feb7a137426bba1248724367df5")
|
||||
g.Assert(hook.Before).Equal("4b2626259b5a97b6b4eab5e6cca66adb986b672b")
|
||||
g.Assert(hook.Compare).Equal("http://forgejo.golang.org/gordon/hello-world/compare/4b2626259b5a97b6b4eab5e6cca66adb986b672b...ef98532add3b2feb7a137426bba1248724367df5")
|
||||
g.Assert(hook.Repo.Name).Equal("hello-world")
|
||||
g.Assert(hook.Repo.HTMLURL).Equal("http://forgejo.golang.org/gordon/hello-world")
|
||||
g.Assert(hook.Repo.Owner.UserName).Equal("gordon")
|
||||
g.Assert(hook.Repo.FullName).Equal("gordon/hello-world")
|
||||
g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org")
|
||||
g.Assert(hook.Repo.Private).Equal(true)
|
||||
g.Assert(hook.Pusher.Email).Equal("gordon@golang.org")
|
||||
g.Assert(hook.Pusher.UserName).Equal("gordon")
|
||||
g.Assert(hook.Sender.UserName).Equal("gordon")
|
||||
g.Assert(hook.Sender.AvatarURL).Equal("http://forgejo.golang.org///1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87")
|
||||
})
|
||||
|
||||
g.It("Should parse tag hook payload", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookTag)
|
||||
hook, err := parsePush(buf)
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(hook.Ref).Equal("v1.0.0")
|
||||
g.Assert(hook.Sha).Equal("ef98532add3b2feb7a137426bba1248724367df5")
|
||||
g.Assert(hook.Repo.Name).Equal("hello-world")
|
||||
g.Assert(hook.Repo.HTMLURL).Equal("http://forgejo.golang.org/gordon/hello-world")
|
||||
g.Assert(hook.Repo.FullName).Equal("gordon/hello-world")
|
||||
g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org")
|
||||
g.Assert(hook.Repo.Owner.UserName).Equal("gordon")
|
||||
g.Assert(hook.Repo.Private).Equal(true)
|
||||
g.Assert(hook.Sender.UserName).Equal("gordon")
|
||||
g.Assert(hook.Sender.AvatarURL).Equal("https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87")
|
||||
})
|
||||
|
||||
g.It("Should parse pull_request hook payload", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPullRequest)
|
||||
hook, err := parsePullRequest(buf)
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(hook.Action).Equal("opened")
|
||||
g.Assert(hook.Number).Equal(int64(1))
|
||||
|
||||
g.Assert(hook.Repo.Name).Equal("hello-world")
|
||||
g.Assert(hook.Repo.HTMLURL).Equal("http://forgejo.golang.org/gordon/hello-world")
|
||||
g.Assert(hook.Repo.FullName).Equal("gordon/hello-world")
|
||||
g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org")
|
||||
g.Assert(hook.Repo.Owner.UserName).Equal("gordon")
|
||||
g.Assert(hook.Repo.Private).Equal(true)
|
||||
g.Assert(hook.Sender.UserName).Equal("gordon")
|
||||
g.Assert(hook.Sender.AvatarURL).Equal("https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87")
|
||||
|
||||
g.Assert(hook.PullRequest.Title).Equal("Update the README with new information")
|
||||
g.Assert(hook.PullRequest.Body).Equal("please merge")
|
||||
g.Assert(hook.PullRequest.State).Equal(forgejo.StateOpen)
|
||||
g.Assert(hook.PullRequest.Poster.UserName).Equal("gordon")
|
||||
g.Assert(hook.PullRequest.Base.Name).Equal("main")
|
||||
g.Assert(hook.PullRequest.Base.Ref).Equal("main")
|
||||
g.Assert(hook.PullRequest.Head.Name).Equal("feature/changes")
|
||||
g.Assert(hook.PullRequest.Head.Ref).Equal("feature/changes")
|
||||
})
|
||||
|
||||
g.It("Should return a Pipeline struct from a push hook", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPush)
|
||||
hook, _ := parsePush(buf)
|
||||
pipeline := pipelineFromPush(hook)
|
||||
g.Assert(pipeline.Event).Equal(model.EventPush)
|
||||
g.Assert(pipeline.Commit).Equal(hook.After)
|
||||
g.Assert(pipeline.Ref).Equal(hook.Ref)
|
||||
g.Assert(pipeline.ForgeURL).Equal(hook.Commits[0].URL)
|
||||
g.Assert(pipeline.Branch).Equal("main")
|
||||
g.Assert(pipeline.Message).Equal(hook.Commits[0].Message)
|
||||
g.Assert(pipeline.Avatar).Equal("http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87")
|
||||
g.Assert(pipeline.Author).Equal(hook.Sender.UserName)
|
||||
g.Assert(utils.EqualSliceValues(pipeline.ChangedFiles, []string{"CHANGELOG.md", "app/controller/application.rb"})).IsTrue()
|
||||
})
|
||||
|
||||
g.It("Should return a Repo struct from a push hook", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPush)
|
||||
hook, _ := parsePush(buf)
|
||||
repo := toRepo(hook.Repo)
|
||||
g.Assert(repo.Name).Equal(hook.Repo.Name)
|
||||
g.Assert(repo.Owner).Equal(hook.Repo.Owner.UserName)
|
||||
g.Assert(repo.FullName).Equal("gordon/hello-world")
|
||||
g.Assert(repo.ForgeURL).Equal(hook.Repo.HTMLURL)
|
||||
})
|
||||
|
||||
g.It("Should return a Pipeline struct from a tag hook", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookTag)
|
||||
hook, _ := parsePush(buf)
|
||||
pipeline := pipelineFromTag(hook)
|
||||
g.Assert(pipeline.Event).Equal(model.EventTag)
|
||||
g.Assert(pipeline.Commit).Equal(hook.Sha)
|
||||
g.Assert(pipeline.Ref).Equal("refs/tags/v1.0.0")
|
||||
g.Assert(pipeline.Branch).Equal("")
|
||||
g.Assert(pipeline.ForgeURL).Equal("http://forgejo.golang.org/gordon/hello-world/src/tag/v1.0.0")
|
||||
g.Assert(pipeline.Message).Equal("created tag v1.0.0")
|
||||
})
|
||||
|
||||
g.It("Should return a Pipeline struct from a pull_request hook", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPullRequest)
|
||||
hook, _ := parsePullRequest(buf)
|
||||
pipeline := pipelineFromPullRequest(hook)
|
||||
g.Assert(pipeline.Event).Equal(model.EventPull)
|
||||
g.Assert(pipeline.Commit).Equal(hook.PullRequest.Head.Sha)
|
||||
g.Assert(pipeline.Ref).Equal("refs/pull/1/head")
|
||||
g.Assert(pipeline.ForgeURL).Equal("http://forgejo.golang.org/gordon/hello-world/pull/1")
|
||||
g.Assert(pipeline.Branch).Equal("main")
|
||||
g.Assert(pipeline.Refspec).Equal("feature/changes:main")
|
||||
g.Assert(pipeline.Message).Equal(hook.PullRequest.Title)
|
||||
g.Assert(pipeline.Avatar).Equal("http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87")
|
||||
g.Assert(pipeline.Author).Equal(hook.PullRequest.Poster.UserName)
|
||||
})
|
||||
|
||||
g.It("Should return a Repo struct from a pull_request hook", func() {
|
||||
buf := bytes.NewBufferString(fixtures.HookPullRequest)
|
||||
hook, _ := parsePullRequest(buf)
|
||||
repo := toRepo(hook.Repo)
|
||||
g.Assert(repo.Name).Equal(hook.Repo.Name)
|
||||
g.Assert(repo.Owner).Equal(hook.Repo.Owner.UserName)
|
||||
g.Assert(repo.FullName).Equal("gordon/hello-world")
|
||||
g.Assert(repo.ForgeURL).Equal(hook.Repo.HTMLURL)
|
||||
})
|
||||
|
||||
g.It("Should return a Perm struct from a Forgejo Perm", func() {
|
||||
perms := []forgejo.Permission{
|
||||
{
|
||||
Admin: true,
|
||||
Push: true,
|
||||
Pull: true,
|
||||
},
|
||||
{
|
||||
Admin: true,
|
||||
Push: true,
|
||||
Pull: false,
|
||||
},
|
||||
{
|
||||
Admin: true,
|
||||
Push: false,
|
||||
Pull: false,
|
||||
},
|
||||
}
|
||||
for _, from := range perms {
|
||||
perm := toPerm(&from)
|
||||
g.Assert(perm.Pull).Equal(from.Pull)
|
||||
g.Assert(perm.Push).Equal(from.Push)
|
||||
g.Assert(perm.Admin).Equal(from.Admin)
|
||||
}
|
||||
})
|
||||
|
||||
g.It("Should return a Team struct from a Forgejo Org", func() {
|
||||
from := &forgejo.Organization{
|
||||
UserName: "woodpecker",
|
||||
AvatarURL: "/avatars/1",
|
||||
}
|
||||
|
||||
to := toTeam(from, "http://localhost:80")
|
||||
g.Assert(to.Login).Equal(from.UserName)
|
||||
g.Assert(to.Avatar).Equal("http://localhost:80/avatars/1")
|
||||
})
|
||||
|
||||
g.It("Should return a Repo struct from a Forgejo Repo", func() {
|
||||
from := forgejo.Repository{
|
||||
FullName: "gophers/hello-world",
|
||||
Owner: &forgejo.User{
|
||||
UserName: "gordon",
|
||||
AvatarURL: "http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
},
|
||||
CloneURL: "http://forgejo.golang.org/gophers/hello-world.git",
|
||||
HTMLURL: "http://forgejo.golang.org/gophers/hello-world",
|
||||
Private: true,
|
||||
DefaultBranch: "main",
|
||||
Permissions: &forgejo.Permission{Admin: true},
|
||||
}
|
||||
repo := toRepo(&from)
|
||||
g.Assert(repo.FullName).Equal(from.FullName)
|
||||
g.Assert(repo.Owner).Equal(from.Owner.UserName)
|
||||
g.Assert(repo.Name).Equal("hello-world")
|
||||
g.Assert(repo.Branch).Equal("main")
|
||||
g.Assert(repo.ForgeURL).Equal(from.HTMLURL)
|
||||
g.Assert(repo.Clone).Equal(from.CloneURL)
|
||||
g.Assert(repo.Avatar).Equal(from.Owner.AvatarURL)
|
||||
g.Assert(repo.IsSCMPrivate).Equal(from.Private)
|
||||
g.Assert(repo.Perm.Admin).IsTrue()
|
||||
})
|
||||
|
||||
g.It("Should correct a malformed avatar url", func() {
|
||||
urls := []struct {
|
||||
Before string
|
||||
After string
|
||||
}{
|
||||
{
|
||||
"http://forgejo.golang.org///1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
"//1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
},
|
||||
{
|
||||
"//1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
"//1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
},
|
||||
{
|
||||
"http://forgejo.golang.org/avatars/1",
|
||||
"http://forgejo.golang.org/avatars/1",
|
||||
},
|
||||
{
|
||||
"http://forgejo.golang.org//avatars/1",
|
||||
"http://forgejo.golang.org/avatars/1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
got := fixMalformedAvatar(url.Before)
|
||||
g.Assert(got).Equal(url.After)
|
||||
}
|
||||
})
|
||||
|
||||
g.It("Should expand the avatar url", func() {
|
||||
urls := []struct {
|
||||
Before string
|
||||
After string
|
||||
}{
|
||||
{
|
||||
"/avatars/1",
|
||||
"http://forgejo.io/avatars/1",
|
||||
},
|
||||
{
|
||||
"//1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
"http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
},
|
||||
{
|
||||
"/forgejo/avatars/2",
|
||||
"http://forgejo.io/forgejo/avatars/2",
|
||||
},
|
||||
}
|
||||
|
||||
repo := "http://forgejo.io/foo/bar"
|
||||
for _, url := range urls {
|
||||
got := expandAvatar(repo, url.Before)
|
||||
g.Assert(got).Equal(url.After)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
139
server/forge/forgejo/parse.go
Normal file
139
server/forge/forgejo/parse.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
)
|
||||
|
||||
const (
|
||||
hookEvent = "X-Forgejo-Event"
|
||||
hookPush = "push"
|
||||
hookCreated = "create"
|
||||
hookPullRequest = "pull_request"
|
||||
hookRelease = "release"
|
||||
|
||||
actionOpen = "opened"
|
||||
actionSync = "synchronized"
|
||||
actionClose = "closed"
|
||||
|
||||
refBranch = "branch"
|
||||
refTag = "tag"
|
||||
)
|
||||
|
||||
// parseHook parses a Forgejo hook from an http.Request and returns
|
||||
// Repo and Pipeline detail. If a hook type is unsupported nil values are returned.
|
||||
func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) {
|
||||
hookType := r.Header.Get(hookEvent)
|
||||
switch hookType {
|
||||
case hookPush:
|
||||
return parsePushHook(r.Body)
|
||||
case hookCreated:
|
||||
return parseCreatedHook(r.Body)
|
||||
case hookPullRequest:
|
||||
return parsePullRequestHook(r.Body)
|
||||
case hookRelease:
|
||||
return parseReleaseHook(r.Body)
|
||||
}
|
||||
log.Debug().Msgf("unsupported hook type: '%s'", hookType)
|
||||
return nil, nil, &types.ErrIgnoreEvent{Event: hookType}
|
||||
}
|
||||
|
||||
// parsePushHook parses a push hook and returns the Repo and Pipeline details.
|
||||
// If the commit type is unsupported nil values are returned.
|
||||
func parsePushHook(payload io.Reader) (repo *model.Repo, pipeline *model.Pipeline, err error) {
|
||||
push, err := parsePush(payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// ignore push events for tags
|
||||
if strings.HasPrefix(push.Ref, "refs/tags/") {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// TODO is this even needed?
|
||||
if push.RefType == refBranch {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
repo = toRepo(push.Repo)
|
||||
pipeline = pipelineFromPush(push)
|
||||
return repo, pipeline, err
|
||||
}
|
||||
|
||||
// parseCreatedHook parses a push hook and returns the Repo and Pipeline details.
|
||||
// If the commit type is unsupported nil values are returned.
|
||||
func parseCreatedHook(payload io.Reader) (repo *model.Repo, pipeline *model.Pipeline, err error) {
|
||||
push, err := parsePush(payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if push.RefType != refTag {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
repo = toRepo(push.Repo)
|
||||
pipeline = pipelineFromTag(push)
|
||||
return repo, pipeline, nil
|
||||
}
|
||||
|
||||
// parsePullRequestHook parses a pull_request hook and returns the Repo and Pipeline details.
|
||||
func parsePullRequestHook(payload io.Reader) (*model.Repo, *model.Pipeline, error) {
|
||||
var (
|
||||
repo *model.Repo
|
||||
pipeline *model.Pipeline
|
||||
)
|
||||
|
||||
pr, err := parsePullRequest(payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Don't trigger pipelines for non-code changes ...
|
||||
if pr.Action != actionOpen && pr.Action != actionSync && pr.Action != actionClose {
|
||||
log.Debug().Msgf("pull_request action is '%s' and no open or sync", pr.Action)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
repo = toRepo(pr.Repo)
|
||||
pipeline = pipelineFromPullRequest(pr)
|
||||
return repo, pipeline, err
|
||||
}
|
||||
|
||||
// parseReleaseHook parses a release hook and returns the Repo and Pipeline details.
|
||||
func parseReleaseHook(payload io.Reader) (*model.Repo, *model.Pipeline, error) {
|
||||
var (
|
||||
repo *model.Repo
|
||||
pipeline *model.Pipeline
|
||||
)
|
||||
|
||||
release, err := parseRelease(payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repo = toRepo(release.Repo)
|
||||
pipeline = pipelineFromRelease(release)
|
||||
return repo, pipeline, err
|
||||
}
|
||||
392
server/forge/forgejo/parse_test.go
Normal file
392
server/forge/forgejo/parse_test.go
Normal file
@@ -0,0 +1,392 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/forgejo/fixtures"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/model"
|
||||
)
|
||||
|
||||
func TestForgejoParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
event string
|
||||
err error
|
||||
repo *model.Repo
|
||||
pipe *model.Pipeline
|
||||
}{
|
||||
{
|
||||
name: "should ignore unsupported hook events",
|
||||
data: fixtures.HookPullRequest,
|
||||
event: "issues",
|
||||
err: &types.ErrIgnoreEvent{},
|
||||
},
|
||||
{
|
||||
name: "push event should handle a push hook",
|
||||
data: fixtures.HookPushBranch,
|
||||
event: "push",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "50820",
|
||||
Owner: "meisam",
|
||||
Name: "woodpecktester",
|
||||
FullName: "meisam/woodpecktester",
|
||||
Avatar: "https://codeberg.org/avatars/96512da76a14cf44e0bb32d1640e878e",
|
||||
ForgeURL: "https://codeberg.org/meisam/woodpecktester",
|
||||
Clone: "https://codeberg.org/meisam/woodpecktester.git",
|
||||
CloneSSH: "git@codeberg.org:meisam/woodpecktester.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
PREnabled: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "6543",
|
||||
Event: "push",
|
||||
Commit: "28c3613ae62640216bea5e7dc71aa65356e4298b",
|
||||
Branch: "fdsafdsa",
|
||||
Ref: "refs/heads/fdsafdsa",
|
||||
Message: "Delete '.woodpecker/.check.yml'\n",
|
||||
Sender: "6543",
|
||||
Avatar: "https://codeberg.org/avatars/09a234c768cb9bca78f6b2f82d6af173",
|
||||
Email: "6543@obermui.de",
|
||||
ForgeURL: "https://codeberg.org/meisam/woodpecktester/commit/28c3613ae62640216bea5e7dc71aa65356e4298b",
|
||||
ChangedFiles: []string{".woodpecker/.check.yml"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push event should extract repository and pipeline details",
|
||||
data: fixtures.HookPush,
|
||||
event: "push",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "1",
|
||||
Owner: "gordon",
|
||||
Name: "hello-world",
|
||||
FullName: "gordon/hello-world",
|
||||
Avatar: "http://forgejo.golang.org/gordon/hello-world",
|
||||
ForgeURL: "http://forgejo.golang.org/gordon/hello-world",
|
||||
Clone: "http://forgejo.golang.org/gordon/hello-world.git",
|
||||
CloneSSH: "git@forgejo.golang.org:gordon/hello-world.git",
|
||||
SCMKind: "git",
|
||||
IsSCMPrivate: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "gordon",
|
||||
Event: "push",
|
||||
Commit: "ef98532add3b2feb7a137426bba1248724367df5",
|
||||
Branch: "main",
|
||||
Ref: "refs/heads/main",
|
||||
Message: "bump\n",
|
||||
Sender: "gordon",
|
||||
Avatar: "http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
Email: "gordon@golang.org",
|
||||
ForgeURL: "http://forgejo.golang.org/gordon/hello-world/commit/ef98532add3b2feb7a137426bba1248724367df5",
|
||||
ChangedFiles: []string{"CHANGELOG.md", "app/controller/application.rb"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "push event should handle multi commit push",
|
||||
data: fixtures.HookPushMulti,
|
||||
event: "push",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "6",
|
||||
Owner: "Test-CI",
|
||||
Name: "multi-line-secrets",
|
||||
FullName: "Test-CI/multi-line-secrets",
|
||||
Avatar: "http://127.0.0.1:3000/avatars/5b0a83c2185b3cb1ebceb11062d6c2eb",
|
||||
ForgeURL: "http://127.0.0.1:3000/Test-CI/multi-line-secrets",
|
||||
Clone: "http://127.0.0.1:3000/Test-CI/multi-line-secrets.git",
|
||||
CloneSSH: "ssh://git@127.0.0.1:2200/Test-CI/multi-line-secrets.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "test-user",
|
||||
Event: "push",
|
||||
Commit: "29be01c073851cf0db0c6a466e396b725a670453",
|
||||
Branch: "main",
|
||||
Ref: "refs/heads/main",
|
||||
Message: "add some text\n",
|
||||
Sender: "test-user",
|
||||
Avatar: "http://127.0.0.1:3000/avatars/dd46a756faad4727fb679320751f6dea",
|
||||
Email: "test@noreply.localhost",
|
||||
ForgeURL: "http://127.0.0.1:3000/Test-CI/multi-line-secrets/compare/6efcf5b7c98f3e7a491675164b7a2e7acac27941...29be01c073851cf0db0c6a466e396b725a670453",
|
||||
ChangedFiles: []string{"aaa", "aa"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tag event should handle a tag hook",
|
||||
data: fixtures.HookTag,
|
||||
event: "create",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "12",
|
||||
Owner: "gordon",
|
||||
Name: "hello-world",
|
||||
FullName: "gordon/hello-world",
|
||||
Avatar: "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
ForgeURL: "http://forgejo.golang.org/gordon/hello-world",
|
||||
Clone: "http://forgejo.golang.org/gordon/hello-world.git",
|
||||
CloneSSH: "git@forgejo.golang.org:gordon/hello-world.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
IsSCMPrivate: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "gordon",
|
||||
Event: "tag",
|
||||
Commit: "ef98532add3b2feb7a137426bba1248724367df5",
|
||||
Ref: "refs/tags/v1.0.0",
|
||||
Message: "created tag v1.0.0",
|
||||
Sender: "gordon",
|
||||
Avatar: "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
Email: "gordon@golang.org",
|
||||
ForgeURL: "http://forgejo.golang.org/gordon/hello-world/src/tag/v1.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pull-request events should handle a PR hook when PR got created",
|
||||
data: fixtures.HookPullRequest,
|
||||
event: "pull_request",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "35129377",
|
||||
Owner: "gordon",
|
||||
Name: "hello-world",
|
||||
FullName: "gordon/hello-world",
|
||||
Avatar: "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
ForgeURL: "http://forgejo.golang.org/gordon/hello-world",
|
||||
Clone: "https://forgejo.golang.org/gordon/hello-world.git",
|
||||
CloneSSH: "",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
IsSCMPrivate: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "gordon",
|
||||
Event: "pull_request",
|
||||
Commit: "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
|
||||
Branch: "main",
|
||||
Ref: "refs/pull/1/head",
|
||||
Refspec: "feature/changes:main",
|
||||
Title: "Update the README with new information",
|
||||
Message: "Update the README with new information",
|
||||
Sender: "gordon",
|
||||
Avatar: "http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87",
|
||||
Email: "gordon@golang.org",
|
||||
ForgeURL: "http://forgejo.golang.org/gordon/hello-world/pull/1",
|
||||
PullRequestLabels: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pull-request events should handle a PR hook when PR got updated",
|
||||
data: fixtures.HookPullRequestUpdated,
|
||||
event: "pull_request",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "6",
|
||||
Owner: "Test-CI",
|
||||
Name: "multi-line-secrets",
|
||||
FullName: "Test-CI/multi-line-secrets",
|
||||
Avatar: "http://127.0.0.1:3000/avatars/5b0a83c2185b3cb1ebceb11062d6c2eb",
|
||||
ForgeURL: "http://127.0.0.1:3000/Test-CI/multi-line-secrets",
|
||||
Clone: "http://127.0.0.1:3000/Test-CI/multi-line-secrets.git",
|
||||
CloneSSH: "ssh://git@127.0.0.1:2200/Test-CI/multi-line-secrets.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
PREnabled: true,
|
||||
IsSCMPrivate: false,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "test",
|
||||
Event: "pull_request",
|
||||
Commit: "788ed8d02d3b7fcfcf6386dbcbca696aa1d4dc25",
|
||||
Branch: "main",
|
||||
Ref: "refs/pull/2/head",
|
||||
Refspec: "test-patch-1:main",
|
||||
Title: "New Pull",
|
||||
Message: "New Pull",
|
||||
Sender: "test",
|
||||
Avatar: "http://127.0.0.1:3000/avatars/dd46a756faad4727fb679320751f6dea",
|
||||
Email: "test@noreply.localhost",
|
||||
ForgeURL: "http://127.0.0.1:3000/Test-CI/multi-line-secrets/pulls/2",
|
||||
PullRequestLabels: []string{
|
||||
"Kind/Bug",
|
||||
"Kind/Security",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pull-request events should handle a PR closed hook when PR got closed",
|
||||
data: fixtures.HookPullRequestClosed,
|
||||
event: "pull_request",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "46534",
|
||||
Owner: "anbraten",
|
||||
Name: "test-repo",
|
||||
FullName: "anbraten/test-repo",
|
||||
Avatar: "https://seccdn.libravatar.org/avatar/fc9b6fe77c6b732a02925a62a81f05a0?d=identicon",
|
||||
ForgeURL: "https://forgejo.com/anbraten/test-repo",
|
||||
Clone: "https://forgejo.com/anbraten/test-repo.git",
|
||||
CloneSSH: "git@forgejo.com:anbraten/test-repo.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
PREnabled: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "anbraten",
|
||||
Event: "pull_request_closed",
|
||||
Commit: "d555a5dd07f4d0148a58d4686ec381502ae6a2d4",
|
||||
Branch: "main",
|
||||
Ref: "refs/pull/1/head",
|
||||
Refspec: "anbraten-patch-1:main",
|
||||
Title: "Adjust file",
|
||||
Message: "Adjust file",
|
||||
Sender: "anbraten",
|
||||
Avatar: "https://seccdn.libravatar.org/avatar/fc9b6fe77c6b732a02925a62a81f05a0?d=identicon",
|
||||
Email: "anbraten@sender.forgejo.com",
|
||||
ForgeURL: "https://forgejo.com/anbraten/test-repo/pulls/1",
|
||||
PullRequestLabels: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pull-request events should handle a PR closed hook when PR was merged",
|
||||
data: fixtures.HookPullRequestMerged,
|
||||
event: "pull_request",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "46534",
|
||||
Owner: "anbraten",
|
||||
Name: "test-repo",
|
||||
FullName: "anbraten/test-repo",
|
||||
Avatar: "https://seccdn.libravatar.org/avatar/fc9b6fe77c6b732a02925a62a81f05a0?d=identicon",
|
||||
ForgeURL: "https://forgejo.com/anbraten/test-repo",
|
||||
Clone: "https://forgejo.com/anbraten/test-repo.git",
|
||||
CloneSSH: "git@forgejo.com:anbraten/test-repo.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
PREnabled: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "anbraten",
|
||||
Event: "pull_request_closed",
|
||||
Commit: "d555a5dd07f4d0148a58d4686ec381502ae6a2d4",
|
||||
Branch: "main",
|
||||
Ref: "refs/pull/1/head",
|
||||
Refspec: "anbraten-patch-1:main",
|
||||
Title: "Adjust file",
|
||||
Message: "Adjust file",
|
||||
Sender: "anbraten",
|
||||
Avatar: "https://seccdn.libravatar.org/avatar/fc9b6fe77c6b732a02925a62a81f05a0?d=identicon",
|
||||
Email: "anbraten@noreply.forgejo.com",
|
||||
ForgeURL: "https://forgejo.com/anbraten/test-repo/pulls/1",
|
||||
PullRequestLabels: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "release events should handle release hook",
|
||||
data: fixtures.HookRelease,
|
||||
event: "release",
|
||||
repo: &model.Repo{
|
||||
ForgeRemoteID: "77",
|
||||
Owner: "anbraten",
|
||||
Name: "demo",
|
||||
FullName: "anbraten/demo",
|
||||
Avatar: "https://git.xxx/user/avatar/anbraten/-1",
|
||||
ForgeURL: "https://git.xxx/anbraten/demo",
|
||||
Clone: "https://git.xxx/anbraten/demo.git",
|
||||
CloneSSH: "ssh://git@git.xxx:22/anbraten/demo.git",
|
||||
Branch: "main",
|
||||
SCMKind: "git",
|
||||
PREnabled: true,
|
||||
IsSCMPrivate: true,
|
||||
Perm: &model.Perm{
|
||||
Pull: true,
|
||||
Push: true,
|
||||
Admin: true,
|
||||
},
|
||||
},
|
||||
pipe: &model.Pipeline{
|
||||
Author: "anbraten",
|
||||
Event: "release",
|
||||
Branch: "main",
|
||||
Ref: "refs/tags/0.0.5",
|
||||
Message: "created release Version 0.0.5",
|
||||
Sender: "anbraten",
|
||||
Avatar: "https://git.xxx/user/avatar/anbraten/-1",
|
||||
Email: "anbraten@noreply.xxx",
|
||||
ForgeURL: "https://git.xxx/anbraten/demo/releases/tag/0.0.5",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("POST", "/api/hook", bytes.NewBufferString(tc.data))
|
||||
req.Header = http.Header{}
|
||||
req.Header.Set(hookEvent, tc.event)
|
||||
r, p, err := parseHook(req)
|
||||
if tc.err != nil {
|
||||
assert.ErrorIs(t, err, tc.err)
|
||||
} else if assert.NoError(t, err) {
|
||||
assert.EqualValues(t, tc.repo, r)
|
||||
p.Timestamp = 0
|
||||
assert.EqualValues(t, tc.pipe, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
51
server/forge/forgejo/types.go
Normal file
51
server/forge/forgejo/types.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2024 Woodpecker Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package forgejo
|
||||
|
||||
import "codeberg.org/mvdkleijn/forgejo-sdk/forgejo"
|
||||
|
||||
type pushHook struct {
|
||||
Sha string `json:"sha"`
|
||||
Ref string `json:"ref"`
|
||||
Before string `json:"before"`
|
||||
After string `json:"after"`
|
||||
Compare string `json:"compare_url"`
|
||||
RefType string `json:"ref_type"`
|
||||
|
||||
Pusher *forgejo.User `json:"pusher"`
|
||||
|
||||
Repo *forgejo.Repository `json:"repository"`
|
||||
|
||||
Commits []forgejo.PayloadCommit `json:"commits"`
|
||||
|
||||
HeadCommit forgejo.PayloadCommit `json:"head_commit"`
|
||||
|
||||
Sender *forgejo.User `json:"sender"`
|
||||
}
|
||||
|
||||
type pullRequestHook struct {
|
||||
Action string `json:"action"`
|
||||
Number int64 `json:"number"`
|
||||
PullRequest *forgejo.PullRequest `json:"pull_request"`
|
||||
Repo *forgejo.Repository `json:"repository"`
|
||||
Sender *forgejo.User `json:"sender"`
|
||||
}
|
||||
|
||||
type releaseHook struct {
|
||||
Action string `json:"action"`
|
||||
Repo *forgejo.Repository `json:"repository"`
|
||||
Sender *forgejo.User `json:"sender"`
|
||||
Release *forgejo.Release
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/addon"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucket"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/bitbucketdatacenter"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/forgejo"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitea"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/github"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/forge/gitlab"
|
||||
@@ -29,6 +30,8 @@ func Forge(forge *model.Forge) (forge.Forge, error) {
|
||||
return setupBitbucket(forge)
|
||||
case model.ForgeTypeGitea:
|
||||
return setupGitea(forge)
|
||||
case model.ForgeTypeForgejo:
|
||||
return setupForgejo(forge)
|
||||
case model.ForgeTypeBitbucketDatacenter:
|
||||
return setupBitbucketDatacenter(forge)
|
||||
default:
|
||||
@@ -65,6 +68,26 @@ func setupGitea(forge *model.Forge) (forge.Forge, error) {
|
||||
return gitea.New(opts)
|
||||
}
|
||||
|
||||
func setupForgejo(forge *model.Forge) (forge.Forge, error) {
|
||||
server, err := url.Parse(forge.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := forgejo.Opts{
|
||||
URL: strings.TrimRight(server.String(), "/"),
|
||||
Client: forge.Client,
|
||||
Secret: forge.ClientSecret,
|
||||
SkipVerify: forge.SkipVerify,
|
||||
OAuth2URL: forge.OAuthHost,
|
||||
}
|
||||
if len(opts.URL) == 0 {
|
||||
return nil, fmt.Errorf("WOODPECKER_FORGEJO_URL must be set")
|
||||
}
|
||||
log.Trace().Msgf("Forge (forgejo) opts: %#v", opts)
|
||||
return forgejo.New(opts)
|
||||
}
|
||||
|
||||
func setupGitLab(forge *model.Forge) (forge.Forge, error) {
|
||||
return gitlab.New(gitlab.Opts{
|
||||
URL: forge.URL,
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
ForgeTypeGithub ForgeType = "github"
|
||||
ForgeTypeGitlab ForgeType = "gitlab"
|
||||
ForgeTypeGitea ForgeType = "gitea"
|
||||
ForgeTypeForgejo ForgeType = "forgejo"
|
||||
ForgeTypeBitbucket ForgeType = "bitbucket"
|
||||
ForgeTypeBitbucketDatacenter ForgeType = "bitbucket-dc"
|
||||
ForgeTypeAddon ForgeType = "addon"
|
||||
|
||||
@@ -142,6 +142,12 @@ func setupForgeService(c *cli.Context, _store store.Store) error {
|
||||
if _forge.URL == "" {
|
||||
_forge.URL = "https://try.gitea.com"
|
||||
}
|
||||
case c.Bool("forgejo"):
|
||||
_forge.Type = model.ForgeTypeForgejo
|
||||
// TODO enable oauth URL with generic config option
|
||||
if _forge.URL == "" {
|
||||
_forge.URL = "https://next.forgejo.org"
|
||||
}
|
||||
case c.Bool("bitbucket"):
|
||||
_forge.Type = model.ForgeTypeBitbucket
|
||||
case c.Bool("bitbucket-dc"):
|
||||
|
||||
Reference in New Issue
Block a user