1
0
Fork 0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2025-04-26 05:24:45 +00:00

Drop coding support ()

Coding support is likely broken and nobody will ever fix it. Also it
looks like nobody wants to use it, otherwise we would have get some bug
reports.

---------

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
qwerty287 2023-03-19 09:36:04 +01:00 committed by GitHub
parent 7ddc18348f
commit 37dc8a46e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 11 additions and 2235 deletions

View file

@ -453,65 +453,6 @@ var flags = []cli.Flag{
Usage: "stash skip ssl verification",
},
//
// Coding
//
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_CODING"},
Name: "coding",
Usage: "coding driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CODING_URL"},
Name: "coding-server",
Usage: "coding server address",
Value: "https://coding.net",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CODING_CLIENT"},
Name: "coding-client",
Usage: "coding oauth2 client id",
FilePath: os.Getenv("WOODPECKER_CODING_CLIENT_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CODING_SECRET"},
Name: "coding-secret",
Usage: "coding oauth2 client secret",
FilePath: os.Getenv("WOODPECKER_CODING_SECRET_FILE"),
},
&cli.StringSliceFlag{
EnvVars: []string{"WOODPECKER_CODING_SCOPE"},
Name: "coding-scope",
Usage: "coding oauth scope",
Value: cli.NewStringSlice(
"user",
"project",
"project:depot",
),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CODING_GIT_MACHINE"},
Name: "coding-git-machine",
Usage: "coding machine name",
Value: "git.coding.net",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CODING_GIT_USERNAME"},
Name: "coding-git-username",
Usage: "coding machine user username",
FilePath: os.Getenv("WOODPECKER_CODING_GIT_USERNAME_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_CODING_GIT_PASSWORD"},
Name: "coding-git-password",
Usage: "coding machine user password",
FilePath: os.Getenv("WOODPECKER_CODING_GIT_PASSWORD_FILE"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_CODING_SKIP_VERIFY"},
Name: "coding-skip-verify",
Usage: "coding skip ssl verification",
},
//
// development flags
//
&cli.StringFlag{

View file

@ -39,7 +39,6 @@ import (
"github.com/woodpecker-ci/woodpecker/server/forge"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucket"
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver"
"github.com/woodpecker-ci/woodpecker/server/forge/coding"
"github.com/woodpecker-ci/woodpecker/server/forge/gitea"
"github.com/woodpecker-ci/woodpecker/server/forge/github"
"github.com/woodpecker-ci/woodpecker/server/forge/gitlab"
@ -201,8 +200,6 @@ func setupForge(c *cli.Context) (forge.Forge, error) {
return setupGogs(c)
case c.Bool("gitea"):
return setupGitea(c)
case c.Bool("coding"):
return setupCoding(c)
default:
return nil, fmt.Errorf("version control system not configured")
}
@ -288,21 +285,6 @@ func setupGitHub(c *cli.Context) (forge.Forge, error) {
return github.New(opts)
}
// helper function to setup the Coding forge from the CLI arguments.
func setupCoding(c *cli.Context) (forge.Forge, error) {
opts := coding.Opts{
URL: c.String("coding-server"),
Client: c.String("coding-client"),
Secret: c.String("coding-secret"),
Scopes: c.StringSlice("coding-scope"),
Username: c.String("coding-git-username"),
Password: c.String("coding-git-password"),
SkipVerify: c.Bool("coding-skip-verify"),
}
log.Trace().Msgf("Forge (coding) opts: %#v", opts)
return coding.New(opts)
}
func setupMetrics(g *errgroup.Group, _store store.Store) {
pendingSteps := promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "woodpecker",

View file

@ -415,7 +415,3 @@ See [Bitbucket server configuration](forges/bitbucket_server/#configuration)
### `WOODPECKER_GITLAB_...`
See [Gitlab configuration](forges/gitlab/#configuration)
### `WOODPECKER_CODING_...`
See [Coding configuration](forges/coding/#configuration)

View file

@ -2,14 +2,14 @@
## Supported features
| Feature | [GitHub](github/) | [Gitea](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) | [Gogs](gogs/) | [Coding](coding/) |
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| Event: Push | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: |
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: |
| OAuth | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: |
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | :x: | :x: |
| Feature | [GitHub](github/) | [Gitea](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) | [Gogs](gogs/) |
| --- | :---: | :---: | :---: | :---: | :---: | :---: |
| Event: Push | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | :x: |
| OAuth | :white_check_mark: | :white_check_mark: | :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: | :x: | :x: | :x: |
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | :x: |
¹) for Gitea versions 1.17 or lower not for pull requests
¹ for Gitea versions 1.17 or lower not for pull requests

View file

@ -1,70 +0,0 @@
# Coding
## Configuration
This is a full list of configuration options. Please note that many of these options use default configuration values that should work for the majority of installations.
### `WOODPECKER_CODING`
> Default: `false`
Enables the Coding driver.
### `WOODPECKER_CODING_URL`
> Default: `https://coding.net`
Configures the Coding server address.
### `WOODPECKER_CODING_CLIENT`
> Default: empty
Configures the Coding OAuth client id. This is used to authorize access.
### `WOODPECKER_CODING_CLIENT_FILE`
> Default: empty
Read the value for `WOODPECKER_CODING_CLIENT` from the specified filepath
### `WOODPECKER_CODING_SECRET`
> Default: empty
Configures the Coding OAuth client secret. This is used to authorize access.
### `WOODPECKER_CODING_SECRET_FILE`
> Default: empty
Read the value for `WOODPECKER_CODING_SECRET` from the specified filepath
### `WOODPECKER_CODING_SCOPE`
> Default: `user, project, project:depot`
Comma-separated list of OAuth scopes.
### `WOODPECKER_CODING_GIT_MACHINE`
> Default: `git.coding.net`
TODO
### `WOODPECKER_CODING_GIT_USERNAME`
> Default: empty
This username is used to authenticate and clone all private repositories.
### `WOODPECKER_CODING_GIT_USERNAME_FILE`
> Default: empty
Read the value for `WOODPECKER_CODING_GIT_USERNAME` from the specified filepath
### `WOODPECKER_CODING_GIT_PASSWORD`
> Default: empty
The password is used to authenticate and clone all private repositories.
### `WOODPECKER_CODING_GIT_PASSWORD_FILE`
> Default: empty
Read the value for `WOODPECKER_CODING_GIT_PASSWORD` from the specified filepath
### `WOODPECKER_CODING_SKIP_VERIFY`
> Default: `false`
Configure if SSL verification should be skipped.

View file

@ -15,6 +15,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Updated Prometheus gauge `*_job_*` to `*_step_*`
- Renamed config env `WOODPECKER_MAX_PROCS` to `WOODPECKER_MAX_WORKFLOWS` (still available as fallback)
- The pipelines are now also read from `.yaml` files, the new default order is `.woodpecker/*.yml` and `.woodpecker/*.yaml` (without any prioritization) -> `.woodpecker.yml` -> `.woodpecker.yaml` -> `.drone.yml`
- Dropped support for [Coding](https://coding.net/).
## 0.15.0

View file

@ -1,374 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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 coding
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"strings"
"golang.org/x/oauth2"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/forge"
"github.com/woodpecker-ci/woodpecker/server/forge/coding/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"
)
const (
defaultURL = "https://coding.net" // Default Coding URL
)
// Opts defines configuration options.
type Opts struct {
URL string // Coding server url.
Client string // Coding oauth client id.
Secret string // Coding oauth client secret.
Scopes []string // Coding oauth scopes.
Username string // Optional machine account username.
Password string // Optional machine account password.
SkipVerify bool // Skip ssl verification.
}
// New returns a Forge implementation that integrates with a Coding Platform or
// Coding Enterprise version control hosting provider.
func New(opts Opts) (forge.Forge, error) {
r := &Coding{
URL: defaultURL,
Client: opts.Client,
Secret: opts.Secret,
Scopes: opts.Scopes,
Username: opts.Username,
Password: opts.Password,
SkipVerify: opts.SkipVerify,
}
if opts.URL != defaultURL {
r.URL = strings.TrimSuffix(opts.URL, "/")
}
return r, nil
}
type Coding struct {
URL string
Client string
Secret string
Scopes []string
Username string
Password string
SkipVerify bool
}
// Name returns the string name of this driver
func (c *Coding) Name() string {
return "coding"
}
// Login authenticates the session and returns the
// forge user details.
func (c *Coding) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) {
config := c.newConfig(server.Config.Server.Host)
// get the OAuth errors
if err := req.FormValue("error"); err != "" {
return nil, &forge_types.AuthError{
Err: err,
Description: req.FormValue("error_description"),
URI: req.FormValue("error_uri"),
}
}
// get the OAuth code
code := req.FormValue("code")
if len(code) == 0 {
http.Redirect(res, req, config.AuthCodeURL("woodpecker"), http.StatusSeeOther)
return nil, nil
}
token, err := config.Exchange(c.newContext(ctx), code)
if err != nil {
return nil, err
}
user, err := c.newClientToken(ctx, token.AccessToken).GetCurrentUser()
if err != nil {
return nil, err
}
return &model.User{
Login: user.GlobalKey,
Email: user.Email,
Token: token.AccessToken,
Secret: token.RefreshToken,
Expiry: token.Expiry.UTC().Unix(),
Avatar: c.resourceLink(user.Avatar),
}, nil
}
// Auth authenticates the session and returns the forge user
// login for the given token and secret
func (c *Coding) Auth(ctx context.Context, token, _ string) (string, error) {
user, err := c.newClientToken(ctx, token).GetCurrentUser()
if err != nil {
return "", err
}
return user.GlobalKey, nil
}
// Refresh refreshes an oauth token and expiration for the given
// user. It returns true if the token was refreshed, false if the
// token was not refreshed, and error if it failed to refresh.
func (c *Coding) Refresh(ctx context.Context, u *model.User) (bool, error) {
config := c.newConfig("")
source := config.TokenSource(c.newContext(ctx), &oauth2.Token{RefreshToken: u.Secret})
token, err := source.Token()
if err != nil || len(token.AccessToken) == 0 {
return false, err
}
u.Token = token.AccessToken
u.Secret = token.RefreshToken
u.Expiry = token.Expiry.UTC().Unix()
return true, nil
}
// Teams fetches a list of team memberships from the forge.
func (c *Coding) Teams(_ context.Context, _ *model.User) ([]*model.Team, error) {
// EMPTY: not implemented in Coding OAuth API
return nil, forge_types.ErrNotImplemented
}
// TeamPerm fetches the named organization permissions from
// the forge for the specified user.
func (c *Coding) TeamPerm(_ *model.User, _ string) (*model.Perm, error) {
// EMPTY: not implemented in Coding OAuth API
return nil, nil
}
// Repo fetches the repository from the forge.
func (c *Coding) Repo(ctx context.Context, u *model.User, _ model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
client := c.newClient(ctx, u)
project, err := client.GetProject(owner, name)
if err != nil {
return nil, err
}
depot, err := client.GetDepot(owner, name)
if err != nil {
return nil, err
}
return &model.Repo{
// TODO(1138) ForgeID: project.ID,
Owner: project.Owner,
Name: project.Name,
FullName: projectFullName(project.Owner, project.Name),
Avatar: c.resourceLink(project.Icon),
Link: c.resourceLink(project.DepotPath),
SCMKind: model.RepoGit,
Clone: project.HTTPSURL,
Branch: depot.DefaultBranch,
IsSCMPrivate: !project.IsPublic,
}, nil
}
// Repos fetches a list of repos from the forge.
func (c *Coding) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
client := c.newClient(ctx, u)
projectList, err := client.GetProjectList()
if err != nil {
return nil, err
}
repos := make([]*model.Repo, 0)
for _, project := range projectList {
depot, err := client.GetDepot(project.Owner, project.Name)
if err != nil {
return nil, err
}
repo := &model.Repo{
// TODO(1138) ForgeID: project.ID,
Owner: project.Owner,
Name: project.Name,
FullName: projectFullName(project.Owner, project.Name),
Avatar: c.resourceLink(project.Icon),
Link: c.resourceLink(project.DepotPath),
SCMKind: model.RepoGit,
Clone: project.HTTPSURL,
Branch: depot.DefaultBranch,
IsSCMPrivate: !project.IsPublic,
}
repos = append(repos, repo)
}
return repos, nil
}
// Perm fetches the named repository permissions from
// the forge for the specified user.
func (c *Coding) Perm(ctx context.Context, u *model.User, repo *model.Repo) (*model.Perm, error) {
project, err := c.newClient(ctx, u).GetProject(repo.Owner, repo.Name)
if err != nil {
return nil, err
}
if project.Role == "owner" || project.Role == "admin" {
return &model.Perm{Pull: true, Push: true, Admin: true}, nil
}
if project.Role == "member" {
return &model.Perm{Pull: true, Push: true, Admin: false}, nil
}
return &model.Perm{Pull: false, Push: false, Admin: false}, nil
}
// File fetches a file from the forge repository and returns in string
// format.
func (c *Coding) File(ctx context.Context, u *model.User, r *model.Repo, b *model.Pipeline, f string) ([]byte, error) {
data, err := c.newClient(ctx, u).GetFile(r.Owner, r.Name, b.Commit, f)
if err != nil {
return nil, err
}
return data, nil
}
func (c *Coding) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ string) ([]*forge_types.FileMeta, error) {
return nil, forge_types.ErrNotImplemented
}
// Status sends the commit status to the forge.
func (c *Coding) Status(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ *model.Step) error {
// EMPTY: not implemented in Coding OAuth API
return nil
}
// Netrc returns a .netrc file that can be used to clone
// private repositories from a forge.
func (c *Coding) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
host, err := common.ExtractHostFromCloneURL(r.Clone)
if err != nil {
return nil, err
}
if c.Password != "" {
return &model.Netrc{
Login: c.Username,
Password: c.Password,
Machine: host,
}, nil
}
return &model.Netrc{
Login: u.Token,
Password: "x-oauth-basic",
Machine: host,
}, nil
}
// Activate activates a repository by creating the post-commit hook.
func (c *Coding) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
return c.newClient(ctx, u).AddWebhook(r.Owner, r.Name, link)
}
// Deactivate deactivates a repository by removing all previously created
// post-commit hooks matching the given link.
func (c *Coding) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
return c.newClient(ctx, u).RemoveWebhook(r.Owner, r.Name, link)
}
// Branches returns the names of all branches for the named repository.
func (c *Coding) Branches(_ context.Context, _ *model.User, r *model.Repo) ([]string, error) {
// TODO: fetch all branches
return []string{r.Branch}, nil
}
// BranchHead returns the sha of the head (latest commit) of the specified branch
func (c *Coding) BranchHead(_ context.Context, _ *model.User, _ *model.Repo, _ string) (string, error) {
// TODO(1138): missing implementation
return "", forge_types.ErrNotImplemented
}
// Hook parses the post-commit hook from the Request body and returns the
// required data in a standard format.
func (c *Coding) Hook(_ context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) {
repo, pipeline, err := parseHook(r)
if pipeline != nil {
pipeline.Avatar = c.resourceLink(pipeline.Avatar)
}
return repo, pipeline, err
}
// OrgMembership returns if user is member of organization and if user
// is admin/owner in this organization.
func (c *Coding) OrgMembership(_ context.Context, _ *model.User, _ string) (*model.OrgPerm, error) {
// TODO: Not supported in Coding OAuth API
return nil, nil
}
// helper function to return the Coding oauth2 context using an HTTPClient that
// disables TLS verification if disabled in the forge settings.
func (c *Coding) newContext(ctx context.Context) context.Context {
if !c.SkipVerify {
return ctx
}
return context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
})
}
// helper function to return the Coding oauth2 config
func (c *Coding) newConfig(redirect string) *oauth2.Config {
return &oauth2.Config{
ClientID: c.Client,
ClientSecret: c.Secret,
Scopes: []string{strings.Join(c.Scopes, ",")},
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/oauth_authorize.html", c.URL),
TokenURL: fmt.Sprintf("%s/api/oauth/access_token_v2", c.URL),
},
RedirectURL: fmt.Sprintf("%s/authorize", redirect),
}
}
// helper function to return the Coding oauth2 client
func (c *Coding) newClient(ctx context.Context, u *model.User) *internal.Client {
return c.newClientToken(ctx, u.Token)
}
// helper function to return the Coding oauth2 client
func (c *Coding) newClientToken(ctx context.Context, token string) *internal.Client {
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.SkipVerify,
},
},
}
return internal.NewClient(ctx, c.URL, "/api", token, "woodpecker", client)
}
func (c *Coding) resourceLink(resourcePath string) string {
if strings.HasPrefix(resourcePath, "http") {
return resourcePath
}
return c.URL + resourcePath
}

