Drop coding support (#1644)

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")
})
})
}