Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for Forgejo #1543

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 63 additions & 0 deletions .woodpecker/forgejo.yml
@@ -0,0 +1,63 @@
variables:
- &golang_image 'golang:1.19'
- &when_path
- ".woodpecker/forgejo.yml"
- "server/forge/forgejo/**"
- "cmd/server/flags.go"
- "cmd/server/setup.go"
- "contrib/woodpecker-test-repo/*"

pipeline:
forgejo:
detach: true
image: codeberg.org/forgejo/forgejo:1.18
pull: true
ports: ["80"]
environment:
- FORGEJO__security__INSTALL_LOCK=true
- FORGEJO__server__HTTP_PORT=80
- FORGEJO__server__ROOT_URL=http://forgejo/
- FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true
commands: |
/usr/bin/entrypoint &
sleep 5
su git -c 'gitea admin user create --admin --username root --password admin1234 --email root@example.com'
cd contrib/woodpecker-test-repo
git init
git checkout -b main
git config user.email root@example.com
git config user.name username
git remote add origin http':'//root':'admin1234@forgejo/root/forgejo-test.git
git add .
git commit -m "Initial commit"
git push -u origin main
git rev-parse HEAD > /woodpecker/commit
su git -c 'gitea admin user generate-access-token -u root --raw' > /woodpecker/token-admin
( echo -n 'Authorization: token ' ; cat /woodpecker/token-admin ) > /woodpecker/header
( echo "/bin/sh" ; echo 'curl -sS -H "Content-Type: application/json" -H @/woodpecker/header "$@"' ) > /woodpecker/api && chmod +x /woodpecker/api
su git -c 'gitea admin user create --must-change-password=false --username normaluser --password admin1234 --email normaluser@example.com'
su git -c 'gitea admin user generate-access-token -u normaluser --raw' > /woodpecker/token-user
wait
when:
path: *when_path

forgejo-wait:
image: *golang_image
commands:
- for i in $(seq 60) ; do test -f /woodpecker/token-admin && break ; sleep 1 ; done
- test -f /woodpecker/token-admin
when:
path: *when_path