View file

@ -1,273 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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 coding
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/franela/goblin"
"github.com/gin-gonic/gin"
"github.com/woodpecker-ci/woodpecker/server/forge/coding/fixtures"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func Test_coding(t *testing.T) {
gin.SetMode(gin.TestMode)
s := httptest.NewServer(fixtures.Handler())
c := &Coding{URL: s.URL}
ctx := context.Background()
g := goblin.Goblin(t)
g.Describe("Coding", 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: "https://coding.net",
Client: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
Scopes: []string{"user", "project", "project:depot"},
Username: "someuser",
Password: "password",
SkipVerify: true,
})
g.Assert(forge.(*Coding).URL).Equal("https://coding.net")
g.Assert(forge.(*Coding).Client).Equal("KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP")
g.Assert(forge.(*Coding).Secret).Equal("zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp")
g.Assert(forge.(*Coding).Scopes).Equal([]string{"user", "project", "project:depot"})
g.Assert(forge.(*Coding).Username).Equal("someuser")
g.Assert(forge.(*Coding).Password).Equal("password")
g.Assert(forge.(*Coding).SkipVerify).Equal(true)
})
})
g.Describe("Given an authorization request", func() {
g.It("Should redirect to authorize", func() {
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "", nil)
_, err := c.Login(ctx, w, r)
g.Assert(err).IsNil()
g.Assert(w.Code).Equal(http.StatusSeeOther)
})
g.It("Should return authenticated user", func() {
r, _ := http.NewRequest("GET", "?code=code", nil)
u, err := c.Login(ctx, nil, r)
g.Assert(err).IsNil()
g.Assert(u.Login).Equal(fakeUser.Login)
g.Assert(u.Token).Equal(fakeUser.Token)
g.Assert(u.Secret).Equal(fakeUser.Secret)
})
})
g.Describe("Given an access token", func() {
g.It("Should return the authenticated user", func() {
login, err := c.Auth(ctx, fakeUser.Token, fakeUser.Secret)
g.Assert(err).IsNil()
g.Assert(login).Equal(fakeUser.Login)
})
g.It("Should handle a failure to resolve user", func() {
_, err := c.Auth(ctx, fakeUserNotFound.Token, fakeUserNotFound.Secret)
g.Assert(err).IsNotNil()
})
})
g.Describe("Given a refresh token", func() {
g.It("Should return a refresh access token", func() {
ok, err := c.Refresh(ctx, fakeUserRefresh)
g.Assert(err).IsNil()
g.Assert(ok).IsTrue()
g.Assert(fakeUserRefresh.Token).Equal("VDZupx0usVRV4oOd1FCu4xUxgk8SY0TK")
g.Assert(fakeUserRefresh.Secret).Equal("BenBQq7TWZ7Cp0aUM47nQjTz2QHNmTWcPctB609n")
})
g.It("Should handle an invalid refresh token", func() {
ok, _ := c.Refresh(ctx, fakeUserRefreshInvalid)
g.Assert(ok).IsFalse()
})
})
g.Describe("When requesting a repository", func() {
g.It("Should return the details", func() {
repo, err := c.Repo(ctx, fakeUser, "", fakeRepo.Owner, fakeRepo.Name)
g.Assert(err).IsNil()
g.Assert(repo.FullName).Equal(fakeRepo.FullName)
g.Assert(repo.Avatar).Equal(s.URL + fakeRepo.Avatar)
g.Assert(repo.Link).Equal(s.URL + fakeRepo.Link)
g.Assert(repo.SCMKind).Equal(fakeRepo.SCMKind)
g.Assert(repo.Clone).Equal(fakeRepo.Clone)
g.Assert(repo.Branch).Equal(fakeRepo.Branch)
g.Assert(repo.IsSCMPrivate).Equal(fakeRepo.IsSCMPrivate)
})
g.It("Should handle not found errors", func() {
_, err := c.Repo(ctx, fakeUser, "", fakeRepoNotFound.Owner, fakeRepoNotFound.Name)
g.Assert(err).IsNotNil()
})
})
g.Describe("When requesting repository permissions", func() {
g.It("Should authorize admin access for project owner", func() {
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_owner"})
g.Assert(err).IsNil()
g.Assert(perm.Pull).IsTrue()
g.Assert(perm.Push).IsTrue()
g.Assert(perm.Admin).IsTrue()
})
g.It("Should authorize admin access for project admin", func() {
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_admin"})
g.Assert(err).IsNil()
g.Assert(perm.Pull).IsTrue()
g.Assert(perm.Push).IsTrue()
g.Assert(perm.Admin).IsTrue()
})
g.It("Should authorize read access for project member", func() {
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_member"})
g.Assert(err).IsNil()
g.Assert(perm.Pull).IsTrue()
g.Assert(perm.Push).IsTrue()
g.Assert(perm.Admin).IsFalse()
})
g.It("Should authorize no access for project guest", func() {
perm, err := c.Perm(ctx, fakeUser, &model.Repo{Owner: "demo1", Name: "perm_guest"})
g.Assert(err).IsNil()
g.Assert(perm.Pull).IsFalse()
g.Assert(perm.Push).IsFalse()
g.Assert(perm.Admin).IsFalse()
})
g.It("Should handle not found errors", func() {
_, err := c.Perm(ctx, fakeUser, fakeRepoNotFound)
g.Assert(err).IsNotNil()
})
})
g.Describe("When downloading a file", func() {
g.It("Should return file for specified pipeline", func() {
data, err := c.File(ctx, fakeUser, fakeRepo, fakePipeline, ".woodpecker.yml")
g.Assert(err).IsNil()
g.Assert(string(data)).Equal("pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n")
})
})
g.Describe("When requesting a netrc config", func() {
g.It("Should return the netrc file for global credential", func() {
forge, _ := New(Opts{
Username: "someuser",
Password: "password",
})
netrc, err := forge.Netrc(fakeUser, fakeRepo)
g.Assert(err).IsNil()
g.Assert(netrc.Login).Equal("someuser")
g.Assert(netrc.Password).Equal("password")
g.Assert(netrc.Machine).Equal("git.coding.net")
})
g.It("Should return the netrc file for specified user", func() {
forge, _ := New(Opts{})
netrc, err := forge.Netrc(fakeUser, fakeRepo)
g.Assert(err).IsNil()
g.Assert(netrc.Login).Equal(fakeUser.Token)
g.Assert(netrc.Password).Equal("x-oauth-basic")
g.Assert(netrc.Machine).Equal("git.coding.net")
})
})
g.Describe("When activating a repository", func() {
g.It("Should create the hook", func() {
err := c.Activate(ctx, fakeUser, fakeRepo, "http://127.0.0.1")
g.Assert(err).IsNil()
})
g.It("Should update the hook when exists", func() {
err := c.Activate(ctx, fakeUser, fakeRepo, "http://127.0.0.2")
g.Assert(err).IsNil()
})
})
g.Describe("When deactivating a repository", func() {
g.It("Should successfully remove hook", func() {
err := c.Deactivate(ctx, fakeUser, fakeRepo, "http://127.0.0.3")
g.Assert(err).IsNil()
})
g.It("Should successfully deactivate when hook already removed", func() {
err := c.Deactivate(ctx, fakeUser, fakeRepo, "http://127.0.0.4")
g.Assert(err).IsNil()
})
})
g.Describe("When parsing post-commit hook body", func() {
g.It("Should parse the hook", func() {
buf := bytes.NewBufferString(fixtures.PushHook)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, _, err := c.Hook(ctx, req)
g.Assert(err).IsNil()
g.Assert(r.FullName).Equal("demo1/test1")
})
})
})
}
var (
fakeUser = &model.User{
Login: "demo1",
Token: "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
Secret: "zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
}
fakeUserNotFound = &model.User{
Login: "demo1",
Token: "8DpqlE0hI6yr5MLlq8ysAL4p72cKGwT0",
Secret: "8Em2dkFE8Xsze88Ar8LMG7TF4CO3VCQMgpKa0VCm",
}
fakeUserRefresh = &model.User{
Login: "demo1",
Secret: "i9i0HQqNR8bTY4rALYEF2itayFJNbnzC1eMFppwT",
}
fakeUserRefreshInvalid = &model.User{
Login: "demo1",
Secret: "invalid_refresh_token",
}
fakeRepo = &model.Repo{
Owner: "demo1",
Name: "test1",
FullName: "demo1/test1",
Avatar: "/static/project_icon/scenery-5.png",
Link: "/u/gilala/p/abp/git",
SCMKind: model.RepoGit,
Clone: "https://git.coding.net/demo1/test1.git",
Branch: "master",
IsSCMPrivate: true,
}
fakeRepoNotFound = &model.Repo{
Owner: "not_found_owner",
Name: "not_found_project",
}
fakePipeline = &model.Pipeline{
Commit: "4504a072cc",
}
)

