mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 13:21:00 +00:00
test: add e2e tests with local Forgejo instance (#201)
* feat(forge): add new forge for forgejo We only support repositories hosted on Forgejo instances, but not Forgejo Actions or Woodpecker as CI solutions for now. * test(e2e): introduce e2e test framework with local forgejo
This commit is contained in:
parent
afef176e37
commit
fcf7906149
14 changed files with 936 additions and 1 deletions
29
.github/workflows/ci.yaml
vendored
29
.github/workflows/ci.yaml
vendored
|
|
@ -35,6 +35,35 @@ jobs:
|
||||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
flags: unit
|
||||||
|
|
||||||
|
test-e2e-forgejo:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
# We can not use "jobs.<job>.services".
|
||||||
|
# We want to mount the config file, which is only available after "Checkout".
|
||||||
|
- name: Start Forgejo
|
||||||
|
working-directory: test/e2e/forgejo
|
||||||
|
run: docker compose up --wait
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -tags e2e_forgejo -v -race -coverpkg=./... -coverprofile=coverage.txt ./test/e2e/forgejo
|
||||||
|
|
||||||
|
|
||||||
|
- name: Upload results to Codecov
|
||||||
|
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
flags: e2e
|
||||||
|
|
||||||
go-mod-tidy:
|
go-mod-tidy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ linters:
|
||||||
- name: exported
|
- name: exported
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
|
gomoddirectives:
|
||||||
|
replace-allow-list:
|
||||||
|
- codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2
|
||||||
|
|
||||||
formatters:
|
formatters:
|
||||||
enable:
|
enable:
|
||||||
- gci
|
- gci
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
rp "github.com/apricote/releaser-pleaser"
|
rp "github.com/apricote/releaser-pleaser"
|
||||||
"github.com/apricote/releaser-pleaser/internal/commitparser/conventionalcommits"
|
"github.com/apricote/releaser-pleaser/internal/commitparser/conventionalcommits"
|
||||||
"github.com/apricote/releaser-pleaser/internal/forge"
|
"github.com/apricote/releaser-pleaser/internal/forge"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/forge/forgejo"
|
||||||
"github.com/apricote/releaser-pleaser/internal/forge/github"
|
"github.com/apricote/releaser-pleaser/internal/forge/github"
|
||||||
"github.com/apricote/releaser-pleaser/internal/forge/gitlab"
|
"github.com/apricote/releaser-pleaser/internal/forge/gitlab"
|
||||||
"github.com/apricote/releaser-pleaser/internal/log"
|
"github.com/apricote/releaser-pleaser/internal/log"
|
||||||
|
|
@ -26,6 +27,10 @@ func newRunCommand() *cobra.Command {
|
||||||
flagRepo string
|
flagRepo string
|
||||||
flagExtraFiles string
|
flagExtraFiles string
|
||||||
flagUpdaters []string
|
flagUpdaters []string
|
||||||
|
|
||||||
|
flagAPIURL string
|
||||||
|
flagAPIToken string
|
||||||
|
flagUsername string
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmd = &cobra.Command{
|
var cmd = &cobra.Command{
|
||||||
|
|
@ -68,6 +73,21 @@ func newRunCommand() *cobra.Command {
|
||||||
Owner: flagOwner,
|
Owner: flagOwner,
|
||||||
Repo: flagRepo,
|
Repo: flagRepo,
|
||||||
})
|
})
|
||||||
|
case "forgejo":
|
||||||
|
logger.DebugContext(ctx, "using forge Forgejo")
|
||||||
|
f, err = forgejo.New(logger, &forgejo.Options{
|
||||||
|
Options: forgeOptions,
|
||||||
|
Owner: flagOwner,
|
||||||
|
Repo: flagRepo,
|
||||||
|
|
||||||
|
APIURL: flagAPIURL,
|
||||||
|
APIToken: flagAPIToken,
|
||||||
|
Username: flagUsername,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, "failed to create client", "err", err)
|
||||||
|
return fmt.Errorf("failed to create forgejo client: %w", err)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown --forge: %s", flagForge)
|
return fmt.Errorf("unknown --forge: %s", flagForge)
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +130,10 @@ func newRunCommand() *cobra.Command {
|
||||||
cmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
|
cmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
|
||||||
cmd.PersistentFlags().StringSliceVar(&flagUpdaters, "updaters", []string{}, "")
|
cmd.PersistentFlags().StringSliceVar(&flagUpdaters, "updaters", []string{}, "")
|
||||||
|
|
||||||
|
cmd.PersistentFlags().StringVar(&flagAPIURL, "api-url", "", "")
|
||||||
|
cmd.PersistentFlags().StringVar(&flagAPIToken, "api-token", "", "")
|
||||||
|
cmd.PersistentFlags().StringVar(&flagUsername, "username", "", "")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
codecov.yaml
Normal file
2
codecov.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ignore:
|
||||||
|
- "test"
|
||||||
9
go.mod
9
go.mod
|
|
@ -1,10 +1,11 @@
|
||||||
module github.com/apricote/releaser-pleaser
|
module github.com/apricote/releaser-pleaser
|
||||||
|
|
||||||
go 1.23.2
|
go 1.24
|
||||||
|
|
||||||
toolchain go1.25.1
|
toolchain go1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.0.0-00010101000000-000000000000
|
||||||
github.com/blang/semver/v4 v4.0.0
|
github.com/blang/semver/v4 v4.0.0
|
||||||
github.com/go-git/go-billy/v5 v5.6.2
|
github.com/go-git/go-billy/v5 v5.6.2
|
||||||
github.com/go-git/go-git/v5 v5.16.2
|
github.com/go-git/go-git/v5 v5.16.2
|
||||||
|
|
@ -20,17 +21,21 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
|
github.com/42wim/httpsig v1.2.3 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
|
@ -49,3 +54,5 @@ require (
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 => codeberg.org/apricote/forgejo-sdk/forgejo/v2 v2.1.2-0.20250615152743-47d3f0434561
|
||||||
|
|
|
||||||
19
go.sum
19
go.sum
|
|
@ -1,5 +1,9 @@
|
||||||
|
codeberg.org/apricote/forgejo-sdk/forgejo/v2 v2.1.2-0.20250615152743-47d3f0434561 h1:ZFGmrGQ7cd2mbSLrfjrj3COwPKFfKM6sDO/IsrGDW7w=
|
||||||
|
codeberg.org/apricote/forgejo-sdk/forgejo/v2 v2.1.2-0.20250615152743-47d3f0434561/go.mod h1:2i9GsyawlJtVMO5pTS/Om5uo2O3JN/eCjGWy5v15NGg=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||||
|
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
|
@ -19,6 +23,8 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||||
|
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
|
|
@ -27,6 +33,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||||
|
|
@ -50,6 +58,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
|
||||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||||
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
|
|
@ -111,16 +121,23 @@ gitlab.com/gitlab-org/api/client-go v0.144.0 h1:np2G+h2vanpxQrqGIM9LGmoKQecyUanj
|
||||||
gitlab.com/gitlab-org/api/client-go v0.144.0/go.mod h1:rw89Kl9AsKmxRhzkfUSfZ+1jpTewwueKvAYwoYmUoQ8=
|
gitlab.com/gitlab-org/api/client-go v0.144.0/go.mod h1:rw89Kl9AsKmxRhzkfUSfZ+1jpTewwueKvAYwoYmUoQ8=
|
||||||
go.abhg.dev/goldmark/toc v0.11.0 h1:IRixVy3/yVPKvFBc37EeBPi8XLTXrtH6BYaonSjkF8o=
|
go.abhg.dev/goldmark/toc v0.11.0 h1:IRixVy3/yVPKvFBc37EeBPi8XLTXrtH6BYaonSjkF8o=
|
||||||
go.abhg.dev/goldmark/toc v0.11.0/go.mod h1:XMFIoI1Sm6dwF9vKzVDOYE/g1o5BmKXghLG8q/wJNww=
|
go.abhg.dev/goldmark/toc v0.11.0/go.mod h1:XMFIoI1Sm6dwF9vKzVDOYE/g1o5BmKXghLG8q/wJNww=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|
@ -132,6 +149,8 @@ golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
|
|
||||||
529
internal/forge/forgejo/forgejo.go
Normal file
529
internal/forge/forgejo/forgejo.go
Normal file
|
|
@ -0,0 +1,529 @@
|
||||||
|
package forgejo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||||
|
"github.com/blang/semver/v4"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/forge"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/git"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/pointer"
|
||||||
|
"github.com/apricote/releaser-pleaser/internal/releasepr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ()
|
||||||
|
|
||||||
|
var _ forge.Forge = &Forgejo{}
|
||||||
|
|
||||||
|
type Forgejo struct {
|
||||||
|
options *Options
|
||||||
|
|
||||||
|
client *forgejo.Client
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) RepoURL() string {
|
||||||
|
return fmt.Sprintf("%s/%s/%s", f.options.APIURL, f.options.Owner, f.options.Repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) CloneURL() string {
|
||||||
|
return fmt.Sprintf("%s.git", f.RepoURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) ReleaseURL(version string) string {
|
||||||
|
return fmt.Sprintf("%s/releases/tag/%s", f.RepoURL(), version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) PullRequestURL(id int) string {
|
||||||
|
return fmt.Sprintf("%s/pulls/%d", f.RepoURL(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) GitAuth() transport.AuthMethod {
|
||||||
|
return &http.BasicAuth{
|
||||||
|
Username: f.options.Username,
|
||||||
|
Password: f.options.APIToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) CommitAuthor(ctx context.Context) (git.Author, error) {
|
||||||
|
f.log.DebugContext(ctx, "getting commit author from current token user")
|
||||||
|
|
||||||
|
user, _, err := f.client.GetMyUserInfo()
|
||||||
|
if err != nil {
|
||||||
|
return git.Author{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Same for other forges?
|
||||||
|
name := user.FullName
|
||||||
|
if name == "" {
|
||||||
|
name = user.UserName
|
||||||
|
}
|
||||||
|
|
||||||
|
return git.Author{
|
||||||
|
Name: name,
|
||||||
|
Email: user.Email,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) LatestTags(ctx context.Context) (git.Releases, error) {
|
||||||
|
f.log.DebugContext(ctx, "listing all tags in forgejo repository")
|
||||||
|
|
||||||
|
tags, err := all(func(listOptions forgejo.ListOptions) ([]*forgejo.Tag, *forgejo.Response, error) {
|
||||||
|
return f.client.ListRepoTags(f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.ListRepoTagsOptions{ListOptions: listOptions},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return git.Releases{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var releases git.Releases
|
||||||
|
|
||||||
|
for _, fTag := range tags {
|
||||||
|
tag := &git.Tag{
|
||||||
|
Hash: fTag.Commit.SHA,
|
||||||
|
Name: fTag.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := semver.Parse(strings.TrimPrefix(tag.Name, "v"))
|
||||||
|
if err != nil {
|
||||||
|
f.log.WarnContext(
|
||||||
|
ctx, "unable to parse tag as semver, skipping",
|
||||||
|
"tag.name", tag.Name,
|
||||||
|
"tag.hash", tag.Hash,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if releases.Latest == nil {
|
||||||
|
releases.Latest = tag
|
||||||
|
}
|
||||||
|
if len(version.Pre) == 0 {
|
||||||
|
// Stable version tag
|
||||||
|
// We return once we have found the latest stable tag, not needed to look at every single tag.
|
||||||
|
releases.Stable = tag
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) CommitsSince(ctx context.Context, tag *git.Tag) ([]git.Commit, error) {
|
||||||
|
var repositoryCommits []*forgejo.Commit
|
||||||
|
var err error
|
||||||
|
if tag != nil {
|
||||||
|
repositoryCommits, err = f.commitsSinceTag(ctx, tag)
|
||||||
|
} else {
|
||||||
|
repositoryCommits, err = f.commitsSinceInit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var commits = make([]git.Commit, 0, len(repositoryCommits))
|
||||||
|
for _, fCommit := range repositoryCommits {
|
||||||
|
commit := git.Commit{
|
||||||
|
Hash: fCommit.SHA,
|
||||||
|
Message: fCommit.RepoCommit.Message,
|
||||||
|
}
|
||||||
|
commit.PullRequest, err = f.prForCommit(ctx, commit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check for commit pull request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commits = append(commits, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) commitsSinceTag(_ context.Context, tag *git.Tag) ([]*forgejo.Commit, error) {
|
||||||
|
head := f.options.BaseBranch
|
||||||
|
log := f.log.With("base", tag.Hash, "head", head)
|
||||||
|
log.Debug("comparing commits")
|
||||||
|
|
||||||
|
compare, _, err := f.client.CompareCommits(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
tag.Hash, head)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return compare.Commits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) commitsSinceInit(_ context.Context) ([]*forgejo.Commit, error) {
|
||||||
|
head := f.options.BaseBranch
|
||||||
|
log := f.log.With("head", head)
|
||||||
|
log.Debug("listing all commits")
|
||||||
|
|
||||||
|
repositoryCommits, err := all(
|
||||||
|
func(listOptions forgejo.ListOptions) ([]*forgejo.Commit, *forgejo.Response, error) {
|
||||||
|
return f.client.ListRepoCommits(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.ListCommitOptions{
|
||||||
|
ListOptions: listOptions,
|
||||||
|
SHA: f.options.BaseBranch,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repositoryCommits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) prForCommit(_ context.Context, commit git.Commit) (*git.PullRequest, error) {
|
||||||
|
// We naively look up the associated PR for each commit through the "List pull requests associated with a commit"
|
||||||
|
// endpoint. This requires len(commits) requests.
|
||||||
|
// Using the "List pull requests" endpoint might be faster, as it allows us to fetch 100 arbitrary PRs per request,
|
||||||
|
// but worst case we need to look up all PRs made in the repository ever.
|
||||||
|
|
||||||
|
f.log.Debug("fetching pull requests associated with commit", "commit.hash", commit.Hash)
|
||||||
|
|
||||||
|
pullRequest, _, err := f.client.GetCommitPullRequest(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
commit.Hash,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if strings.HasPrefix(err.Error(), "pull request does not exist") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return forgejoPRToPullRequest(pullRequest), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) EnsureLabelsExist(_ context.Context, labels []releasepr.Label) error {
|
||||||
|
f.log.Debug("fetching labels on repo")
|
||||||
|
fLabels, err := all(func(listOptions forgejo.ListOptions) ([]*forgejo.Label, *forgejo.Response, error) {
|
||||||
|
return f.client.ListRepoLabels(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.ListLabelsOptions{ListOptions: listOptions})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range labels {
|
||||||
|
if !slices.ContainsFunc(fLabels, func(fLabel *forgejo.Label) bool {
|
||||||
|
return fLabel.Name == label.Name
|
||||||
|
}) {
|
||||||
|
f.log.Info("creating label in repository", "label.name", label.Name)
|
||||||
|
_, _, err = f.client.CreateLabel(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.CreateLabelOption{
|
||||||
|
Name: label.Name,
|
||||||
|
Color: label.Color,
|
||||||
|
Description: label.Description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) PullRequestForBranch(_ context.Context, branch string) (*releasepr.ReleasePullRequest, error) {
|
||||||
|
prs, err := all(
|
||||||
|
func(listOptions forgejo.ListOptions) ([]*forgejo.PullRequest, *forgejo.Response, error) {
|
||||||
|
return f.client.ListRepoPullRequests(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.ListPullRequestsOptions{
|
||||||
|
ListOptions: listOptions,
|
||||||
|
State: forgejo.StateOpen,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pr := range prs {
|
||||||
|
if pr.Base.Ref == f.options.BaseBranch && pr.Head.Ref == branch {
|
||||||
|
return forgejoPRToReleasePullRequest(pr), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error {
|
||||||
|
fPR, _, err := f.client.CreatePullRequest(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.CreatePullRequestOption{
|
||||||
|
Title: pr.Title,
|
||||||
|
Head: pr.Head,
|
||||||
|
Base: f.options.BaseBranch,
|
||||||
|
Body: pr.Description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: String ID?
|
||||||
|
pr.ID = int(fPR.ID)
|
||||||
|
|
||||||
|
err = f.SetPullRequestLabels(ctx, pr, []releasepr.Label{}, pr.Labels)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) UpdatePullRequest(_ context.Context, pr *releasepr.ReleasePullRequest) error {
|
||||||
|
_, _, err := f.client.EditPullRequest(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
int64(pr.ID), forgejo.EditPullRequestOption{
|
||||||
|
Title: pr.Title,
|
||||||
|
Body: pr.Description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) SetPullRequestLabels(_ context.Context, pr *releasepr.ReleasePullRequest, remove, add []releasepr.Label) error {
|
||||||
|
allLabels, err := all(
|
||||||
|
func(listOptions forgejo.ListOptions) ([]*forgejo.Label, *forgejo.Response, error) {
|
||||||
|
return f.client.ListRepoLabels(f.options.Owner, f.options.Repo, forgejo.ListLabelsOptions{ListOptions: listOptions})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
findLabel := func(labelName string) *forgejo.Label {
|
||||||
|
for _, fLabel := range allLabels {
|
||||||
|
if fLabel.Name == labelName {
|
||||||
|
return fLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range remove {
|
||||||
|
fLabel := findLabel(label.Name)
|
||||||
|
if fLabel == nil {
|
||||||
|
return fmt.Errorf("unable to remove label %q, not found in API", label.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.client.DeleteIssueLabel(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
int64(pr.ID), fLabel.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addIDs := make([]int64, 0, len(add))
|
||||||
|
for _, label := range add {
|
||||||
|
fLabel := findLabel(label.Name)
|
||||||
|
if fLabel == nil {
|
||||||
|
return fmt.Errorf("unable to add label %q, not found in API", label.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
addIDs = append(addIDs, fLabel.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = f.client.AddIssueLabels(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
int64(pr.ID), forgejo.IssueLabelsOption{Labels: addIDs},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) ClosePullRequest(_ context.Context, pr *releasepr.ReleasePullRequest) error {
|
||||||
|
_, _, err := f.client.EditPullRequest(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
int64(pr.ID), forgejo.EditPullRequestOption{
|
||||||
|
State: pointer.Pointer(forgejo.StateClosed),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) PendingReleases(_ context.Context, pendingLabel releasepr.Label) ([]*releasepr.ReleasePullRequest, error) {
|
||||||
|
fPRs, err := all(func(listOptions forgejo.ListOptions) ([]*forgejo.PullRequest, *forgejo.Response, error) {
|
||||||
|
return f.client.ListRepoPullRequests(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.ListPullRequestsOptions{
|
||||||
|
// Filtering by Label ID is possible in the API, but not implemented in the Go SDK.
|
||||||
|
State: forgejo.StateClosed,
|
||||||
|
ListOptions: listOptions,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// "The target couldn't be found." means that the repo does not have pull requests activated.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prs := make([]*releasepr.ReleasePullRequest, 0, len(fPRs))
|
||||||
|
|
||||||
|
for _, pr := range fPRs {
|
||||||
|
pending := slices.ContainsFunc(pr.Labels, func(l *forgejo.Label) bool {
|
||||||
|
return l.Name == pendingLabel.Name
|
||||||
|
})
|
||||||
|
if !pending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// pr.Merged is always nil :(
|
||||||
|
if !pr.HasMerged {
|
||||||
|
// Closed and not merged
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
prs = append(prs, forgejoPRToReleasePullRequest(pr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return prs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forgejo) CreateRelease(_ context.Context, commit git.Commit, title, changelog string, preRelease, latest bool) error {
|
||||||
|
// latest can not be set through the API
|
||||||
|
|
||||||
|
_, _, err := f.client.CreateRelease(
|
||||||
|
f.options.Owner, f.options.Repo,
|
||||||
|
forgejo.CreateReleaseOption{
|
||||||
|
TagName: title,
|
||||||
|
Target: commit.Hash,
|
||||||
|
Title: title,
|
||||||
|
Note: changelog,
|
||||||
|
IsPrerelease: preRelease,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func all[T any](f func(listOptions forgejo.ListOptions) ([]T, *forgejo.Response, error)) ([]T, error) {
|
||||||
|
results := make([]T, 0)
|
||||||
|
page := 1
|
||||||
|
|
||||||
|
for {
|
||||||
|
pageResults, resp, err := f(forgejo.ListOptions{Page: page})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, pageResults...)
|
||||||
|
|
||||||
|
if page == resp.LastPage || resp.LastPage == 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forgejoPRToPullRequest(pr *forgejo.PullRequest) *git.PullRequest {
|
||||||
|
return &git.PullRequest{
|
||||||
|
ID: int(pr.ID),
|
||||||
|
Title: pr.Title,
|
||||||
|
Description: pr.Body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forgejoPRToReleasePullRequest(pr *forgejo.PullRequest) *releasepr.ReleasePullRequest {
|
||||||
|
labels := make([]releasepr.Label, 0, len(pr.Labels))
|
||||||
|
for _, label := range pr.Labels {
|
||||||
|
labelName := label.Name
|
||||||
|
if i := slices.IndexFunc(releasepr.KnownLabels, func(label releasepr.Label) bool {
|
||||||
|
return label.Name == labelName
|
||||||
|
}); i >= 0 {
|
||||||
|
labels = append(labels, releasepr.KnownLabels[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var releaseCommit *git.Commit
|
||||||
|
if pr.MergedCommitID != nil {
|
||||||
|
releaseCommit = &git.Commit{Hash: *pr.MergedCommitID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &releasepr.ReleasePullRequest{
|
||||||
|
PullRequest: *forgejoPRToPullRequest(pr),
|
||||||
|
Labels: labels,
|
||||||
|
|
||||||
|
Head: pr.Head.Ref,
|
||||||
|
ReleaseCommit: releaseCommit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Options) autodiscover() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Options) ClientOptions() []forgejo.ClientOption {
|
||||||
|
options := []forgejo.ClientOption{}
|
||||||
|
|
||||||
|
if g.APIToken != "" {
|
||||||
|
options = append(options, forgejo.SetToken(g.APIToken))
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
forge.Options
|
||||||
|
|
||||||
|
Owner string
|
||||||
|
Repo string
|
||||||
|
|
||||||
|
APIURL string
|
||||||
|
Username string
|
||||||
|
APIToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(log *slog.Logger, options *Options) (*Forgejo, error) {
|
||||||
|
options.autodiscover()
|
||||||
|
|
||||||
|
client, err := forgejo.NewClient(options.APIURL, options.ClientOptions()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetUserAgent("releaser-pleaser")
|
||||||
|
|
||||||
|
f := &Forgejo{
|
||||||
|
options: options,
|
||||||
|
|
||||||
|
client: client,
|
||||||
|
log: log.With("forge", "forgejo"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
15
mise.toml
15
mise.toml
|
|
@ -8,3 +8,18 @@ ko = "v0.18.0" # renovate: datasource=github-releases depName=ko-build/ko
|
||||||
[settings]
|
[settings]
|
||||||
# Experimental features are needed for the Go backend
|
# Experimental features are needed for the Go backend
|
||||||
experimental = true
|
experimental = true
|
||||||
|
|
||||||
|
[tasks.lint]
|
||||||
|
run = "golangci-lint run"
|
||||||
|
|
||||||
|
[tasks.test]
|
||||||
|
run = "go test -v -race ./..."
|
||||||
|
|
||||||
|
[tasks.test-e2e]
|
||||||
|
run = "go test -tags e2e_forgejo -v -race ./test/e2e/forgejo"
|
||||||
|
|
||||||
|
[tasks.e2e-forgejo-start]
|
||||||
|
run = "docker compose --project-directory ./test/e2e/forgejo up -d --wait"
|
||||||
|
|
||||||
|
[tasks.e2e-forgejo-stop]
|
||||||
|
run = "docker compose --project-directory ./test/e2e/forgejo down"
|
||||||
|
|
|
||||||
19
test/e2e/forge.go
Normal file
19
test/e2e/forge.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestForge interface {
|
||||||
|
Init(ctx context.Context, runID string) error
|
||||||
|
CreateRepo(t *testing.T, opts CreateRepoOpts) (*Repository, error)
|
||||||
|
|
||||||
|
RunArguments() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRepoOpts struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
DefaultBranch string
|
||||||
|
}
|
||||||
23
test/e2e/forgejo/app.ini
Normal file
23
test/e2e/forgejo/app.ini
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
WORK_PATH = /data/gitea
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = sqlite3
|
||||||
|
PATH = /data/gitea/forgejo.db
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = true
|
||||||
|
SECRET_KEY = releaser-pleaser
|
||||||
|
INTERNAL_TOKEN = releaser-pleaser
|
||||||
|
|
||||||
|
[service]
|
||||||
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
|
ENABLE_NOTIFY_MAIL = false
|
||||||
|
DISABLE_REGISTRATION = true
|
||||||
|
|
||||||
|
[server]
|
||||||
|
DOMAIN = localhost
|
||||||
|
HTTP_PORT = 3000
|
||||||
|
ROOT_URL = http://localhost:3000/
|
||||||
|
|
||||||
|
[oauth2]
|
||||||
|
JWT_SECRET = rTD-FL2n_aBB6v4AOcr5lBvwgZ6PSr3HGZAuNH6nMu8
|
||||||
16
test/e2e/forgejo/compose.yaml
Normal file
16
test/e2e/forgejo/compose.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
services:
|
||||||
|
forgejo:
|
||||||
|
image: codeberg.org/forgejo/forgejo:11
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
- '222:22'
|
||||||
|
volumes:
|
||||||
|
- data:/data/gitea
|
||||||
|
- ./app.ini:/data/gitea/conf/app.ini:ro
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "localhost:3000/api/healthz"]
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
113
test/e2e/forgejo/forge.go
Normal file
113
test/e2e/forgejo/forge.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package forgejo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/test/e2e"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TestAPIURL = "http://localhost:3000"
|
||||||
|
|
||||||
|
TestUserNameTemplate = "rp-%s"
|
||||||
|
TestUserPassword = "releaser-pleaser"
|
||||||
|
TestUserEmailTemplate = "releaser-pleaser-%s@example.com"
|
||||||
|
TestTokenName = "rp"
|
||||||
|
TestTokenScopes = "write:user,write:issue,write:repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestForge struct {
|
||||||
|
username string
|
||||||
|
token string
|
||||||
|
client *forgejo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TestForge) Init(ctx context.Context, runID string) error {
|
||||||
|
if err := f.initUser(ctx, runID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := f.initClient(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TestForge) initUser(ctx context.Context, runID string) error {
|
||||||
|
f.username = fmt.Sprintf(TestUserNameTemplate, runID)
|
||||||
|
|
||||||
|
//gosec:disable G204
|
||||||
|
if output, err := exec.CommandContext(ctx,
|
||||||
|
"docker", "compose", "exec", "--user=1000", "forgejo",
|
||||||
|
"forgejo", "admin", "user", "create",
|
||||||
|
"--username", f.username,
|
||||||
|
"--password", TestUserPassword,
|
||||||
|
"--email", fmt.Sprintf(TestUserEmailTemplate, runID),
|
||||||
|
"--must-change-password=false",
|
||||||
|
).CombinedOutput(); err != nil {
|
||||||
|
slog.Debug("create forgejo user output", "output", output)
|
||||||
|
return fmt.Errorf("failed to create forgejo user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//gosec:disable G204
|
||||||
|
token, err := exec.CommandContext(ctx,
|
||||||
|
"docker", "compose", "exec", "--user=1000", "forgejo",
|
||||||
|
"forgejo", "admin", "user", "generate-access-token",
|
||||||
|
"--username", f.username,
|
||||||
|
"--token-name", TestTokenName,
|
||||||
|
"--scopes", TestTokenScopes,
|
||||||
|
"--raw",
|
||||||
|
).Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create forgejo token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.token = strings.TrimSpace(string(token))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TestForge) initClient(ctx context.Context) (err error) {
|
||||||
|
f.client, err = forgejo.NewClient(TestAPIURL,
|
||||||
|
forgejo.SetToken(f.token),
|
||||||
|
forgejo.SetUserAgent("releaser-pleaser-e2e-tests"),
|
||||||
|
forgejo.SetContext(ctx),
|
||||||
|
// forgejo.SetDebugMode(),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TestForge) CreateRepo(t *testing.T, opts e2e.CreateRepoOpts) (*e2e.Repository, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
repo, _, err := f.client.CreateRepo(forgejo.CreateRepoOption{
|
||||||
|
Name: opts.Name,
|
||||||
|
Description: opts.Description,
|
||||||
|
DefaultBranch: opts.DefaultBranch,
|
||||||
|
Readme: "Default",
|
||||||
|
AutoInit: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e2e.Repository{
|
||||||
|
Name: repo.Name,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TestForge) RunArguments() []string {
|
||||||
|
return []string{"--forge=forgejo",
|
||||||
|
fmt.Sprintf("--owner=%s", f.username),
|
||||||
|
fmt.Sprintf("--api-url=%s", TestAPIURL),
|
||||||
|
fmt.Sprintf("--api-token=%s", f.token),
|
||||||
|
fmt.Sprintf("--username=%s", f.username),
|
||||||
|
}
|
||||||
|
}
|
||||||
39
test/e2e/forgejo/forgejo_test.go
Normal file
39
test/e2e/forgejo/forgejo_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
//go:build e2e_forgejo
|
||||||
|
|
||||||
|
package forgejo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/test/e2e"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
f *e2e.Framework
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
f, err = e2e.NewFramework(ctx, &TestForge{})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to set up test framework", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRepository(t *testing.T) {
|
||||||
|
_ = f.NewRepository(t, t.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRun(t *testing.T) {
|
||||||
|
repo := f.NewRepository(t, t.Name())
|
||||||
|
require.NoError(t, f.Run(t, repo, []string{}))
|
||||||
|
}
|
||||||
96
test/e2e/framework.go
Normal file
96
test/e2e/framework.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/apricote/releaser-pleaser/cmd/rp/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TestDefaultBranch = "main"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomString() string {
|
||||||
|
randomBytes := make([]byte, 4)
|
||||||
|
if _, err := rand.Read(randomBytes); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(randomBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Framework struct {
|
||||||
|
runID string
|
||||||
|
forge TestForge
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFramework(ctx context.Context, forge TestForge) (*Framework, error) {
|
||||||
|
f := &Framework{
|
||||||
|
runID: randomString(),
|
||||||
|
forge: forge,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := forge.Init(ctx, f.runID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framework) NewRepository(t *testing.T, name string) *Repository {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
r := &Repository{
|
||||||
|
Name: fmt.Sprintf("%s-%s-%s", name, f.runID, randomString()),
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := f.forge.CreateRepo(t, CreateRepoOpts{
|
||||||
|
Name: r.Name,
|
||||||
|
Description: name,
|
||||||
|
DefaultBranch: TestDefaultBranch,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, repo)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Framework) Run(t *testing.T, r *Repository, extraFiles []string) error {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
|
||||||
|
rootCmd := cmd.NewRootCmd()
|
||||||
|
rootCmd.SetArgs(append([]string{
|
||||||
|
"run",
|
||||||
|
fmt.Sprintf("--repo=%s", r.Name),
|
||||||
|
fmt.Sprintf("--extra-files=%q", strings.Join(extraFiles, "\n")),
|
||||||
|
}, f.forge.RunArguments()...))
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
rootCmd.SetOut(&stdout)
|
||||||
|
rootCmd.SetErr(&stderr)
|
||||||
|
|
||||||
|
err := rootCmd.ExecuteContext(ctx)
|
||||||
|
|
||||||
|
stdoutString := stdout.String()
|
||||||
|
stderrString := stderr.String()
|
||||||
|
|
||||||
|
t.Log(stdoutString)
|
||||||
|
t.Log(stderrString)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue