Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions docs/content/docs/providers/forgejo.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ name):

<https://your.forgejo.domain/user/settings/applications>

When creating the token, select these scopes:
{{< callout type="info" >}}
If using the CLI, the token needs to be created with access to **All (public, private, and limited)** repositories.
{{< /callout >}}

You should also select these scopes:

### Required Scopes

Expand All @@ -42,12 +46,36 @@ unless you plan to use `settings.policy.ok_to_test` or

Store the generated token in a safe place, or you will have to recreate it.

## Webhook Configuration (Manual)
## Webhook Configuration using the CLI

{{< callout type="info" >}}
The `tkn pac create repo` and `tkn pac webhook` commands do not currently support Forgejo. You must configure the webhook manually.
The CLI uses your Forgejo token to create the webhook and also stores it for runtime
use. To use a token with tighter repository access, follow the manual
configuration steps instead.
{{< /callout >}}

Use [`tkn pac create repo`]({{< relref "/docs/cli" >}}) to create the
Repository CR, create the Kubernetes secret, and configure the Forgejo webhook:

```shell
tkn pac create repo
```

The command prompts for the Forgejo repository URL, controller URL, webhook
secret, Forgejo token, and Forgejo instance URL.

For an existing Repository CR, use `tkn pac webhook add` to create the Forgejo
webhook and update the webhook secret:

```shell
tkn pac webhook add -n target-namespace my-repo
```

If the Repository CR does not reference a `git_provider` secret yet, the command
creates one and updates the Repository CR.

## Webhook Configuration (Manual)

1. From your Forgejo repository, go to **Settings** -> **Webhooks** and click **Add Webhook** -> **Forgejo**.

2. Set the **HTTP method** to **POST** and **POST content type** to **application/json**.
Expand Down
174 changes: 174 additions & 0 deletions pkg/cli/webhook/forgejo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package webhook

import (
"context"
"fmt"
"net/url"
"strings"

"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
"github.com/AlecAivazis/survey/v2"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli/prompt"
"github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
"github.com/openshift-pipelines/pipelines-as-code/pkg/random"
)

type forgejoConfig struct {
Client *forgejo.Client
IOStream *cli.IOStreams
controllerURL string
repoOwner string
repoName string
webhookSecret string
personalAccessToken string
APIURL string
}

func (fg *forgejoConfig) Run(_ context.Context, opts *Options) (*response, error) {
err := fg.askForgejoWebhookConfig(opts.RepositoryURL, opts.ControllerURL, opts.ProviderAPIURL, opts.PersonalAccessToken)
if err != nil {
return nil, err
}

return &response{
ControllerURL: fg.controllerURL,
PersonalAccessToken: fg.personalAccessToken,
WebhookSecret: fg.webhookSecret,
APIURL: fg.APIURL,
}, fg.create()
}