test:
image: *golang_image
group: test
commands: |
apt-get update --quiet && apt-get install -y -qq jq
/woodpecker/api -X POST --data-binary '{"username": "myorg"}' http://forgejo/api/v1/orgs
owners=$(/woodpecker/api http://forgejo/api/v1/orgs/myorg/teams | jq '.[0].id')
/woodpecker/api -X PUT http://forgejo/api/v1/teams/$owners/members/normaluser
myteam=$(/woodpecker/api -X POST --data-binary '{"name": "myteam", "units": ["repo.code"], "permission": "read"}' http://forgejo/api/v1/orgs/myorg/teams | jq .id)
make FORGEJO_COMMIT=$(cat /woodpecker/commit) FORGEJO_TOKEN_ADMIN=$(cat /woodpecker/token-admin) FORGEJO_TOKEN_USER=$(cat /woodpecker/token-user) FORGEJO_URL=http://forgejo/ test-server-forgejo
when:
path: *when_path
3 changes: 3 additions & 0 deletions Makefile
Expand Up @@ -132,6 +132,9 @@ test-agent: ## Test agent code
test-server: ## Test server code
go test -race -cover -coverprofile server-coverage.out -timeout 30s github.com/woodpecker-ci/woodpecker/cmd/server $(shell go list github.com/woodpecker-ci/woodpecker/server/... | grep -v '/store')

test-server-forgejo: ## Test only Forgejo server code
go test -v -race -cover -coverprofile server-coverage.out -timeout 120s $(shell go list github.com/woodpecker-ci/woodpecker/server/... | grep '/forgejo')

test-cli: ## Test cli code
go test -race -cover -coverprofile cli-coverage.out -timeout 30s github.com/woodpecker-ci/woodpecker/cmd/cli github.com/woodpecker-ci/woodpecker/cli/...

Expand Down
36 changes: 36 additions & 0 deletions cmd/server/flags.go
Expand Up @@ -313,6 +313,42 @@ var flags = []cli.Flag{
Usage: "gogs skip ssl verification",
},
//
// Forgejo
//
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_FORGEJO"},
Name: "forgejo",
Usage: "forgejo driver is enabled",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_FORGEJO_URL"},
Name: "forgejo-server",
Usage: "forgejo server address",
Value: "https://codeberg.org",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_FORGEJO_CLIENT"},
Name: "forgejo-client",
Usage: "forgejo oauth2 client id",
FilePath: os.Getenv("WOODPECKER_FORGEJO_CLIENT_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_FORGEJO_SECRET"},
Name: "forgejo-secret",
Usage: "forgejo oauth2 client secret",
FilePath: os.Getenv("WOODPECKER_FORGEJO_SECRET_FILE"),
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_FORGEJO_SKIP_VERIFY"},
Name: "forgejo-skip-verify",
Usage: "forgejo skip ssl verification",
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_FORGEJO_DEBUG"},
Name: "forgejo-debug",
Usage: "forgejo debug messages",
},
//
// Gitea
//
&cli.BoolFlag{
Expand Down
23 changes: 23 additions & 0 deletions cmd/server/setup.go
Expand Up @@ -40,6 +40,7 @@ import (
"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/forgejo"
"github.com/woodpecker-ci/woodpecker/server/forge/gitea"
"github.com/woodpecker-ci/woodpecker/server/forge/github"
"github.com/woodpecker-ci/woodpecker/server/forge/gitlab"
Expand Down Expand Up @@ -199,6 +200,8 @@ func setupForge(c *cli.Context) (forge.Forge, error) {
return setupStash(c)
case c.Bool("gogs"):
return setupGogs(c)
case c.Bool("forgejo"):
return setupForgejo(c)
case c.Bool("gitea"):
return setupGitea(c)
case c.Bool("coding"):
Expand Down Expand Up @@ -231,6 +234,26 @@ func setupGogs(c *cli.Context) (forge.Forge, error) {
return gogs.New(opts)
}

// helper function to setup the Forgejo forge from the CLI arguments.
func setupForgejo(c *cli.Context) (forge.Forge, error) {
server, err := url.Parse(c.String("forgejo-server"))
if err != nil {
return nil, err
}
opts := forgejo.Opts{
URL: strings.TrimRight(server.String(), "/"),
Client: c.String("forgejo-client"),
Secret: c.String("forgejo-secret"),
SkipVerify: c.Bool("forgejo-skip-verify"),
Debug: c.Bool("forgejo-debug"),
}
if len(opts.URL) == 0 {
log.Fatal().Msg("WOODPECKER_FORGEJO_URL must be set")
}
log.Trace().Msgf("Forge (forgejo) opts: %#v", opts)
return forgejo.New(opts)
}

// helper function to setup the Gitea forge from the CLI arguments.
func setupGitea(c *cli.Context) (forge.Forge, error) {
server, err := url.Parse(c.String("gitea-server"))
Expand Down
1 change: 1 addition & 0 deletions contrib/woodpecker-test-repo/README
@@ -0,0 +1 @@
SOMETHING
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
4 changes: 4 additions & 0 deletions docs/docs/30-administration/10-server-config.md
Expand Up @@ -387,6 +387,10 @@ See [GitHub configuration](forges/github/#configuration)

See [Gogs configuration](forges/gogs/#configuration)

### `WOODPECKER_FORGEJO_...`

See [Forgejo configuration](forges/forgejo/#configuration)

### `WOODPECKER_GITEA_...`

See [Gitea configuration](forges/gitea/#configuration)
Expand Down
16 changes: 8 additions & 8 deletions docs/docs/30-administration/11-forges/10-overview.md
Expand Up @@ -2,14 +2,14 @@

## Supported features

| Feature | [GitHub](github/) | [Gitea](gitea/) | [Gitlab](gitlab/) | [Bitbucket](bitbucket/) | [Bitbucket Server](bitbucket_server/) | [Gogs](gogs/) | [Coding](coding/) |
| Feature | [GitHub](github/) | [Forgejo](forgejo/) | [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: |
| [Multi pipeline](../../20-usage/25-multi-pipeline.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: |
| Event: Push | :white_check_mark: | :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: | :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: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: |
| Event: Deploy | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | :x: |
| OAuth | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [Multi pipeline](../../20-usage/25-multi-pipeline.md) | :white_check_mark: | :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:¹ | :white_check_mark: | :x: | :x: | :x: | :x: |

¹) for Gitea versions 1.17 or lower not for pull requests
75 changes: 75 additions & 0 deletions docs/docs/30-administration/11-forges/25-forgejo.md
@@ -0,0 +1,75 @@
# Forgejo

Woodpecker comes with built-in support for Forgejo. To enable Forgejo you should configure the Woodpecker container using the following environment variables:

```diff
# docker-compose.yml
version: '3'

services:
woodpecker-server:
[...]
environment:
- [...]
+ - WOODPECKER_FORGEJO=true
+ - WOODPECKER_FORGEJO_URL=${WOODPECKER_FORGEJO_URL}
+ - WOODPECKER_FORGEJO_CLIENT=${WOODPECKER_FORGEJO_CLIENT}
+ - WOODPECKER_FORGEJO_SECRET=${WOODPECKER_FORGEJO_SECRET}

woodpecker-agent:
[...]
```

## Registration

Register your application with Forgejo to create your client id and secret. You can find the OAuth applications settings of Forgejo at `https://forgejo.<host>/user/settings/`. It is very import the authorization callback URL matches your http(s) scheme and hostname exactly with `https://<host>/authorize` as the path.

If you run the Woodpecker CI server on the same host as the Forgejo instance, you might also need to allow local connections in Forgejo, since version `v1.16`. Otherwise webhooks will fail. Add the following lines to your Forgejo configuration (usually at `/etc/forgejo/conf/app.ini`).
```ini
...
[webhook]
ALLOWED_HOST_LIST=external,loopback
```
For reference see [Configuration Cheat Sheet](https://docs.gitea.io/en-us/config-cheat-sheet/#webhook-webhook).

![forgejo oauth setup](forgejo_oauth.gif)


## 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_FORGEJO`
> Default: `false`

Enables the Forgejo driver.

### `WOODPECKER_FORGEJO_URL`
> Default: `https://codeberg.org`

Configures the Forgejo server address.

### `WOODPECKER_FORGEJO_CLIENT`
> Default: empty

Configures the Forgejo OAuth client id. This is used to authorize access.

### `WOODPECKER_FORGEJO_CLIENT_FILE`
> Default: empty

Read the value for `WOODPECKER_FORGEJO_CLIENT` from the specified filepath

### `WOODPECKER_FORGEJO_SECRET`
> Default: empty

Configures the Forgejo OAuth client secret. This is used to authorize access.

### `WOODPECKER_FORGEJO_SECRET_FILE`
> Default: empty

Read the value for `WOODPECKER_FORGEJO_SECRET` from the specified filepath

### `WOODPECKER_FORGEJO_SKIP_VERIFY`
> Default: `false`

Configure if SSL verification should be skipped.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions server/forge/forgejo/client/branch.go
@@ -0,0 +1,55 @@
// Copyright The Forgejo Authors.
// SPDX-License-Identifier: Apache-2.0

package client

import (
"fmt"
)

// PayloadUser represents the author or committer of a commit
type PayloadUser struct {
// Full name of the commit author
Name string `json:"name"`
Email string `json:"email"`
UserName string `json:"username"`
}

// PayloadCommit represents a commit
type PayloadCommit struct {
// sha1 hash of the commit
ID string `json:"id"`
Message string `json:"message"`
URL string `json:"url"`
// Author *PayloadUser `json:"author"`
// Committer *PayloadUser `json:"committer"`
// Verification *PayloadCommitVerification `json:"verification"`
// Timestamp time.Time `json:"timestamp"`
Added []string `json:"added"`
Removed []string `json:"removed"`
Modified []string `json:"modified"`
}

type Branch struct {
Name string `json:"name"`
Commit *PayloadCommit `json:"commit"`
}

func (f *Forgejo) ListRepoBranches(user, repo string, opt ListOptions) ([]*Branch, *Response, error) {
if err := escapeValidatePathSegments(&user, &repo); err != nil {
return nil, nil, err
}
opt.setDefaults()
branches := make([]*Branch, 0, opt.PageSize)
resp, err := f.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/branches?%s", user, repo, opt.getURLQuery().Encode()), nil, nil, &branches)
return branches, resp, err
}

func (f *Forgejo) GetRepoBranch(user, repo, branch string) (*Branch, *Response, error) {
if err := escapeValidatePathSegments(&user, &repo, &branch); err != nil {
return nil, nil, err
}
b := new(Branch)
resp, err := f.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/branches/%s", user, repo, branch), nil, nil, &b)
return b, resp, err
}