View file

@ -1,305 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// Handler returns an http.Handler that is capable of handing a variety of mock
// Coding requests and returns mock responses.
func Handler() http.Handler {
gin.SetMode(gin.TestMode)
e := gin.New()
e.POST("/api/oauth/access_token_v2", getToken)
e.GET("/api/account/current_user", getUser)
e.GET("/api/user/:gk/project/:prj", getProject)
e.GET("/api/user/:gk/project/:prj/git", getDepot)
e.GET("/api/user/:gk/project/:prj/git/blob/:ref/:path", getFile)
e.GET("/api/user/:gk/project/:prj/git/hooks", getHooks)
e.POST("/api/user/:gk/project/:prj/git/hook", postHook)
e.PUT("/api/user/:gk/project/:prj/git/hook/:id", putHook)
e.DELETE("/api/user/:gk/project/:prj/git/hook/:id", deleteHook)
return e
}
func getToken(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch c.PostForm("grant_type") {
case "refresh_token":
switch c.PostForm("refresh_token") {
case "i9i0HQqNR8bTY4rALYEF2itayFJNbnzC1eMFppwT":
c.String(200, refreshedTokenPayload)
default:
c.String(200, invalidRefreshTokenPayload)
}
case "authorization_code":
fallthrough
default:
switch c.PostForm("code") {
case "code":
c.String(200, tokenPayload)
default:
c.String(200, invalidCodePayload)
}
}
}
func getUser(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch c.Query("access_token") {
case "KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP":
c.String(200, userPayload)
default:
c.String(200, userNotFoundPayload)
}
}
func getProject(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch fmt.Sprintf("%s/%s", c.Param("gk"), c.Param("prj")) {
case "demo1/test1":
c.String(200, fakeProjectPayload)
case "demo1/perm_owner":
c.String(200, fakePermOwnerPayload)
case "demo1/perm_admin":
c.String(200, fakePermAdminPayload)
case "demo1/perm_member":
c.String(200, fakePermMemberPayload)
case "demo1/perm_guest":
c.String(200, fakePermGuestPayload)
default:
c.String(200, projectNotFoundPayload)
}
}
func getDepot(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch fmt.Sprintf("%s/%s", c.Param("gk"), c.Param("prj")) {
case "demo1/test1":
c.String(200, fakeDepotPayload)
default:
c.String(200, projectNotFoundPayload)
}
}
func getFile(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch fmt.Sprintf("%s/%s/%s/%s", c.Param("gk"), c.Param("prj"), c.Param("ref"), c.Param("path")) {
case "demo1/test1/master/.woodpecker.yml", "demo1/test1/4504a072cc/.woodpecker.yml":
c.String(200, fakeFilePayload)
default:
c.String(200, fileNotFoundPayload)
}
}
func getHooks(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
c.String(200, fakeHooksPayload)
}
func postHook(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch c.PostForm("hook_url") {
case "http://127.0.0.1":
c.String(200, `{"code":0}`)
default:
c.String(200, `{"code":1}`)
}
}
func putHook(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch c.Param("id") {
case "2":
c.String(200, `{"code":0}`)
default:
c.String(200, `{"code":1}`)
}
}
func deleteHook(c *gin.Context) {
c.Header("Content-Type", "application/json;charset=UTF-8")
switch c.Param("id") {
case "3":
c.String(200, `{"code":0}`)
default:
c.String(200, `{"code":1}`)
}
}
const tokenPayload = `
{
"access_token":"KTNF2ALdm3ofbtxLh6IbV95Ro5AKWJUP",
"refresh_token":"zVtxJrKhNhBcNyqCz1NggNAAmehAxnRO3Z0fXmCp",
"expires_in":36000
}
`
const refreshedTokenPayload = `
{
"access_token":"VDZupx0usVRV4oOd1FCu4xUxgk8SY0TK",
"refresh_token":"BenBQq7TWZ7Cp0aUM47nQjTz2QHNmTWcPctB609n",
"expires_in":36000
}
`
const invalidRefreshTokenPayload = `
{
"code":3006,
"msg":{
"oauth_refresh_token_error":"Token校验失败"
}
}
`
const invalidCodePayload = `
{
"code":3003,
"msg":{
"oauth_validate_code_error":"code校验失败"
}
}
`
const userPayload = `
{
"code":0,
"data":{
"global_key":"demo1",
"email":"demo1@gmail.com",
"avatar":"/static/fruit_avatar/Fruit-20.png"
}
}
`
const userNotFoundPayload = `
{
"code":1,
"msg":{
"user_not_login":"用户未登录"
}
}
`
const fakeProjectPayload = `
{
"code":0,
"data":{
"owner_user_name":"demo1",
"name":"test1",
"depot_path":"/u/gilala/p/abp/git",
"https_url":"https://git.coding.net/demo1/test1.git",
"is_public": false,
"icon":"/static/project_icon/scenery-5.png",
"current_user_role":"owner"
}
}
`
const fakePermOwnerPayload = `
{
"code":0,
"data":{
"current_user_role":"owner"
}
}
`
const fakePermAdminPayload = `
{
"code":0,
"data":{
"current_user_role":"admin"
}
}
`
const fakePermMemberPayload = `
{
"code":0,
"data":{
"current_user_role":"member"
}
}
`
const fakePermGuestPayload = `
{
"code":0,
"data":{
"current_user_role":"guest"
}
}
`
const fakeDepotPayload = `
{
"code":0,
"data":{
"default_branch":"master"
}
}
`
const projectNotFoundPayload = `
{
"code":1100,
"msg":{
"project_not_exists":"项目不存在"
}
}
`
const fakeFilePayload = `
{
"code":0,
"data":{
"file":{
"data":"pipeline:\n test:\n image: golang:1.6\n commands:\n - go test\n"
}
}
}
`
const fileNotFoundPayload = `
{
"code":0,
"data":{
"ref":"master"
}
}
`
const fakeHooksPayload = `
{
"code":0,
"data":[
{
"id":2,
"hook_url":"http://127.0.0.2"
},
{
"id":3,
"hook_url":"http://127.0.0.3"
}
]
}
`

