mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-26 11:51:02 +00:00
parent
874c2ea114
commit
570f5044e8
13 changed files with 10 additions and 1546 deletions
|
@ -421,52 +421,6 @@ var flags = []cli.Flag{
|
|||
Usage: "gitlab skip ssl verification",
|
||||
},
|
||||
//
|
||||
// Bitbucket Stash
|
||||
//
|
||||
&cli.BoolFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH"},
|
||||
Name: "stash",
|
||||
Usage: "stash driver is enabled",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_URL"},
|
||||
Name: "stash-server",
|
||||
Usage: "stash server address",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_CONSUMER_KEY"},
|
||||
Name: "stash-consumer-key",
|
||||
Usage: "stash oauth1 consumer key",
|
||||
FilePath: os.Getenv("WOODPECKER_STASH_CONSUMER_KEY_FILE"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_CONSUMER_RSA"},
|
||||
Name: "stash-consumer-rsa",
|
||||
Usage: "stash oauth1 private key file",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_CONSUMER_RSA_STRING"},
|
||||
Name: "stash-consumer-rsa-string",
|
||||
Usage: "stash oauth1 private key string",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_GIT_USERNAME"},
|
||||
Name: "stash-git-username",
|
||||
Usage: "stash service account username",
|
||||
FilePath: os.Getenv("WOODPECKER_STASH_GIT_USERNAME_FILE"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_GIT_PASSWORD"},
|
||||
Name: "stash-git-password",
|
||||
Usage: "stash service account password",
|
||||
FilePath: os.Getenv("WOODPECKER_STASH_GIT_PASSWORD_FILE"),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
EnvVars: []string{"WOODPECKER_STASH_SKIP_VERIFY"},
|
||||
Name: "stash-skip-verify",
|
||||
Usage: "stash skip ssl verification",
|
||||
},
|
||||
//
|
||||
// development flags
|
||||
//
|
||||
&cli.StringFlag{
|
||||
|
|
|
@ -38,7 +38,6 @@ import (
|
|||
"github.com/woodpecker-ci/woodpecker/server/cache"
|
||||
"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/gitea"
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge/github"
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge/gitlab"
|
||||
|
@ -193,8 +192,6 @@ func setupForge(c *cli.Context) (forge.Forge, error) {
|
|||
return setupGitLab(c)
|
||||
case c.Bool("bitbucket"):
|
||||
return setupBitbucket(c)
|
||||
case c.Bool("stash"):
|
||||
return setupStash(c)
|
||||
case c.Bool("gitea"):
|
||||
return setupGitea(c)
|
||||
default:
|
||||
|
@ -231,21 +228,6 @@ func setupGitea(c *cli.Context) (forge.Forge, error) {
|
|||
return gitea.New(opts)
|
||||
}
|
||||
|
||||
// setupStash helper function to setup the Stash forge from the CLI arguments.
|
||||
func setupStash(c *cli.Context) (forge.Forge, error) {
|
||||
opts := bitbucketserver.Opts{
|
||||
URL: c.String("stash-server"),
|
||||
Username: c.String("stash-git-username"),
|
||||
Password: c.String("stash-git-password"),
|
||||
ConsumerKey: c.String("stash-consumer-key"),
|
||||
ConsumerRSA: c.String("stash-consumer-rsa"),
|
||||
ConsumerRSAString: c.String("stash-consumer-rsa-string"),
|
||||
SkipVerify: c.Bool("stash-skip-verify"),
|
||||
}
|
||||
log.Trace().Msgf("Forge (bitbucketserver) opts: %#v", opts)
|
||||
return bitbucketserver.New(opts)
|
||||
}
|
||||
|
||||
// setupGitLab helper function to setup the GitLab forge from the CLI arguments.
|
||||
func setupGitLab(c *cli.Context) (forge.Forge, error) {
|
||||
return gitlab.New(gitlab.Opts{
|
||||
|
|
|
@ -536,10 +536,6 @@ See [Gitea configuration](forges/gitea/#configuration)
|
|||
|
||||
See [Bitbucket configuration](forges/bitbucket/#configuration)
|
||||
|
||||
### `WOODPECKER_STASH_...`
|
||||
|
||||
See [Bitbucket server configuration](forges/bitbucket_server/#configuration)
|
||||
|
||||
### `WOODPECKER_GITLAB_...`
|
||||
|
||||
See [Gitlab configuration](forges/gitlab/#configuration)
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
## Supported features
|
||||
|
||||
| Feature | [GitHub](github/) | [Gitea / Forgejo](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) |
|
||||
| --- | :---: | :---: | :---: | :---: | :---: |
|
||||
| Event: Push | :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: |
|
||||
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
|
||||
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: |
|
||||
| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: |
|
||||
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: |
|
||||
| Feature | [GitHub](github/) | [Gitea / Forgejo](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) |
|
||||
| --- | :---: | :---: | :---: | :---: |
|
||||
| Event: Push | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Event: Tag | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
|
||||
| Event: Pull-Request | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: |
|
||||
| [Multiple workflows](../../20-usage/25-workflows.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: |
|
||||
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: |
|
||||
|
||||
¹ for pull requests at least Gitea version 1.17 is required
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
# Bitbucket Server
|
||||
|
||||
Woodpecker comes with experimental support for Bitbucket Server, formerly known as Atlassian Stash. To enable Bitbucket Server you should configure the Woodpecker container using the following environment variables:
|
||||
|
||||
```diff
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
woodpecker-server:
|
||||
[...]
|
||||
environment:
|
||||
- [...]
|
||||
+ - WOODPECKER_STASH=true
|
||||
+ - WOODPECKER_STASH_GIT_USERNAME=foo
|
||||
+ - WOODPECKER_STASH_GIT_PASSWORD=bar
|
||||
+ - WOODPECKER_STASH_CONSUMER_KEY=95c0282573633eb25e82
|
||||
+ - WOODPECKER_STASH_CONSUMER_RSA=/etc/bitbucket/key.pem
|
||||
+ - WOODPECKER_STASH_URL=http://stash.mycompany.com
|
||||
volumes:
|
||||
+ - /path/to/key.pem:/path/to/key.pem
|
||||
|
||||
woodpecker-agent:
|
||||
[...]
|
||||
```
|
||||
|
||||
## Private Key File
|
||||
|
||||
The OAuth process in Bitbucket server requires a private and a public RSA certificate. This is how you create the private RSA certificate.
|
||||
|
||||
```nohighlight
|
||||
openssl genrsa -out /etc/bitbucket/key.pem 1024
|
||||
```
|
||||
|
||||
This stores the private RSA certificate in `key.pem`. The next command generates the public RSA certificate and stores it in `key.pub`.
|
||||
|
||||
```nohighlight
|
||||
openssl rsa -in /etc/bitbucket/key.pem -pubout >> /etc/bitbucket/key.pub
|
||||
```
|
||||
|
||||
Please note that the private key file can be mounted into your Woodpecker container at runtime or as an environment variable
|
||||
|
||||
Private key file mounted into your Woodpecker container at runtime as a volume.
|
||||
|
||||
```diff
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
woodpecker-server:
|
||||
[...]
|
||||
environment:
|
||||
- [...]
|
||||
- WOODPECKER_STASH=true
|
||||
- WOODPECKER_STASH_GIT_USERNAME=foo
|
||||
- WOODPECKER_STASH_GIT_PASSWORD=bar
|
||||
- WOODPECKER_STASH_CONSUMER_KEY=95c0282573633eb25e82
|
||||
+ - WOODPECKER_STASH_CONSUMER_RSA=/etc/bitbucket/key.pem
|
||||
- WOODPECKER_STASH_URL=http://stash.mycompany.com
|
||||
+ volumes:
|
||||
+ - /etc/bitbucket/key.pem:/etc/bitbucket/key.pem
|
||||
|
||||
woodpecker-agent:
|
||||
[...]
|
||||
```
|
||||
|
||||
Private key as environment variable
|
||||
|
||||
```diff
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
woodpecker-server:
|
||||
[...]
|
||||
environment:
|
||||
- [...]
|
||||
- WOODPECKER_STASH=true
|
||||
- WOODPECKER_STASH_GIT_USERNAME=foo
|
||||
- WOODPECKER_STASH_GIT_PASSWORD=bar
|
||||
- WOODPECKER_STASH_CONSUMER_KEY=95c0282573633eb25e82
|
||||
+ - WOODPECKER_STASH_CONSUMER_RSA_STRING=contentOfPemKeyAsString
|
||||
- WOODPECKER_STASH_URL=http://stash.mycompany.com
|
||||
|
||||
woodpecker-agent:
|
||||
[...]
|
||||
```
|
||||
|
||||
## Service Account
|
||||
|
||||
Woodpecker uses `git+https` to clone repositories, however, Bitbucket Server does not currently support cloning repositories with OAuth token. To work around this limitation, you must create a service account and provide the username and password to Woodpecker. This service account will be used to authenticate and clone private repositories.
|
||||
|
||||
## Registration
|
||||
|
||||
You must register your application with Bitbucket Server in order to generate a consumer key. Navigate to your account settings and choose Applications from the menu, and click Register new application. Now copy & paste the text value from `/etc/bitbucket/key.pub` into the `Public Key` in the incoming link part of the application registration.
|
||||
|
||||
Please use http://woodpecker.mycompany.com/authorize as the Authorization callback URL.
|
||||
|
||||
## 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_STASH`
|
||||
> Default: `false`
|
||||
|
||||
Enables the Bitbucket Server driver.
|
||||
|
||||
### `WOODPECKER_STASH_URL`
|
||||
> Default: empty
|
||||
|
||||
Configures the Bitbucket Server address.
|
||||
|
||||
### `WOODPECKER_STASH_CONSUMER_KEY`
|
||||
> Default: empty
|
||||
|
||||
Configures your Bitbucket Server consumer key.
|
||||
|
||||
### `WOODPECKER_STASH_CONSUMER_KEY_FILE`
|
||||
> Default: empty
|
||||
|
||||
Read the value for `WOODPECKER_STASH_CONSUMER_KEY` from the specified filepath
|
||||
|
||||
### `WOODPECKER_STASH_CONSUMER_RSA`
|
||||
> Default: empty
|
||||
|
||||
Configures the path to your Bitbucket Server private key file.
|
||||
|
||||
### `WOODPECKER_STASH_CONSUMER_RSA_STRING`
|
||||
> Default: empty
|
||||
|
||||
Configures your Bitbucket Server private key.
|
||||
|
||||
### `WOODPECKER_STASH_GIT_USERNAME`
|
||||
> Default: empty
|
||||
|
||||
This username is used to authenticate and clone all private repositories.
|
||||
|
||||
### `WOODPECKER_STASH_GIT_USERNAME_FILE`
|
||||
> Default: empty
|
||||
|
||||
Read the value for `WOODPECKER_STASH_GIT_USERNAME` from the specified filepath
|
||||
|
||||
### `WOODPECKER_STASH_GIT_PASSWORD`
|
||||
> Default: empty
|
||||
|
||||
The password is used to authenticate and clone all private repositories.
|
||||
|
||||
### `WOODPECKER_STASH_GIT_PASSWORD_FILE`
|
||||
> Default: empty
|
||||
|
||||
Read the value for `WOODPECKER_STASH_GIT_PASSWORD` from the specified filepath
|
||||
|
||||
### `WOODPECKER_STASH_SKIP_VERIFY`
|
||||
> Default: `false`
|
||||
|
||||
Configure if SSL verification should be skipped.
|
|
@ -16,7 +16,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`
|
||||
- Dropped support for [Coding](https://coding.net/) and [Gogs](https://gogs.io).
|
||||
- Dropped support for [Coding](https://coding.net/), [Gogs](https://gogs.io) and Bitbucket Server (Stash).
|
||||
- `/api/queue/resume` & `/api/queue/pause` endpoint methods were changed from `GET` to `POST`
|
||||
- rename `pipeline:` key in your workflow config to `steps:`
|
||||
- If you want to migrate old logs to the new format, watch the error messages on start. If there are none we are good to go, else you have to plan a migration that can take hours. Set `WOODPECKER_MIGRATIONS_ALLOW_LONG` to true and let it run.
|
||||
|
|
|
@ -1,293 +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 bitbucketserver
|
||||
|
||||
// WARNING! This is an work-in-progress patch and does not yet conform to the coding,
|
||||
// quality or security standards expected of this project. Please use with caution.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/mrjones/oauth"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge"
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/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 (
|
||||
requestTokenURL = "%s/plugins/servlet/oauth/request-token"
|
||||
authorizeTokenURL = "%s/plugins/servlet/oauth/authorize"
|
||||
accessTokenURL = "%s/plugins/servlet/oauth/access-token"
|
||||
)
|
||||
|
||||
// Opts defines configuration options.
|
||||
type Opts struct {
|
||||
URL string // Stash server url.
|
||||
Username string // Git machine account username.
|
||||
Password string // Git machine account password.
|
||||
ConsumerKey string // Oauth1 consumer key.
|
||||
ConsumerRSA string // Oauth1 consumer key file.
|
||||
ConsumerRSAString string
|
||||
SkipVerify bool // Skip ssl verification.
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
url string
|
||||
Username string
|
||||
Password string
|
||||
SkipVerify bool
|
||||
Consumer *oauth.Consumer
|
||||
}
|
||||
|
||||
// New returns a Forge implementation that integrates with Bitbucket Server,
|
||||
// the on-premise edition of Bitbucket Cloud, formerly known as Stash.
|
||||
func New(opts Opts) (forge.Forge, error) {
|
||||
config := &Config{
|
||||
url: opts.URL,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
SkipVerify: opts.SkipVerify,
|
||||
}
|
||||
|
||||
switch {
|
||||
case opts.Username == "":
|
||||
return nil, fmt.Errorf("Must have a git machine account username")
|
||||
case opts.Password == "":
|
||||
return nil, fmt.Errorf("Must have a git machine account password")
|
||||
case opts.ConsumerKey == "":
|
||||
return nil, fmt.Errorf("Must have a oauth1 consumer key")
|
||||
}
|
||||
|
||||
if opts.ConsumerRSA == "" && opts.ConsumerRSAString == "" {
|
||||
return nil, fmt.Errorf("must have CONSUMER_RSA_KEY set to the path of a oauth1 consumer key file or CONSUMER_RSA_KEY_STRING set to the value of a oauth1 consumer key")
|
||||
}
|
||||
|
||||
var keyFileBytes []byte
|
||||
if opts.ConsumerRSA != "" {
|
||||
var err error
|
||||
keyFileBytes, err = os.ReadFile(opts.ConsumerRSA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
keyFileBytes = []byte(opts.ConsumerRSAString)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(keyFileBytes)
|
||||
PrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Consumer = CreateConsumer(opts.URL, opts.ConsumerKey, PrivateKey)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Name returns the string name of this driver
|
||||
func (c *Config) Name() string {
|
||||
return "stash"
|
||||
}
|
||||
|
||||
// URL returns the root url of a configured forge
|
||||
func (c *Config) URL() string {
|
||||
return c.url
|
||||
}
|
||||
|
||||
func (c *Config) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) {
|
||||
requestToken, u, err := c.Consumer.GetRequestTokenAndUrl("oob")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := req.FormValue("oauth_verifier")
|
||||
if len(code) == 0 {
|
||||
http.Redirect(res, req, u, http.StatusSeeOther)
|
||||
return nil, nil
|
||||
}
|
||||
requestToken.Token = req.FormValue("oauth_token")
|
||||
accessToken, err := c.Consumer.AuthorizeToken(requestToken, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, accessToken.Token)
|
||||
|
||||
user, err := client.FindCurrentUser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertUser(user, accessToken), nil
|
||||
}
|
||||
|
||||
// Auth is not supported by the Stash driver.
|
||||
func (*Config) Auth(_ context.Context, _, _ string) (string, error) {
|
||||
return "", fmt.Errorf("Not Implemented")
|
||||
}
|
||||
|
||||
// Teams is not supported by the Stash driver.
|
||||
func (*Config) Teams(_ context.Context, _ *model.User) ([]*model.Team, error) {
|
||||
var teams []*model.Team
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
// TeamPerm is not supported by the Stash driver.
|
||||
func (*Config) TeamPerm(_ *model.User, _ string) (*model.Perm, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Config) Repo(ctx context.Context, u *model.User, _ model.ForgeRemoteID, owner, name string) (*model.Repo, error) {
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
|
||||
repo, err := client.FindRepo(owner, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perm, err := client.FindRepoPerms(repo.Project.Key, repo.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertRepo(repo, perm), nil
|
||||
}
|
||||
|
||||
func (c *Config) Repos(ctx context.Context, u *model.User) ([]*model.Repo, error) {
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
|
||||
repos, err := client.FindRepos()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var all []*model.Repo
|
||||
for _, repo := range repos {
|
||||
perm, err := client.FindRepoPerms(repo.Project.Key, repo.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, convertRepo(repo, perm))
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func (c *Config) File(ctx context.Context, u *model.User, r *model.Repo, p *model.Pipeline, f string) ([]byte, error) {
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
|
||||
|
||||
return client.FindFileForRepo(r.Owner, r.Name, f, p.Ref)
|
||||
}
|
||||
|
||||
func (c *Config) Dir(_ context.Context, _ *model.User, _ *model.Repo, _ *model.Pipeline, _ string) ([]*forge_types.FileMeta, error) {
|
||||
return nil, forge_types.ErrNotImplemented
|
||||
}
|
||||
|
||||
// Status is not supported by the bitbucketserver driver.
|
||||
func (c *Config) Status(ctx context.Context, user *model.User, repo *model.Repo, pipeline *model.Pipeline, _ *model.Workflow) error {
|
||||
status := internal.PipelineStatus{
|
||||
State: convertStatus(pipeline.Status),
|
||||
Desc: common.GetPipelineStatusDescription(pipeline.Status),
|
||||
Name: fmt.Sprintf("Woodpecker #%d - %s", pipeline.Number, pipeline.Branch),
|
||||
Key: "Woodpecker",
|
||||
URL: common.GetPipelineStatusLink(repo, pipeline, nil),
|
||||
}
|
||||
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, user.Token)
|
||||
|
||||
return client.CreateStatus(pipeline.Commit, &status)
|
||||
}
|
||||
|
||||
func (c *Config) Netrc(_ *model.User, r *model.Repo) (*model.Netrc, error) {
|
||||
host, err := common.ExtractHostFromCloneURL(r.Clone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Netrc{
|
||||
Login: c.Username,
|
||||
Password: c.Password,
|
||||
Machine: host,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Config) Activate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
|
||||
|
||||
return client.CreateHook(r.Owner, r.Name, link)
|
||||
}
|
||||
|
||||
// Branches returns the names of all branches for the named repository.
|
||||
func (c *Config) Branches(ctx context.Context, u *model.User, r *model.Repo, p *model.ListOptions) ([]string, error) {
|
||||
bitbucketBranches, err := internal.NewClientWithToken(ctx, c.url, c.Consumer, common.UserToken(ctx, r, u)).ListBranches(r.Owner, r.Name, p.Page, p.PerPage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branches := make([]string, 0)
|
||||
for _, branch := range bitbucketBranches {
|
||||
branches = append(branches, branch.Name)
|
||||
}
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
// BranchHead returns the sha of the head (latest commit) of the specified branch
|
||||
func (c *Config) BranchHead(_ context.Context, _ *model.User, _ *model.Repo, _ string) (string, error) {
|
||||
// TODO(1138): missing implementation
|
||||
return "", forge_types.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Config) PullRequests(_ context.Context, _ *model.User, _ *model.Repo, _ *model.ListOptions) ([]*model.PullRequest, error) {
|
||||
return nil, forge_types.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Config) Deactivate(ctx context.Context, u *model.User, r *model.Repo, link string) error {
|
||||
client := internal.NewClientWithToken(ctx, c.url, c.Consumer, u.Token)
|
||||
return client.DeleteHook(r.Owner, r.Name, link)
|
||||
}
|
||||
|
||||
func (c *Config) Hook(_ context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) {
|
||||
return parseHook(r, c.url)
|
||||
}
|
||||
|
||||
// OrgMembership returns if user is member of organization and if user
|
||||
// is admin/owner in this organization.
|
||||
func (c *Config) OrgMembership(_ context.Context, _ *model.User, _ string) (*model.OrgPerm, error) {
|
||||
// TODO: Not implemented currently
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func CreateConsumer(URL, ConsumerKey string, PrivateKey *rsa.PrivateKey) *oauth.Consumer {
|
||||
consumer := oauth.NewRSAConsumer(
|
||||
ConsumerKey,
|
||||
PrivateKey,
|
||||
oauth.ServiceProvider{
|
||||
RequestTokenUrl: fmt.Sprintf(requestTokenURL, URL),
|
||||
AuthorizeTokenUrl: fmt.Sprintf(authorizeTokenURL, URL),
|
||||
AccessTokenUrl: fmt.Sprintf(accessTokenURL, URL),
|
||||
HttpMethod: "POST",
|
||||
})
|
||||
consumer.HttpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
return consumer
|
||||
}
|
|
@ -1,138 +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 bitbucketserver
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mrjones/oauth"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
|
||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||
)
|
||||
|
||||
const (
|
||||
statusPending = "INPROGRESS"
|
||||
statusSuccess = "SUCCESSFUL"
|
||||
statusFailure = "FAILED"
|
||||
)
|
||||
|
||||
// convertStatus is a helper function used to convert a Woodpecker status to a
|
||||
// Bitbucket commit status.
|
||||
func convertStatus(status model.StatusValue) string {
|
||||
switch status {
|
||||
case model.StatusPending, model.StatusRunning:
|
||||
return statusPending
|
||||
case model.StatusSuccess:
|
||||
return statusSuccess
|
||||
default:
|
||||
return statusFailure
|
||||
}
|
||||
}
|
||||
|
||||
// convertRepo is a helper function used to convert a Bitbucket server repository
|
||||
// structure to the common Woodpecker repository structure.
|
||||
func convertRepo(from *internal.Repo, perm *model.Perm) *model.Repo {
|
||||
repo := model.Repo{
|
||||
ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)),
|
||||
Name: from.Slug,
|
||||
Owner: from.Project.Key,
|
||||
Branch: "master",
|
||||
SCMKind: model.RepoGit,
|
||||
IsSCMPrivate: true, // Since we have to use Netrc it has to always be private :/
|
||||
FullName: fmt.Sprintf("%s/%s", from.Project.Key, from.Slug),
|
||||
Perm: perm,
|
||||
}
|
||||
|
||||
for _, item := range from.Links.Clone {
|
||||
if item.Name == "http" {
|
||||
uri, err := url.Parse(item.Href)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
uri.User = nil
|
||||
repo.Clone = uri.String()
|
||||
}
|
||||
}
|
||||
for _, item := range from.Links.Self {
|
||||
if item.Href != "" {
|
||||
repo.Link = item.Href
|
||||
}
|
||||
}
|
||||
return &repo
|
||||
}
|
||||
|
||||
// convertPushHook is a helper function used to convert a Bitbucket push
|
||||
// hook to the Woodpecker pipeline struct holding commit information.
|
||||
func convertPushHook(hook *internal.PostHook, baseURL string) *model.Pipeline {
|
||||
branch := strings.TrimPrefix(
|
||||
strings.TrimPrefix(
|
||||
hook.RefChanges[0].RefID,
|
||||
"refs/heads/",
|
||||
),
|
||||
"refs/tags/",
|
||||
)
|
||||
|
||||
// Ensuring the author label is not longer then 40 for the label of the commit author (default size in the db)
|
||||
authorLabel := hook.Changesets.Values[0].ToCommit.Author.Name
|
||||
if len(authorLabel) > 40 {
|
||||
authorLabel = authorLabel[0:37] + "..."
|
||||
}
|
||||
|
||||
pipeline := &model.Pipeline{
|
||||
Commit: hook.RefChanges[0].ToHash, // TODO check for index value
|
||||
Branch: branch,
|
||||
Message: hook.Changesets.Values[0].ToCommit.Message, // TODO check for index Values
|
||||
Avatar: avatarLink(hook.Changesets.Values[0].ToCommit.Author.EmailAddress),
|
||||
Author: authorLabel,
|
||||
Email: hook.Changesets.Values[0].ToCommit.Author.EmailAddress,
|
||||
Timestamp: time.Now().UTC().Unix(),
|
||||
Ref: hook.RefChanges[0].RefID, // TODO check for index Values
|
||||
Link: fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s", baseURL, hook.Repository.Project.Key, hook.Repository.Slug, hook.RefChanges[0].ToHash),
|
||||
}
|
||||
if strings.HasPrefix(hook.RefChanges[0].RefID, "refs/tags/") {
|
||||
pipeline.Event = model.EventTag
|
||||
} else {
|
||||
pipeline.Event = model.EventPush
|
||||
}
|
||||
|
||||
return pipeline
|
||||
}
|
||||
|
||||
// convertUser is a helper function used to convert a Bitbucket user account
|
||||
// structure to the Woodpecker User structure.
|
||||
func convertUser(from *internal.User, token *oauth.AccessToken) *model.User {
|
||||
return &model.User{
|
||||
Login: from.Slug,
|
||||
Token: token.Token,
|
||||
Email: from.EmailAddress,
|
||||
Avatar: avatarLink(from.EmailAddress),
|
||||
ForgeRemoteID: model.ForgeRemoteID(fmt.Sprint(from.ID)),
|
||||
}
|
||||
}
|
||||
|
||||
func avatarLink(email string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(strings.ToLower(email)))
|
||||
emailHash := fmt.Sprintf("%v", hex.EncodeToString(hasher.Sum(nil)))
|
||||
avatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s.jpg", emailHash)
|
||||
return avatarURL
|
||||
}
|
|
@ -1,156 +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 bitbucketserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/franela/goblin"
|
||||
"github.com/mrjones/oauth"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
|
||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||
)
|
||||
|
||||
func Test_helper(t *testing.T) {
|
||||
g := goblin.Goblin(t)
|
||||
g.Describe("Bitbucket Server converter", func() {
|
||||
g.It("should convert repository", func() {
|
||||
from := &internal.Repo{
|
||||
Slug: "hello-world",
|
||||
}
|
||||
from.Project.Key = "octocat"
|
||||
|
||||
// var links [1]internal.LinkType
|
||||
link := internal.CloneLink{
|
||||
Name: "http",
|
||||
Href: "https://x7hw@server.org/foo/bar.git",
|
||||
}
|
||||
from.Links.Clone = append(from.Links.Clone, link)
|
||||
|
||||
selfRef := internal.SelfRefLink{
|
||||
Href: "https://server.org/foo/bar",
|
||||
}
|
||||
|
||||
from.Links.Self = append(from.Links.Self, selfRef)
|
||||
|
||||
to := convertRepo(from, &model.Perm{Pull: true})
|
||||
g.Assert(to.FullName).Equal("octocat/hello-world")
|
||||
g.Assert(to.Owner).Equal("octocat")
|
||||
g.Assert(to.Name).Equal("hello-world")
|
||||
g.Assert(to.Branch).Equal("master")
|
||||
g.Assert(to.SCMKind).Equal(model.RepoGit)
|
||||
g.Assert(to.IsSCMPrivate).Equal(true)
|
||||
g.Assert(to.Clone).Equal("https://server.org/foo/bar.git")
|
||||
g.Assert(to.Link).Equal("https://server.org/foo/bar")
|
||||
g.Assert(to.Perm.Pull).IsTrue()
|
||||
})
|
||||
|
||||
g.It("should convert user", func() {
|
||||
token := &oauth.AccessToken{
|
||||
Token: "foo",
|
||||
}
|
||||
user := &internal.User{
|
||||
Slug: "x12f",
|
||||
EmailAddress: "huh@huh.com",
|
||||
}
|
||||
|
||||
result := convertUser(user, token)
|
||||
g.Assert(result.Avatar).Equal(avatarLink("huh@huh.com"))
|
||||
g.Assert(result.Login).Equal("x12f")
|
||||
g.Assert(result.Token).Equal("foo")
|
||||
})
|
||||
|
||||
g.It("branch should be empty", func() {
|
||||
change := internal.PostHook{}
|
||||
change.RefChanges = append(change.RefChanges, internal.RefChange{
|
||||
RefID: "refs/heads/",
|
||||
ToHash: "73f9c44d",
|
||||
})
|
||||
|
||||
value := internal.Value{}
|
||||
value.ToCommit.Author.Name = "John Doe, Appleboy, Mary, Janet E. Dawson and Ann S. Palmer"
|
||||
value.ToCommit.Author.EmailAddress = "huh@huh.com"
|
||||
value.ToCommit.Message = "message"
|
||||
|
||||
change.Changesets.Values = append(change.Changesets.Values, value)
|
||||
|
||||
change.Repository.Project = internal.Project{
|
||||
Key: "octocat",
|
||||
}
|
||||
change.Repository.Slug = "hello-world"
|
||||
pipeline := convertPushHook(&change, "http://base.com")
|
||||
g.Assert(pipeline.Branch).Equal("")
|
||||
})
|
||||
|
||||
g.It("should convert push hook to pipeline", func() {
|
||||
change := internal.PostHook{}
|
||||
|
||||
change.RefChanges = append(change.RefChanges, internal.RefChange{
|
||||
RefID: "refs/heads/release/some-feature",
|
||||
ToHash: "73f9c44d",
|
||||
})
|
||||
|
||||
value := internal.Value{}
|
||||
value.ToCommit.Author.Name = "John Doe, Appleboy, Mary, Janet E. Dawson and Ann S. Palmer"
|
||||
value.ToCommit.Author.EmailAddress = "huh@huh.com"
|
||||
value.ToCommit.Message = "message"
|
||||
|
||||
change.Changesets.Values = append(change.Changesets.Values, value)
|
||||
|
||||
change.Repository.Project.Key = "octocat"
|
||||
change.Repository.Slug = "hello-world"
|
||||
|
||||
pipeline := convertPushHook(&change, "http://base.com")
|
||||
g.Assert(pipeline.Event).Equal(model.EventPush)
|
||||
// Ensuring the author label is not longer then 40
|
||||
g.Assert(pipeline.Author).Equal("John Doe, Appleboy, Mary, Janet E. Da...")
|
||||
g.Assert(pipeline.Avatar).Equal(avatarLink("huh@huh.com"))
|
||||
g.Assert(pipeline.Commit).Equal("73f9c44d")
|
||||
g.Assert(pipeline.Branch).Equal("release/some-feature")
|
||||
g.Assert(pipeline.Link).Equal("http://base.com/projects/octocat/repos/hello-world/commits/73f9c44d")
|
||||
g.Assert(pipeline.Ref).Equal("refs/heads/release/some-feature")
|
||||
g.Assert(pipeline.Message).Equal("message")
|
||||
})
|
||||
|
||||
g.It("should convert tag hook to pipeline", func() {
|
||||
change := internal.PostHook{}
|
||||
change.RefChanges = append(change.RefChanges, internal.RefChange{
|
||||
RefID: "refs/tags/v1",
|
||||
ToHash: "73f9c44d",
|
||||
})
|
||||
|
||||
value := internal.Value{}
|
||||
value.ToCommit.Author.Name = "John Doe"
|
||||
value.ToCommit.Author.EmailAddress = "huh@huh.com"
|
||||
value.ToCommit.Message = "message"
|
||||
|
||||
change.Changesets.Values = append(change.Changesets.Values, value)
|
||||
change.Repository.Project.Key = "octocat"
|
||||
change.Repository.Slug = "hello-world"
|
||||
|
||||
pipeline := convertPushHook(&change, "http://base.com")
|
||||
g.Assert(pipeline.Event).Equal(model.EventTag)
|
||||
g.Assert(pipeline.Author).Equal("John Doe")
|
||||
g.Assert(pipeline.Avatar).Equal(avatarLink("huh@huh.com"))
|
||||
g.Assert(pipeline.Commit).Equal("73f9c44d")
|
||||
g.Assert(pipeline.Branch).Equal("v1")
|
||||
g.Assert(pipeline.Link).Equal("http://base.com/projects/octocat/repos/hello-world/commits/73f9c44d")
|
||||
g.Assert(pipeline.Ref).Equal("refs/tags/v1")
|
||||
g.Assert(pipeline.Message).Equal("message")
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,473 +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 internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mrjones/oauth"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||
)
|
||||
|
||||
const (
|
||||
currentUserID = "%s/plugins/servlet/applinks/whoami"
|
||||
pathUser = "%s/rest/api/1.0/users/%s"
|
||||
pathRepo = "%s/rest/api/1.0/projects/%s/repos/%s"
|
||||
pathRepos = "%s/rest/api/1.0/repos?start=%s&limit=%s"
|
||||
pathHook = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s"
|
||||
pathSource = "%s/projects/%s/repos/%s/browse/%s?at=%s&raw"
|
||||
hookName = "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook"
|
||||
pathHookDetails = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s"
|
||||
pathHookEnabled = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled"
|
||||
pathHookSettings = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/settings"
|
||||
pathStatus = "%s/rest/build-status/1.0/commits/%s"
|
||||
pathBranches = "%s/rest/api/1.0/projects/%s/repos/%s/branches?limit=%d&start=%d"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *http.Client
|
||||
base string
|
||||
accessToken string
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewClientWithToken(ctx context.Context, url string, consumer *oauth.Consumer, AccessToken string) *Client {
|
||||
var token oauth.AccessToken
|
||||
token.Token = AccessToken
|
||||
client, err := consumer.MakeHttpClient(&token)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("")
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: client,
|
||||
base: url,
|
||||
accessToken: AccessToken,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) FindCurrentUser() (*User, error) {
|
||||
currentUserIDResponse, err := c.doGet(fmt.Sprintf(currentUserID, c.base))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer currentUserIDResponse.Body.Close()
|
||||
|
||||
bits, err := io.ReadAll(currentUserIDResponse.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
login := string(bits)
|
||||
|
||||
currentUserResponse, err := c.doGet(fmt.Sprintf(pathUser, c.base, login))
|
||||
if currentUserResponse != nil {
|
||||
defer currentUserResponse.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contents, err := io.ReadAll(currentUserResponse.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user User
|
||||
err = json.Unmarshal(contents, &user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindRepo(owner, name string) (*Repo, error) {
|
||||
urlString := fmt.Sprintf(pathRepo, c.base, owner, name)
|
||||
response, err := c.doGet(urlString)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
log.Err(err).Msg("")
|
||||
}
|
||||
contents, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := Repo{}
|
||||
err = json.Unmarshal(contents, &repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &repo, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindRepos() ([]*Repo, error) {
|
||||
return c.paginatedRepos(0)
|
||||
}
|
||||
|
||||
func (c *Client) FindRepoPerms(owner, repo string) (*model.Perm, error) {
|
||||
perms := new(model.Perm)
|
||||
// If you don't have access return none right away
|
||||
_, err := c.FindRepo(owner, repo)
|
||||
if err != nil {
|
||||
return perms, err
|
||||
}
|
||||
// Must have admin to be able to list hooks. If have access the enable perms
|
||||
resp, err := c.doGet(fmt.Sprintf(pathHook, c.base, owner, repo, hookName))
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if err == nil {
|
||||
perms.Push = true
|
||||
perms.Admin = true
|
||||
}
|
||||
perms.Pull = true
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindFileForRepo(owner, repo, fileName, ref string) ([]byte, error) {
|
||||
response, err := c.doGet(fmt.Sprintf(pathSource, c.base, owner, repo, fileName, ref))
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
log.Err(err).Msg("")
|
||||
}
|
||||
if response.StatusCode == 404 {
|
||||
return nil, nil
|
||||
}
|
||||
responseBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("")
|
||||
}
|
||||
return responseBytes, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateHook(owner, name, callBackLink string) error {
|
||||
hookDetails, err := c.GetHookDetails(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var hooks []string
|
||||
if hookDetails.Enabled {
|
||||
hookSettings, err := c.GetHooks(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hooks = hookSettingsToArray(hookSettings)
|
||||
}
|
||||
if !stringInSlice(callBackLink, hooks) {
|
||||
hooks = append(hooks, callBackLink)
|
||||
}
|
||||
|
||||
putHookSettings := arrayToHookSettings(hooks)
|
||||
hookBytes, err := json.Marshal(putHookSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes)
|
||||
}
|
||||
|
||||
func (c *Client) CreateStatus(revision string, status *PipelineStatus) error {
|
||||
uri := fmt.Sprintf(pathStatus, c.base, revision)
|
||||
return c.doPost(uri, status)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteHook(owner, name, link string) error {
|
||||
hookSettings, err := c.GetHooks(owner, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
putHooks := filter(hookSettingsToArray(hookSettings), func(item string) bool {
|
||||
return !strings.Contains(item, link)
|
||||
})
|
||||
putHookSettings := arrayToHookSettings(putHooks)
|
||||
hookBytes, err := json.Marshal(putHookSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes)
|
||||
}
|
||||
|
||||
func (c *Client) GetHookDetails(owner, name string) (*HookPluginDetails, error) {
|
||||
urlString := fmt.Sprintf(pathHookDetails, c.base, owner, name, hookName)
|
||||
response, err := c.doGet(urlString)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hookDetails := HookPluginDetails{}
|
||||
err = json.NewDecoder(response.Body).Decode(&hookDetails)
|
||||
return &hookDetails, err
|
||||
}
|
||||
|
||||
func (c *Client) GetHooks(owner, name string) (*HookSettings, error) {
|
||||
urlString := fmt.Sprintf(pathHookSettings, c.base, owner, name, hookName)
|
||||
response, err := c.doGet(urlString)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hookSettings := HookSettings{}
|
||||
err = json.NewDecoder(response.Body).Decode(&hookSettings)
|
||||
return &hookSettings, err
|
||||
}
|
||||
|
||||
// TODO: make these as as general do with the action
|
||||
|
||||
// Helper function to help create get
|
||||
func (c *Client) doGet(url string) (*http.Response, error) {
|
||||
request, err := http.NewRequestWithContext(c.ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
return c.client.Do(request)
|
||||
}
|
||||
|
||||
// Helper function to help create the hook
|
||||
func (c *Client) doPut(url string, body []byte) error {
|
||||
request, err := http.NewRequestWithContext(c.ctx, "PUT", url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to help create the hook
|
||||
func (c *Client) doPost(url string, status *PipelineStatus) error {
|
||||
// write it to the body of the request.
|
||||
var buf io.ReadWriter
|
||||
if status != nil {
|
||||
buf = new(bytes.Buffer)
|
||||
err := json.NewEncoder(buf).Encode(status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
request, err := http.NewRequestWithContext(c.ctx, "POST", url, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
response, err := c.client.Do(request)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Helper function to get repos paginated
|
||||
func (c *Client) paginatedRepos(start int) ([]*Repo, error) {
|
||||
limit := 1000
|
||||
requestURL := fmt.Sprintf(pathRepos, c.base, strconv.Itoa(start), strconv.Itoa(limit))
|
||||
response, err := c.doGet(requestURL)
|
||||
if response != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var repoResponse Repos
|
||||
err = json.NewDecoder(response.Body).Decode(&repoResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !repoResponse.IsLastPage {
|
||||
reposList, err := c.paginatedRepos(start + limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoResponse.Values = append(repoResponse.Values, reposList...)
|
||||
}
|
||||
return repoResponse.Values, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListBranches(owner, name string, page, limit int) ([]*Branch, error) {
|
||||
uri := fmt.Sprintf(pathBranches, c.base, owner, name, limit, limit*(page-1))
|
||||
response, err := c.doGet(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
out := new(BranchResp)
|
||||
err = json.NewDecoder(response.Body).Decode(&out)
|
||||
return out.Values, err
|
||||
}
|
||||
|
||||
func filter(vs []string, f func(string) bool) []string {
|
||||
var vsf []string
|
||||
for _, v := range vs {
|
||||
if f(v) {
|
||||
vsf = append(vsf, v)
|
||||
}
|
||||
}
|
||||
return vsf
|
||||
}
|
||||
|
||||
// TODO: find a clean way of doing these next two methods- bitbucket server hooks only support 20 cb hooks
|
||||
func arrayToHookSettings(hooks []string) HookSettings {
|
||||
hookSettings := HookSettings{}
|
||||
for loc, value := range hooks {
|
||||
switch loc {
|
||||
case 0:
|
||||
hookSettings.HookURL0 = value
|
||||
case 1:
|
||||
hookSettings.HookURL1 = value
|
||||
case 2:
|
||||
hookSettings.HookURL2 = value
|
||||
case 3:
|
||||
hookSettings.HookURL3 = value
|
||||
case 4:
|
||||
hookSettings.HookURL4 = value
|
||||
case 5:
|
||||
hookSettings.HookURL5 = value
|
||||
case 6:
|
||||
hookSettings.HookURL6 = value
|
||||
case 7:
|
||||
hookSettings.HookURL7 = value
|
||||
case 8:
|
||||
hookSettings.HookURL8 = value
|
||||
case 9:
|
||||
hookSettings.HookURL9 = value
|
||||
case 10:
|
||||
hookSettings.HookURL10 = value
|
||||
case 11:
|
||||
hookSettings.HookURL11 = value
|
||||
case 12:
|
||||
hookSettings.HookURL12 = value
|
||||
case 13:
|
||||
hookSettings.HookURL13 = value
|
||||
case 14:
|
||||
hookSettings.HookURL14 = value
|
||||
case 15:
|
||||
hookSettings.HookURL15 = value
|
||||
case 16:
|
||||
hookSettings.HookURL16 = value
|
||||
case 17:
|
||||
hookSettings.HookURL17 = value
|
||||
case 18:
|
||||
hookSettings.HookURL18 = value
|
||||
case 19:
|
||||
hookSettings.HookURL19 = value
|
||||
|
||||
// Since there's only 19 hooks it will add to the latest if it doesn't exist :/
|
||||
default:
|
||||
hookSettings.HookURL19 = value
|
||||
}
|
||||
}
|
||||
return hookSettings
|
||||
}
|
||||
|
||||
func hookSettingsToArray(hookSettings *HookSettings) []string {
|
||||
var hooks []string
|
||||
|
||||
if hookSettings.HookURL0 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL0)
|
||||
}
|
||||
if hookSettings.HookURL1 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL1)
|
||||
}
|
||||
if hookSettings.HookURL2 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL2)
|
||||
}
|
||||
if hookSettings.HookURL3 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL3)
|
||||
}
|
||||
if hookSettings.HookURL4 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL4)
|
||||
}
|
||||
if hookSettings.HookURL5 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL5)
|
||||
}
|
||||
if hookSettings.HookURL6 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL6)
|
||||
}
|
||||
if hookSettings.HookURL7 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL7)
|
||||
}
|
||||
if hookSettings.HookURL8 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL8)
|
||||
}
|
||||
if hookSettings.HookURL9 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL9)
|
||||
}
|
||||
if hookSettings.HookURL10 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL10)
|
||||
}
|
||||
if hookSettings.HookURL11 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL11)
|
||||
}
|
||||
if hookSettings.HookURL12 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL12)
|
||||
}
|
||||
if hookSettings.HookURL13 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL13)
|
||||
}
|
||||
if hookSettings.HookURL14 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL14)
|
||||
}
|
||||
if hookSettings.HookURL15 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL15)
|
||||
}
|
||||
if hookSettings.HookURL16 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL16)
|
||||
}
|
||||
if hookSettings.HookURL17 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL17)
|
||||
}
|
||||
if hookSettings.HookURL18 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL18)
|
||||
}
|
||||
if hookSettings.HookURL19 != "" {
|
||||
hooks = append(hooks, hookSettings.HookURL19)
|
||||
}
|
||||
return hooks
|
||||
}
|
||||
|
||||
func stringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,215 +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 internal
|
||||
|
||||
type User struct {
|
||||
Active bool `json:"active"`
|
||||
DisplayName string `json:"displayName"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
ID int `json:"id"`
|
||||
Links struct {
|
||||
Self []struct {
|
||||
Href string `json:"href"`
|
||||
} `json:"self"`
|
||||
} `json:"links"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type CloneLink struct {
|
||||
Href string `json:"href"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SelfRefLink struct {
|
||||
Href string `json:"href"`
|
||||
}
|
||||
|
||||
type PipelineStatus struct {
|
||||
State string `json:"state"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Desc string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
Forkable bool `json:"forkable"`
|
||||
ID int `json:"id"`
|
||||
Links struct {
|
||||
Clone []CloneLink `json:"clone"`
|
||||
Self []struct {
|
||||
Href string `json:"href"`
|
||||
} `json:"self"`
|
||||
} `json:"links"`
|
||||
Name string `json:"name"`
|
||||
Project Project `json:"project"`
|
||||
Public bool `json:"public"`
|
||||
ScmID string `json:"scmId"`
|
||||
Slug string `json:"slug"`
|
||||
State string `json:"state"`
|
||||
StatusMessage string `json:"statusMessage"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Description string `json:"description"`
|
||||
ID int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Links struct {
|
||||
Self []SelfRefLink `json:"self"`
|
||||
} `json:"links"`
|
||||
Name string `json:"name"`
|
||||
Public bool `json:"public"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Repos struct {
|
||||
IsLastPage bool `json:"isLastPage"`
|
||||
Limit int `json:"limit"`
|
||||
Size int `json:"size"`
|
||||
Start int `json:"start"`
|
||||
Values []*Repo `json:"values"`
|
||||
}
|
||||
|
||||
type Hook struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Details *HookDetail `json:"details"`
|
||||
}
|
||||
|
||||
type HookDetail struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
ConfigFormKey string `json:"configFormKey"`
|
||||
}
|
||||
|
||||
type Value struct {
|
||||
Changes struct {
|
||||
Filter interface{} `json:"filter"`
|
||||
IsLastPage bool `json:"isLastPage"`
|
||||
Limit int `json:"limit"`
|
||||
Size int `json:"size"`
|
||||
Start int `json:"start"`
|
||||
Values []struct {
|
||||
ContentID string `json:"contentId"`
|
||||
Executable bool `json:"executable"`
|
||||
Link struct {
|
||||
Rel string `json:"rel"`
|
||||
URL string `json:"url"`
|
||||
} `json:"link"`
|
||||
NodeType string `json:"nodeType"`
|
||||
Path struct {
|
||||
Components []string `json:"components"`
|
||||
Extension string `json:"extension"`
|
||||
Name string `json:"name"`
|
||||
Parent string `json:"parent"`
|
||||
ToString string `json:"toString"`
|
||||
} `json:"path"`
|
||||
PercentUnchanged int `json:"percentUnchanged"`
|
||||
SrcExecutable bool `json:"srcExecutable"`
|
||||
Type string `json:"type"`
|
||||
} `json:"values"`
|
||||
} `json:"changes"`
|
||||
FromCommit struct {
|
||||
DisplayID string `json:"displayId"`
|
||||
ID string `json:"id"`
|
||||
} `json:"fromCommit"`
|
||||
Link struct {
|
||||
Rel string `json:"rel"`
|
||||
URL string `json:"url"`
|
||||
} `json:"link"`
|
||||
ToCommit struct {
|
||||
Author struct {
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
Name string `json:"name"`
|
||||
} `json:"author"`
|
||||
AuthorTimestamp int `json:"authorTimestamp"`
|
||||
DisplayID string `json:"displayId"`
|
||||
ID string `json:"id"`
|
||||
Message string `json:"message"`
|
||||
Parents []struct {
|
||||
DisplayID string `json:"displayId"`
|
||||
ID string `json:"id"`
|
||||
} `json:"parents"`
|
||||
} `json:"toCommit"`
|
||||
}
|
||||
|
||||
type PostHook struct {
|
||||
Changesets struct {
|
||||
Filter interface{} `json:"filter"`
|
||||
IsLastPage bool `json:"isLastPage"`
|
||||
Limit int `json:"limit"`
|
||||
Size int `json:"size"`
|
||||
Start int `json:"start"`
|
||||
Values []Value `json:"values"`
|
||||
} `json:"changesets"`
|
||||
RefChanges []RefChange `json:"refChanges"`
|
||||
Repository Repo `json:"repository"`
|
||||
}
|
||||
|
||||
type RefChange struct {
|
||||
FromHash string `json:"fromHash"`
|
||||
RefID string `json:"refId"`
|
||||
ToHash string `json:"toHash"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type HookPluginDetails struct {
|
||||
Details struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
ConfigFormKey string `json:"configFormKey"`
|
||||
} `json:"details"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Configured bool `json:"configured"`
|
||||
}
|
||||
|
||||
type HookSettings struct {
|
||||
HookURL0 string `json:"hook-url-0,omitempty"`
|
||||
HookURL1 string `json:"hook-url-1,omitempty"`
|
||||
HookURL2 string `json:"hook-url-2,omitempty"`
|
||||
HookURL3 string `json:"hook-url-3,omitempty"`
|
||||
HookURL4 string `json:"hook-url-4,omitempty"`
|
||||
HookURL5 string `json:"hook-url-5,omitempty"`
|
||||
HookURL6 string `json:"hook-url-6,omitempty"`
|
||||
HookURL7 string `json:"hook-url-7,omitempty"`
|
||||
HookURL8 string `json:"hook-url-8,omitempty"`
|
||||
HookURL9 string `json:"hook-url-9,omitempty"`
|
||||
HookURL10 string `json:"hook-url-10,omitempty"`
|
||||
HookURL11 string `json:"hook-url-11,omitempty"`
|
||||
HookURL12 string `json:"hook-url-12,omitempty"`
|
||||
HookURL13 string `json:"hook-url-13,omitempty"`
|
||||
HookURL14 string `json:"hook-url-14,omitempty"`
|
||||
HookURL15 string `json:"hook-url-15,omitempty"`
|
||||
HookURL16 string `json:"hook-url-16,omitempty"`
|
||||
HookURL17 string `json:"hook-url-17,omitempty"`
|
||||
HookURL18 string `json:"hook-url-18,omitempty"`
|
||||
HookURL19 string `json:"hook-url-19,omitempty"`
|
||||
}
|
||||
|
||||
type BranchResp struct {
|
||||
Values []*Branch `json:"values"`
|
||||
}
|
||||
|
||||
type Branch struct {
|
||||
Name string `json:"displayId"`
|
||||
}
|
|
@ -1,37 +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 bitbucketserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/woodpecker-ci/woodpecker/server/forge/bitbucketserver/internal"
|
||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||
)
|
||||
|
||||
// parseHook parses a Bitbucket hook from an http.Request request and returns
|
||||
// Repo and Pipeline detail. TODO: find a way to support PR hooks
|
||||
func parseHook(r *http.Request, baseURL string) (*model.Repo, *model.Pipeline, error) {
|
||||
hook := new(internal.PostHook)
|
||||
if err := json.NewDecoder(r.Body).Decode(hook); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pipeline := convertPushHook(hook, baseURL)
|
||||
repo := convertRepo(&hook.Repository, &model.Perm{})
|
||||
|
||||
return repo, pipeline, nil
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
<Icon v-if="forge === 'github'" name="github" />
|
||||
<Icon v-else-if="forge === 'gitea'" name="gitea" />
|
||||
<Icon v-else-if="forge === 'gitlab'" name="gitlab" />
|
||||
<Icon v-else-if="forge === 'bitbucket' || forge === 'stash'" name="bitbucket" />
|
||||
<Icon v-else-if="forge === 'bitbucket'" name="bitbucket" />
|
||||
<Icon v-else name="repo" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
|
|
Loading…
Reference in a new issue