func (fg *forgejoConfig) askForgejoWebhookConfig(repoURL, controllerURL, apiURL, personalAccessToken string) error {
if repoURL == "" {
msg := "Please enter the git repository url you want to be configured: "
if err := prompt.SurveyAskOne(&survey.Input{Message: msg}, &repoURL,
survey.WithValidator(survey.Required)); err != nil {
return err
}
} else {
fmt.Fprintf(fg.IOStream.Out, "βœ“ Setting up Forgejo Webhook for Repository %s\n", repoURL)
}

repoURLForOwner := strings.TrimSuffix(repoURL, "/")
repoURLForOwner = strings.TrimSuffix(repoURLForOwner, ".git")
defaultRepo, err := formatting.GetRepoOwnerFromURL(repoURLForOwner)
if err != nil {
return err
}

repoArr := strings.Split(defaultRepo, "/")
if len(repoArr) != 2 {
return fmt.Errorf("invalid repository, needs to be of format 'org-name/repo-name'")
}
Comment thread
SecKatie marked this conversation as resolved.
fg.repoOwner = repoArr[0]
fg.repoName = repoArr[1]

fg.controllerURL = controllerURL
if fg.controllerURL != "" {
var answer bool
fmt.Fprintf(fg.IOStream.Out, "πŸ‘€ I have detected a controller url: %s\n", fg.controllerURL)
err := prompt.SurveyAskOne(&survey.Confirm{
Message: "Do you want me to use it?",
Default: true,
}, &answer)
if err != nil {
return err
}
if !answer {
fg.controllerURL = ""
}
}

if fg.controllerURL == "" {
if err := prompt.SurveyAskOne(&survey.Input{
Message: "Please enter your controller public route URL: ",
}, &fg.controllerURL, survey.WithValidator(survey.Required)); err != nil {
return err
}
}

data := random.AlphaString(12)
msg := fmt.Sprintf("Please enter the secret to configure the webhook for payload validation (default: %s): ", data)
if err := prompt.SurveyAskOne(&survey.Input{Message: msg, Default: data}, &fg.webhookSecret); err != nil {
return err
}

if personalAccessToken == "" {
fmt.Fprintln(fg.IOStream.Out, "β„Ή ️You now need to create a Forgejo personal access token with repository access All, write:repository, and write:issue.")
if err := prompt.SurveyAskOne(&survey.Password{
Message: "Please enter the Forgejo access token: ",
}, &fg.personalAccessToken, survey.WithValidator(survey.Required)); err != nil {
return err
}
} else {
fg.personalAccessToken = personalAccessToken
}

if apiURL == "" {
defaultURL, err := forgejoInstanceURL(repoURL)
if err != nil {
return err
}
Comment on lines +109 to +112
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the repository URL is an SSH URL (e.g., git@forgejo.example.com:owner/repo.git), forgejoInstanceURL will return an error because it cannot parse the scheme and host. Returning this error immediately blocks the user from proceeding with the CLI setup.

Instead of returning the error and aborting, we should handle it gracefully by falling back to an empty default URL so the user can still manually input their Forgejo URL.

Suggested change
defaultURL, err := forgejoInstanceURL(repoURL)
if err != nil {
return err
}
defaultURL, _ := forgejoInstanceURL(repoURL)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into this and amend the commit!

Copy link
Copy Markdown
Author

@SecKatie SecKatie Jun 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay this is interesting feedback because ssh urls are not even supported by the admission webhook:

Error: admission webhook "validation.pipelinesascode.tekton.dev" denied the request: URL scheme must be http or https

This error with the admission webhook hits before we even get to this line.

if err := prompt.SurveyAskOne(&survey.Input{
Message: "Please enter your Forgejo URL: ",
Default: defaultURL,
}, &fg.APIURL, survey.WithValidator(survey.Required)); err != nil {
return err
}
} else {
fg.APIURL = apiURL
}

return nil
}

func (fg *forgejoConfig) create() error {
fgClient, err := fg.newClient()
if err != nil {
return err
}

hook := forgejo.CreateHookOption{
Type: forgejo.HookTypeForgejo,
Config: map[string]string{
"content_type": "json",
"url": fg.controllerURL,
"secret": fg.webhookSecret,
},
Events: []string{
Comment thread
SecKatie marked this conversation as resolved.
"push",
"pull_request",
"pull_request_sync",
"pull_request_label",
"issue_comment",
},
Active: true,
}

_, _, err = fgClient.CreateRepoHook(fg.repoOwner, fg.repoName, hook)
if err != nil {
return fmt.Errorf("failed to create Forgejo webhook: %w", err)
}

fmt.Fprintf(fg.IOStream.Out, "βœ“ Webhook has been created on repository %v/%v\n", fg.repoOwner, fg.repoName)
return nil
}

func (fg *forgejoConfig) newClient() (*forgejo.Client, error) {
if fg.Client != nil {
return fg.Client, nil
}
return forgejo.NewClient(fg.APIURL, forgejo.SetToken(fg.personalAccessToken))
}

func forgejoInstanceURL(repoURL string) (string, error) {
parsedURL, err := url.Parse(repoURL)
if err != nil {
return "", err
}
if parsedURL.Scheme == "" || parsedURL.Host == "" {
return "", fmt.Errorf("invalid forgejo repository URL: %s", repoURL)
}
return fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host), nil
}
Loading
Loading