View file

@ -1,189 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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
const PushHook = `
{
"ref": "refs/heads/master",
"before": "861f2315056e8925e627a6f46518b9df05896e24",
"commits": [
{
"committer": {
"name": "demo1",
"email": "demo1@gmail.com"
},
"web_url": "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
"short_message": "new file .woodpecker.yml\n",
"sha": "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4"
}
],
"after": "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
"event": "push",
"repository": {
"owner": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
},
"https_url": "https://git.coding.net/demo1/test1.git",
"web_url": "https://coding.net/u/demo1/p/test1",
"project_id": "99999999",
"ssh_url": "git@git.coding.net:demo1/test1.git",
"name": "test1",
"description": ""
},
"user": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
}
}
`
const DeleteBranchPushHook = `
{
"ref": "refs/heads/master",
"before": "861f2315056e8925e627a6f46518b9df05896e24",
"after": "0000000000000000000000000000000000000000",
"event": "push",
"repository": {
"owner": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
},
"https_url": "https://git.coding.net/demo1/test1.git",
"web_url": "https://coding.net/u/demo1/p/test1",
"project_id": "99999999",
"ssh_url": "git@git.coding.net:demo1/test1.git",
"name": "test1",
"description": ""
},
"user": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
}
}
`
const PullRequestHook = `
{
"pull_request": {
"target_branch": "master",
"title": "pr1",
"body": "pr message",
"source_sha": "",
"source_repository": {
"owner": {
"path": "/u/demo2",
"web_url": "https://coding.net/u/demo2",
"global_key": "demo2",
"name": "demo2",
"avatar": "/static/fruit_avatar/Fruit-2.png"
},
"https_url": "https://git.coding.net/demo2/test2.git",
"web_url": "https://coding.net/u/demo2/p/test2",
"project_id": "7777777",
"ssh_url": "git@git.coding.net:demo2/test2.git",
"name": "test2",
"description": "",
"git_url": "git://git.coding.net/demo2/test2.git"
},
"source_branch": "master",
"number": 1,
"web_url": "https://coding.net/u/demo1/p/test2/git/pull/1",
"merge_commit_sha": "55e77b328b71d3ee4f9e70a5f67231b0acceeadc",
"target_sha": "",
"action": "create",
"id": 7586,
"user": {
"path": "/u/demo2",
"web_url": "https://coding.net/u/demo2",
"global_key": "demo2",
"name": "demo2",
"avatar": "/static/fruit_avatar/Fruit-2.png"
},
"status": "CANMERGE"
},
"repository": {
"owner": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
},
"https_url": "https://git.coding.net/demo1/test2.git",
"web_url": "https://coding.net/u/demo1/p/test2",
"project_id": "6666666",
"ssh_url": "git@git.coding.net:demo1/test2.git",
"name": "test2",
"description": "",
"git_url": "git://git.coding.net/demo1/test2.git"
},
"event": "pull_request"
}
`
const MergeRequestHook = `
{
"merge_request": {
"target_branch": "master",
"title": "mr1",
"body": "<p>mr message</p>",
"source_sha": "",
"source_branch": "branch1",
"number": 1,
"web_url": "https://coding.net/u/demo1/p/test1/git/merge/1",
"merge_commit_sha": "74e6755580c34e9fd81dbcfcbd43ee5f30259436",
"target_sha": "",
"action": "create",
"id": 533428,
"user": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
},
"status": "CANMERGE"
},
"repository": {
"owner": {
"path": "/u/demo1",
"web_url": "https://coding.net/u/demo1",
"global_key": "demo1",
"name": "demo1",
"avatar": "/static/fruit_avatar/Fruit-20.png"
},
"https_url": "https://git.coding.net/demo1/test1.git",
"web_url": "https://coding.net/u/demo1/p/test1",
"project_id": "99999999",
"ssh_url": "git@git.coding.net:demo1/test1.git",
"name": "test1",
"description": ""
},
"event": "merge_request"
}
`

View file

@ -1,246 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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 coding
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"github.com/woodpecker-ci/woodpecker/server/model"
)
const (
hookEvent = "X-Coding-Event"
hookPush = "push"
hookPR = "pull_request"
hookMR = "merge_request"
)
type User struct {
GlobalKey string `json:"global_key"`
Avatar string `json:"avatar"`
}
type Repository struct {
Name string `json:"name"`
HTTPSURL string `json:"https_url"`
SSHURL string `json:"ssh_url"`
WebURL string `json:"web_url"`
Owner *User `json:"owner"`
}
type Committer struct {
Email string `json:"email"`
Name string `json:"name"`
}
type Commit struct {
SHA string `json:"sha"`
ShortMessage string `json:"short_message"`
Committer *Committer `json:"committer"`
}
type PullRequest MergeRequest
type MergeRequest struct {
SourceBranch string `json:"source_branch"`
TargetBranch string `json:"target_branch"`
CommitSHA string `json:"merge_commit_sha"`
Status string `json:"status"`
Action string `json:"action"`
Number float64 `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
WebURL string `json:"web_url"`
User *User `json:"user"`
}
type PushHook struct {
Event string `json:"event"`
Repository *Repository `json:"repository"`
Ref string `json:"ref"`
Before string `json:"before"`
After string `json:"after"`
Commits []*Commit `json:"commits"`
User *User `json:"user"`
}
type PullRequestHook struct {
Event string `json:"event"`
Repository *Repository `json:"repository"`
PullRequest *PullRequest `json:"pull_request"`
}
type MergeRequestHook struct {
Event string `json:"event"`
Repository *Repository `json:"repository"`
MergeRequest *MergeRequest `json:"merge_request"`
}
func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) {
raw, err := io.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
return nil, nil, err
}
switch r.Header.Get(hookEvent) {
case hookPush:
return parsePushHook(raw)
case hookPR:
return parsePullRequestHook(raw)
case hookMR:
return parseMergeRequestHook(raw)
}
return nil, nil, nil
}
func findLastCommit(commits []*Commit, sha string) *Commit {
var lastCommit *Commit
for _, commit := range commits {
if commit.SHA == sha {
lastCommit = commit
break
}
}
if lastCommit == nil {
lastCommit = &Commit{}
}
if lastCommit.Committer == nil {
lastCommit.Committer = &Committer{}
}
return lastCommit
}
func convertRepository(repo *Repository) (*model.Repo, error) {
// tricky stuff for a team project without a team owner instead of a user owner
re := regexp.MustCompile(`git@.+:([^/]+)/.+\.git`)
matches := re.FindStringSubmatch(repo.SSHURL)
if len(matches) != 2 {
return nil, fmt.Errorf("Unable to resolve owner from ssh url %q", repo.SSHURL)
}
return &model.Repo{
// TODO ForgeID: repo.ID,
Owner: matches[1],
Name: repo.Name,
FullName: projectFullName(repo.Owner.GlobalKey, repo.Name),
Link: repo.WebURL,
Clone: repo.HTTPSURL,
SCMKind: model.RepoGit,
}, nil
}
func parsePushHook(raw []byte) (*model.Repo, *model.Pipeline, error) {
hook := &PushHook{}
err := json.Unmarshal(raw, hook)
if err != nil {
return nil, nil, err
}
// no pipeline triggered when removing ref
if hook.After == "0000000000000000000000000000000000000000" {
return nil, nil, nil
}
repo, err := convertRepository(hook.Repository)
if err != nil {
return nil, nil, err
}
lastCommit := findLastCommit(hook.Commits, hook.After)
pipeline := &model.Pipeline{
Event: model.EventPush,
Commit: hook.After,
Ref: hook.Ref,
Link: fmt.Sprintf("%s/git/commit/%s", hook.Repository.WebURL, hook.After),
Branch: strings.Replace(hook.Ref, "refs/heads/", "", -1),
Message: lastCommit.ShortMessage,
Email: lastCommit.Committer.Email,
Avatar: hook.User.Avatar,
Author: hook.User.GlobalKey,
CloneURL: hook.Repository.HTTPSURL,
}
return repo, pipeline, nil
}
func parsePullRequestHook(raw []byte) (*model.Repo, *model.Pipeline, error) {
hook := &PullRequestHook{}
err := json.Unmarshal(raw, hook)
if err != nil {
return nil, nil, err
}
if hook.PullRequest.Status != "CANMERGE" ||
(hook.PullRequest.Action != "create" && hook.PullRequest.Action != "synchronize") {
return nil, nil, nil
}
repo, err := convertRepository(hook.Repository)
if err != nil {
return nil, nil, err
}
pipeline := &model.Pipeline{
Event: model.EventPull,
Commit: hook.PullRequest.CommitSHA,
Link: hook.PullRequest.WebURL,
Ref: fmt.Sprintf("refs/pull/%d/MERGE", int(hook.PullRequest.Number)),
Branch: hook.PullRequest.TargetBranch,
Message: hook.PullRequest.Body,
Author: hook.PullRequest.User.GlobalKey,
Avatar: hook.PullRequest.User.Avatar,
Title: hook.PullRequest.Title,
CloneURL: hook.Repository.HTTPSURL,
Refspec: fmt.Sprintf("%s:%s", hook.PullRequest.SourceBranch, hook.PullRequest.TargetBranch),
}
return repo, pipeline, nil
}
func parseMergeRequestHook(raw []byte) (*model.Repo, *model.Pipeline, error) {
hook := &MergeRequestHook{}
err := json.Unmarshal(raw, hook)
if err != nil {
return nil, nil, err
}
if hook.MergeRequest.Status != "CANMERGE" ||
(hook.MergeRequest.Action != "create" && hook.MergeRequest.Action != "synchronize") {
return nil, nil, nil
}
repo, err := convertRepository(hook.Repository)
if err != nil {
return nil, nil, err
}
pipeline := &model.Pipeline{
Event: model.EventPull,
Commit: hook.MergeRequest.CommitSHA,
Link: hook.MergeRequest.WebURL,
Ref: fmt.Sprintf("refs/merge/%d/MERGE", int(hook.MergeRequest.Number)),
Branch: hook.MergeRequest.TargetBranch,
Message: hook.MergeRequest.Body,
Author: hook.MergeRequest.User.GlobalKey,
Avatar: hook.MergeRequest.User.Avatar,
Title: hook.MergeRequest.Title,
CloneURL: hook.Repository.HTTPSURL,
Refspec: fmt.Sprintf("%s:%s", hook.MergeRequest.SourceBranch, hook.MergeRequest.TargetBranch),
}
return repo, pipeline, nil
}

View file

@ -1,205 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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 coding
import (
"io"
"net/http"
"strings"
"testing"
"github.com/franela/goblin"
"github.com/woodpecker-ci/woodpecker/server/forge/coding/fixtures"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func Test_hook(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Coding hook", func() {
g.It("Should parse hook", func() {
reader := io.NopCloser(strings.NewReader(fixtures.PushHook))
r := &http.Request{
Header: map[string][]string{
hookEvent: {hookPush},
},
Body: reader,
}
repo := &model.Repo{
Owner: "demo1",
Name: "test1",
FullName: "demo1/test1",
Link: "https://coding.net/u/demo1/p/test1",
Clone: "https://git.coding.net/demo1/test1.git",
SCMKind: model.RepoGit,
}
pipeline := &model.Pipeline{
Event: model.EventPush,
Commit: "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
Ref: "refs/heads/master",
Link: "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
Branch: "master",
Message: "new file .woodpecker.yml\n",
Email: "demo1@gmail.com",
Avatar: "/static/fruit_avatar/Fruit-20.png",
Author: "demo1",
CloneURL: "https://git.coding.net/demo1/test1.git",
}
actualRepo, actualPipeline, err := parseHook(r)
g.Assert(err).IsNil()
g.Assert(actualRepo).Equal(repo)
g.Assert(actualPipeline).Equal(pipeline)
})
g.It("Should find last commit", func() {
commit1 := &Commit{SHA: "1234567890", Committer: &Committer{}}
commit2 := &Commit{SHA: "abcdef1234", Committer: &Committer{}}
commits := []*Commit{commit1, commit2}
g.Assert(findLastCommit(commits, "abcdef1234")).Equal(commit2)
})
g.It("Should find last commit", func() {
commit1 := &Commit{SHA: "1234567890", Committer: &Committer{}}
commit2 := &Commit{SHA: "abcdef1234", Committer: &Committer{}}
commits := []*Commit{commit1, commit2}
emptyCommit := &Commit{Committer: &Committer{}}
g.Assert(findLastCommit(commits, "00000000000")).Equal(emptyCommit)
})
g.It("Should convert repository", func() {
repository := &Repository{
Name: "test_project",
HTTPSURL: "https://git.coding.net/kelvin/test_project.git",
SSHURL: "git@git.coding.net:kelvin/test_project.git",
WebURL: "https://coding.net/u/kelvin/p/test_project",
Owner: &User{
GlobalKey: "kelvin",
Avatar: "https://dn-coding-net-production-static.qbox.me/9ed11de3-65e3-4cd8-b6aa-5abe7285ab43.jpeg?imageMogr2/auto-orient/format/jpeg/crop/!209x209a0a0",
},
}
repo := &model.Repo{
Owner: "kelvin",
Name: "test_project",
FullName: "kelvin/test_project",
Link: "https://coding.net/u/kelvin/p/test_project",
Clone: "https://git.coding.net/kelvin/test_project.git",
SCMKind: model.RepoGit,
}
actual, err := convertRepository(repository)
g.Assert(err).IsNil()
g.Assert(actual).Equal(repo)
})
g.It("Should parse push hook", func() {
repo := &model.Repo{
Owner: "demo1",
Name: "test1",
FullName: "demo1/test1",
Link: "https://coding.net/u/demo1/p/test1",
Clone: "https://git.coding.net/demo1/test1.git",
SCMKind: model.RepoGit,
}
pipeline := &model.Pipeline{
Event: model.EventPush,
Commit: "5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
Ref: "refs/heads/master",
Link: "https://coding.net/u/demo1/p/test1/git/commit/5b9912a6ff272e9c93a4c44c278fe9b359ed1ab4",
Branch: "master",
Message: "new file .woodpecker.yml\n",
Email: "demo1@gmail.com",
Avatar: "/static/fruit_avatar/Fruit-20.png",
Author: "demo1",
CloneURL: "https://git.coding.net/demo1/test1.git",
}
actualRepo, actualPipeline, err := parsePushHook([]byte(fixtures.PushHook))
g.Assert(err).IsNil()
g.Assert(actualRepo).Equal(repo)
g.Assert(actualPipeline).Equal(pipeline)
})
g.It("Should parse delete branch push hook", func() {
actualRepo, actualPipeline, err := parsePushHook([]byte(fixtures.DeleteBranchPushHook))
g.Assert(err).IsNil()
g.Assert(actualRepo).IsNil()
g.Assert(actualPipeline).IsNil()
})
g.It("Should parse pull request hook", func() {
repo := &model.Repo{
Owner: "demo1",
Name: "test2",
FullName: "demo1/test2",
Link: "https://coding.net/u/demo1/p/test2",
Clone: "https://git.coding.net/demo1/test2.git",
SCMKind: model.RepoGit,
}
pipeline := &model.Pipeline{
Event: model.EventPull,
Commit: "55e77b328b71d3ee4f9e70a5f67231b0acceeadc",
Link: "https://coding.net/u/demo1/p/test2/git/pull/1",
Ref: "refs/pull/1/MERGE",
Branch: "master",
Message: "pr message",
Author: "demo2",
Avatar: "/static/fruit_avatar/Fruit-2.png",
Title: "pr1",
CloneURL: "https://git.coding.net/demo1/test2.git",
Refspec: "master:master",
}
actualRepo, actualPipeline, err := parsePullRequestHook([]byte(fixtures.PullRequestHook))
g.Assert(err).IsNil()
g.Assert(actualRepo).Equal(repo)
g.Assert(actualPipeline).Equal(pipeline)
})
g.It("Should parse merge request hook", func() {
repo := &model.Repo{
Owner: "demo1",
Name: "test1",
FullName: "demo1/test1",
Link: "https://coding.net/u/demo1/p/test1",
Clone: "https://git.coding.net/demo1/test1.git",
SCMKind: model.RepoGit,
}
pipeline := &model.Pipeline{
Event: model.EventPull,
Commit: "74e6755580c34e9fd81dbcfcbd43ee5f30259436",
Link: "https://coding.net/u/demo1/p/test1/git/merge/1",
Ref: "refs/merge/1/MERGE",
Branch: "master",
Message: "<p>mr message</p>",
Author: "demo1",
Avatar: "/static/fruit_avatar/Fruit-20.png",
Title: "mr1",
CloneURL: "https://git.coding.net/demo1/test1.git",
Refspec: "branch1:master",
}
actualRepo, actualPipeline, err := parseMergeRequestHook([]byte(fixtures.MergeRequestHook))
g.Assert(err).IsNil()
g.Assert(actualRepo).Equal(repo)
g.Assert(actualPipeline).Equal(pipeline)
})
})
}

View file

@ -1,101 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 internal
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type Client struct {
baseURL string
apiPath string
token string
agent string
client *http.Client
ctx context.Context
}
type GenericAPIResponse struct {
Code int `json:"code"`
Data json.RawMessage `json:"data,omitempty"`
}
func NewClient(ctx context.Context, baseURL, apiPath, token, agent string, client *http.Client) *Client {
return &Client{
baseURL: baseURL,
apiPath: apiPath,
token: token,
agent: agent,
client: client,
ctx: ctx,
}
}
// Get Generic GET for requesting Coding OAuth API
func (c *Client) Get(u string, params url.Values) ([]byte, error) {
return c.Do(http.MethodGet, u, params)
}
// Do Generic method for requesting Coding OAuth API
func (c *Client) Do(method, u string, params url.Values) ([]byte, error) {
if params == nil {
params = url.Values{}
}
params.Set("access_token", c.token)
rawURL := c.baseURL + c.apiPath + u
var req *http.Request
var err error
if method != "GET" {
req, err = http.NewRequestWithContext(c.ctx, method, rawURL+"?access_token="+c.token, strings.NewReader(params.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
} else {
req, err = http.NewRequestWithContext(c.ctx, "GET", rawURL+"?"+params.Encode(), nil)
}
if err != nil {
return nil, fmt.Errorf("fail to create request for url %q: %w", rawURL, err)
}
req.Header.Set("User-Agent", c.agent)
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("fail to request %s %s: %w", req.Method, req.URL, err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s %s respond %d", req.Method, req.URL, resp.StatusCode)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("fail to read response from %s %s: %w", req.Method, req.URL.String(), err)
}
apiResp := &GenericAPIResponse{}
err = json.Unmarshal(body, apiResp)
if err != nil {
return nil, fmt.Errorf("fail to parse response from %s %s: %w", req.Method, req.URL.String(), err)
}
if apiResp.Code != 0 {
return nil, fmt.Errorf("Coding OAuth API respond error: %s", string(body))
}
return apiResp.Data, nil
}

View file

@ -1,29 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 internal
import (
"fmt"
)
type APIClientErr struct {
Message string
URL string
Cause error
}
func (e APIClientErr) Error() string {
return fmt.Sprintf("%s (Requested %s): %v", e.Message, e.URL, e.Cause)
}

View file

@ -1,45 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 internal
import (
"encoding/json"
"fmt"
)
type Commit struct {
File *File `json:"file"`
}
type File struct {
Data string `json:"data"`
}
func (c *Client) GetFile(globalKey, projectName, ref, path string) ([]byte, error) {
u := fmt.Sprintf("/user/%s/project/%s/git/blob/%s/%s", globalKey, projectName, ref, path)
resp, err := c.Get(u, nil)
if err != nil {
return nil, err
}
commit := &Commit{}
err = json.Unmarshal(resp, commit)
if err != nil {
return nil, APIClientErr{"fail to parse file data", u, err}
}
if commit == nil || commit.File == nil {
return nil, nil
}
return []byte(commit.File.Data), nil
}

View file

@ -1,107 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 internal
import (
"encoding/json"
"fmt"
"net/url"
)
type Project struct {
Owner string `json:"owner_user_name"`
Name string `json:"name"`
DepotPath string `json:"depot_path"`
HTTPSURL string `json:"https_url"`
IsPublic bool `json:"is_public"`
Icon string `json:"icon"`
Role string `json:"current_user_role"`
}
type Depot struct {
DefaultBranch string `json:"default_branch"`
}
type ProjectListData struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPage int `json:"totalPage"`
TotalRow int `json:"totalRow"`
List []*Project `json:"list"`
}
func (c *Client) GetProject(globalKey, projectName string) (*Project, error) {
u := fmt.Sprintf("/user/%s/project/%s", globalKey, projectName)
resp, err := c.Get(u, nil)
if err != nil {
return nil, err
}
project := &Project{}
err = json.Unmarshal(resp, project)
if err != nil {
return nil, APIClientErr{"fail to parse project data", u, err}
}
return project, nil
}
func (c *Client) GetDepot(globalKey, projectName string) (*Depot, error) {
u := fmt.Sprintf("/user/%s/project/%s/git", globalKey, projectName)
resp, err := c.Get(u, nil)
if err != nil {
return nil, err
}
depot := &Depot{}
err = json.Unmarshal(resp, depot)
if err != nil {
return nil, APIClientErr{"fail to parse depot data", u, err}
}
return depot, nil
}
func (c *Client) GetProjectList() ([]*Project, error) {
u := "/user/projects"
resp, err := c.Get(u, nil)
if err != nil {
return nil, err
}
data := &ProjectListData{}
err = json.Unmarshal(resp, data)
if err != nil {
return nil, APIClientErr{"fail to parse project list data", u, err}
}
if data.TotalPage == 1 {
return data.List, nil
}
projectList := make([]*Project, 0)
projectList = append(projectList, data.List...)
for i := 2; i <= data.TotalPage; i++ {
params := url.Values{}
params.Set("page", fmt.Sprintf("%d", i))
resp, err := c.Get(u, params)
if err != nil {
return nil, err
}
data := &ProjectListData{}
err = json.Unmarshal(resp, data)
if err != nil {
return nil, APIClientErr{"fail to parse project list data", u, err}
}
projectList = append(projectList, data.List...)
}
return projectList, nil
}

View file

@ -1,39 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 internal
import (
"encoding/json"
)
type User struct {
GlobalKey string `json:"global_key"`
Email string `json:"email"`
Avatar string `json:"avatar"`
}
func (c *Client) GetCurrentUser() (*User, error) {
u := "/account/current_user"
resp, err := c.Get(u, nil)
if err != nil {
return nil, err
}
user := &User{}
err = json.Unmarshal(resp, user)
if err != nil {
return nil, APIClientErr{"fail to parse current user data", u, err}
}
return user, nil
}

View file

@ -1,106 +0,0 @@
// Copyright 2018 Drone.IO Inc.
//
// 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 internal
import (
"encoding/json"
"fmt"
"net/url"
)
type Webhook struct {
ID int `json:"id"`
HookURL string `json:"hook_url"`
}
func (c *Client) GetWebhooks(globalKey, projectName string) ([]*Webhook, error) {
u := fmt.Sprintf("/user/%s/project/%s/git/hooks", globalKey, projectName)
resp, err := c.Get(u, nil)
if err != nil {
return nil, err
}
webhooks := make([]*Webhook, 0)
err = json.Unmarshal(resp, &webhooks)
if err != nil {
return nil, APIClientErr{"fail to parse webhooks data", u, err}
}
return webhooks, nil
}
func (c *Client) AddWebhook(globalKey, projectName, link string) error {
webhooks, err := c.GetWebhooks(globalKey, projectName)
if err != nil {
return err
}
webhook := matchingHooks(webhooks, link)
if webhook != nil {
u := fmt.Sprintf("/user/%s/project/%s/git/hook/%d", globalKey, projectName, webhook.ID)
params := url.Values{}
params.Set("hook_url", link)
params.Set("type_push", "true")
params.Set("type_mr_pr", "true")
_, err := c.Do("PUT", u, params)
if err != nil {
return APIClientErr{"fail to edit webhook", u, err}
}
return nil
}
u := fmt.Sprintf("/user/%s/project/%s/git/hook", globalKey, projectName)
params := url.Values{}
params.Set("hook_url", link)
params.Set("type_push", "true")
params.Set("type_mr_pr", "true")
_, err = c.Do("POST", u, params)
if err != nil {
return APIClientErr{"fail to add webhook", u, err}
}
return nil
}
func (c *Client) RemoveWebhook(globalKey, projectName, link string) error {
webhooks, err := c.GetWebhooks(globalKey, projectName)
if err != nil {
return err
}
webhook := matchingHooks(webhooks, link)
if webhook == nil {
return nil
}
u := fmt.Sprintf("/user/%s/project/%s/git/hook/%d", globalKey, projectName, webhook.ID)
_, err = c.Do("DELETE", u, nil)
if err != nil {
return APIClientErr{"fail to remove webhook", u, err}
}
return nil
}
// helper function to return matching hook.
func matchingHooks(hooks []*Webhook, rawurl string) *Webhook {
link, err := url.Parse(rawurl)
if err != nil {
return nil
}
for _, hook := range hooks {
hookurl, err := url.Parse(hook.HookURL)
if err == nil && hookurl.Host == link.Host {
return hook
}
}
return nil
}

View file

@ -1,24 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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 coding
import (
"fmt"
)
func projectFullName(owner, name string) string {
return fmt.Sprintf("%s/%s", owner, name)
}

View file

@ -1,31 +0,0 @@
// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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 coding
import (
"testing"
"github.com/franela/goblin"
)
func Test_util(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Coding util", func() {
g.It("Should form project full name", func() {
g.Assert(projectFullName("gk", "prj")).Equal("gk/prj")
})
})
}