Compare commits

...

137 commits
v0.5.1 ... main

Author SHA1 Message Date
renovate[bot]
944b70cee9
deps: update dependency golangci-lint to v2.8.0 (#325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 01:43:29 +00:00
renovate[bot]
2a1a08057b
deps: update module github.com/yuin/goldmark to v1.7.16 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 10:02:37 +00:00
renovate[bot]
60cbffba9d
deps: update module github.com/yuin/goldmark to v1.7.15 (#323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 21:42:52 +00:00
renovate[bot]
6ef1405140
deps: update module github.com/yuin/goldmark to v1.7.14 (#322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-04 14:32:11 +00:00
renovate[bot]
d9c8d3e5af
deps: update dependency ko-build/ko to v0.18.1 (#320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 16:47:59 +00:00
renovate[bot]
b48c9a654f
deps: update dependency rust-lang/mdbook to v0.5.2 (#319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 22:50:29 +00:00
renovate[bot]
163eaf31a6
deps: update dependency golangci-lint to v2.7.2 (#316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 18:13:31 +00:00
renovate[bot]
804cf8040a
deps: update dependency golangci-lint to v2.7.1 (#313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 18:00:02 +00:00
renovate[bot]
60d9aa3982
deps: update module github.com/go-git/go-billy/v5 to v5.7.0 (#315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 13:00:46 +00:00
renovate[bot]
8d6175c13b
deps: update module github.com/spf13/cobra to v1.10.2 (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 03:50:01 +00:00
renovate[bot]
6dfa96a9ba
deps: update dependency golangci-lint to v2.7.0 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 21:01:35 +00:00
renovate[bot]
fb2a0b8167
deps: update dependency go to v1.25.5 (#310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 17:37:09 +00:00
renovate[bot]
db611e5cc2
deps: update module gitlab.com/gitlab-org/api/client-go to v0.161.1 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 21:30:33 +00:00
renovate[bot]
b451c08634
deps: update module gitlab.com/gitlab-org/api/client-go to v0.160.2 (#308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 12:50:46 +00:00
renovate[bot]
9592a6a975
deps: update module github.com/go-git/go-git/v5 to v5.16.4 (#307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 02:08:18 +00:00
renovate[bot]
e5bccc9fb9
deps: update module golang.org/x/crypto to v0.45.0 [security] (#306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 09:57:10 +00:00
renovate[bot]
995f4beb9a
deps: update dependency rust-lang/mdbook to v0.5.1 (#305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 05:39:33 +00:00
renovate[bot]
ec851d7511
deps: update module gitlab.com/gitlab-org/api/client-go to v0.160.1 (#304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 21:04:09 +00:00
renovate[bot]
ad845e61c7
deps: update dependency rust-lang/mdbook to v0.5.0 (#303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 06:50:50 +00:00
renovate[bot]
0f040ff8e7
deps: update dependency golangci-lint to v2.6.2 (#301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-14 18:13:08 +00:00
renovate[bot]
6dd0424029
deps: update module gitlab.com/gitlab-org/api/client-go to v0.160.0 (#300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 00:00:03 +00:00
renovate[bot]
291581ef6d
deps: update dependency go to v1.25.4 (#299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 21:07:16 +00:00
renovate[bot]
29a033103d
deps: update module gitlab.com/gitlab-org/api/client-go to v0.159.0 (#298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 01:05:09 +00:00
renovate[bot]
c1c2111e03
deps: update dependency golangci-lint to v2.6.1 (#297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 12:51:40 +00:00
renovate[bot]
c4796a546e
deps: update module gitlab.com/gitlab-org/api/client-go to v0.158.0 (#296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 11:31:58 +00:00
renovate[bot]
cb92e2b67f
deps: update dependency golangci-lint to v2.6.0 (#295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 01:06:35 +00:00
renovate[bot]
23e9d06c6e
deps: update module gitlab.com/gitlab-org/api/client-go to v0.157.1 (#294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 16:15:14 +00:00
renovate[bot]
0129c78abc
deps: update dependency go to v1.25.3 (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 22:16:30 +00:00
renovate[bot]
226548adfa
deps: update module gitlab.com/gitlab-org/api/client-go to v0.157.0 (#292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 18:11:57 +00:00
renovate[bot]
0d2efe0a6d
deps: update module gitlab.com/gitlab-org/api/client-go to v0.156.0 (#291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 08:51:53 +00:00
renovate[bot]
cead3fb5c1
deps: update module gitlab.com/gitlab-org/api/client-go to v0.155.0 (#290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 02:00:41 +00:00
renovate[bot]
918249a0d3
deps: update module gitlab.com/gitlab-org/api/client-go to v0.154.0 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 15:03:19 +00:00
renovate[bot]
f24b69e8fa
deps: update module gitlab.com/gitlab-org/api/client-go to v0.153.0 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 09:01:44 +00:00
renovate[bot]
d95b779f83
deps: update dependency go to v1.25.2 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 04:15:40 +00:00
renovate[bot]
e32838e3d0
deps: update module gitlab.com/gitlab-org/api/client-go to v0.152.0 (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 00:46:25 +00:00
renovate[bot]
8607cb6f71
deps: update module github.com/go-git/go-git/v5 to v5.16.3 (#284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 21:47:34 +00:00
renovate[bot]
ff899fe9e8
deps: update module gitlab.com/gitlab-org/api/client-go to v0.151.0 (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 00:49:57 +00:00
renovate[bot]
0d16c770d3
deps: update module gitlab.com/gitlab-org/api/client-go to v0.150.0 (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:02:04 +00:00
renovate[bot]
21e3fdbcad
deps: update module gitlab.com/gitlab-org/api/client-go to v0.149.0 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 01:14:18 +00:00
renovate[bot]
71364599d8
deps: update module gitlab.com/gitlab-org/api/client-go to v0.148.1 (#279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 17:51:38 +00:00
fa27415be5
chore(main): release v0.7.1 (#278) 2025-09-26 05:36:27 +00:00
Jonas L.
b0c50518b3
fix: no html escaping for changelog template (#277) 2025-09-25 12:40:35 +02:00
Jonas L.
612928a382
fix: using code blocks within release-notes (#275)
Increase the number of code blocks backticks to 4 for the release note prefix and suffix, to allow users to embed their own code blocks using only 3 backticks.
2025-09-25 12:25:35 +02:00
renovate[bot]
93bb42e781
deps: update module gitlab.com/gitlab-org/api/client-go to v0.148.0 (#274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 11:10:35 +00:00
renovate[bot]
63a8b91b34
deps: update module gitlab.com/gitlab-org/api/client-go to v0.147.1 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 16:42:57 +00:00
renovate[bot]
d0a44e3fb8
deps: update module gitlab.com/gitlab-org/api/client-go to v0.147.0 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 10:09:57 +00:00
renovate[bot]
63acf9aa3a
deps: update dependency golangci-lint to v2.5.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 20:45:11 +00:00
renovate[bot]
250cc6c2aa
deps: update module gitlab.com/gitlab-org/api/client-go to v0.146.0 (#270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 14:32:34 +00:00
renovate[bot]
ef94c3c8f7
deps: update module gitlab.com/gitlab-org/api/client-go to v0.145.0 (#269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 10:51:32 +00:00
renovate[bot]
f300bbb6b0
deps: update actions/checkout digest to 08eba0b (#264) 2025-09-15 14:47:12 +02:00
renovate[bot]
cc627599db
deps: update codecov/codecov-action digest to 5a10915 (#266)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 14:46:37 +02:00
renovate[bot]
8c6b99560c
deps: update module codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 to v2.2.0 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 09:39:48 +00:00
renovate[bot]
2c1d29f639
deps: update module gitlab.com/gitlab-org/api/client-go to v0.144.1 (#267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 01:31:08 +00:00
fcf7906149
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
2025-09-13 12:00:54 +02:00
renovate[bot]
afef176e37
deps: update module gitlab.com/gitlab-org/api/client-go to v0.144.0 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-12 15:20:21 +00:00
renovate[bot]
e3d2cfa6b8
deps: update codecov/codecov-action digest to 5a10915 (#253) 2025-09-11 13:30:36 +02:00
renovate[bot]
6b9738bcea
deps: update module gitlab.com/gitlab-org/api/client-go to v0.143.3 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 06:39:09 +00:00
renovate[bot]
e7950cfbc1
deps: update module gitlab.com/gitlab-org/api/client-go to v0.143.2 (#259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 13:03:16 +00:00
renovate[bot]
bccfa93a15
deps: update module gitlab.com/gitlab-org/api/client-go to v0.143.1 (#258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 22:14:06 +00:00
renovate[bot]
3f9108e702
deps: update module gitlab.com/gitlab-org/api/client-go to v0.143.0 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 12:07:27 +00:00
renovate[bot]
5ea1798a68
deps: update module github.com/spf13/cobra to v1.10.1 (#256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 21:37:52 +00:00
renovate[bot]
de80649838
deps: update module gitlab.com/gitlab-org/api/client-go to v0.142.6 (#255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 21:37:38 +00:00
renovate[bot]
f765abbb92
deps: update dependency go to v1.25.1 (#254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 18:51:22 +00:00
renovate[bot]
132f62d82d
deps: update module gitlab.com/gitlab-org/api/client-go to v0.142.5 (#252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 10:33:26 +00:00
renovate[bot]
1d2f74752b
deps: update module gitlab.com/gitlab-org/api/client-go to v0.142.4 (#251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-28 07:46:08 +00:00
renovate[bot]
84b3acbe4d
deps: update module github.com/stretchr/testify to v1.11.1 (#250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 12:05:21 +00:00
renovate[bot]
c4610b8e5e
deps: update module gitlab.com/gitlab-org/api/client-go to v0.142.2 (#249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-26 18:07:17 +00:00
renovate[bot]
ce56ac0cd1
deps: update module gitlab.com/gitlab-org/api/client-go to v0.142.1 (#248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 05:34:37 +00:00
renovate[bot]
69be23dd6f
deps: update module github.com/stretchr/testify to v1.11.0 (#247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 22:11:38 +00:00
852c08ed3d
ci: fix ko settings after using mise (#246) 2025-08-24 17:39:10 +02:00
e1afa22e0a
ci: mdbooks binary is missing (#245) 2025-08-24 17:16:40 +02:00
f077b647e7
ci: separate renovate manager for toml (#244) 2025-08-24 15:07:58 +00:00
44b76e55f8
ci: allow regex manager in toml files for mise (#243) 2025-08-24 14:56:11 +00:00
e83a7c9a23
ci: mise cleanup (#242)
- Renovate does not find the "github:*" dependencies in `mise.toml`
- The `mdbooks` tools was still installed manually with our own action,
  this is removed and mise is used instead.
2025-08-24 14:52:01 +00:00
renovate[bot]
563885899c
deps: update actions/checkout action to v5 (#239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 16:46:49 +02:00
renovate[bot]
16ba2c6b09
deps: update module github.com/google/go-github/v72 to v74 (#241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 16:46:16 +02:00
renovate[bot]
e6c8f3f93b
deps: update actions/upload-pages-artifact action to v4 (#240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 16:45:41 +02:00
e6503da93a
refactor(cmd): use factories instead of global cobra command structs (#238)
This enables us to create new commands for e2e tests.
2025-08-24 14:44:05 +00:00
renovate[bot]
5b5b29c0b5
deps: update dependency go to v1.25.0 (#222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 13:50:59 +00:00
c768260a2e
chore: use mise to install all tools in CI and locally (#237)
Makes sure that I have the same versions locally as CI
2025-08-24 15:49:23 +02:00
b3cb9e128c
chore(main): release v0.7.0 (#232) 2025-08-23 23:05:30 +02:00
d259921215
fix(github): duplicate release pr when process is stopped at wrong moment (#236)
In a timing issue, the release pull request may be created but the
releaser-pleaser labels not added. On the next run releaser-pleaser
then creates a second release pull request. We try to reduce the chance
of this happening by checking the context cancellation at the top, and
if its not cancelled we run both API requests without passing along any
cancellations from the parent context.

Closes #215
2025-08-23 23:00:52 +02:00
48b1894cac
feat: highlight breaking changes in release notes (#234)
Add a `**BREAKING**` prefix to any entries in the changelog that are
marked as breaking changes.

Closes #225
2025-08-23 22:40:28 +02:00
5306e2dd35
fix: filter out empty updaters in input (#235) 2025-08-23 22:38:24 +02:00
f1aa1a2ef4
refactor: let updaters define the files they want to run on (#233)
This change reverses the responsibility for which files the updaters are
run on. Now each updater can specify the list of files and wether the
files should be created when they do not exist yet. This simplifies the
handling of each update in releaserpleaser.go, as we can just iterate
over all updaters and call it for each file of that updater.

Also update the flags to allow users to easily define which updaters
should run.
2025-08-23 22:14:34 +02:00
Mattis Krämer
1e9e0aa5d9
feat: add updater for package.json (#213) 2025-08-23 22:05:52 +02:00
renovate[bot]
6237c9b666
deps: update codecov/codecov-action digest to fdcc847 (#229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:23:41 +02:00
renovate[bot]
2f7e8b9afe
deps: update actions/checkout digest to 08eba0b (#220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 20:23:12 +02:00
renovate[bot]
73d9c877b0
deps: update module gitlab.com/gitlab-org/api/client-go to v0.142.0 (#231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 17:57:43 +00:00
renovate[bot]
014ec7b723
deps: update module gitlab.com/gitlab-org/api/client-go to v0.141.2 (#230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 17:46:15 +00:00
renovate[bot]
dbde726d15
deps: update module gitlab.com/gitlab-org/api/client-go to v0.141.1 (#228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 22:53:08 +00:00
renovate[bot]
6e2c754376
deps: update module gitlab.com/gitlab-org/api/client-go to v0.140.0 (#227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 17:25:07 +00:00
renovate[bot]
eb6c687737
deps: update module gitlab.com/gitlab-org/api/client-go to v0.139.2 (#226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 10:09:38 +00:00
renovate[bot]
2fe0f0e5b6
deps: update dependency golangci/golangci-lint to v2.4.0 (#224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-14 00:57:48 +00:00
renovate[bot]
fd903e056c
deps: update module gitlab.com/gitlab-org/api/client-go to v0.139.0 (#223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 09:51:56 +00:00
renovate[bot]
763a5defac
deps: update module gitlab.com/gitlab-org/api/client-go to v0.138.0 (#221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-12 13:44:44 +00:00
renovate[bot]
e73bf82a92
deps: update dependency go to v1.24.6 (#219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 21:26:34 +00:00
renovate[bot]
1fad5e6264
deps: update dependency golangci/golangci-lint to v2.3.1 (#216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-03 00:47:54 +00:00
renovate[bot]
785c29deb2
deps: update module github.com/yuin/goldmark to v1.7.13 (#218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 21:14:29 +00:00
renovate[bot]
273107b9af
deps: update module gitlab.com/gitlab-org/api/client-go to v0.137.0 (#217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 22:36:38 +00:00
renovate[bot]
64874f9089
deps: update dependency rust-lang/mdbook to v0.4.52 (#214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 02:12:30 +00:00
renovate[bot]
942aa80aa9
deps: update dependency golangci/golangci-lint to v2.2.2 (#212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 16:02:05 +00:00
8eb7eadc4e
chore(main): release v0.6.1 (#211) 2025-07-11 11:32:09 +02:00
Zadkiel AHARONIAN
bcca36e856
fix(gitlab): support fast-forward merges (#210)
This change allows detecting that the releaser-pleaser PR is well merged. 

As of today, it fails with "ERR failed to create pending releases: pull request is missing the merge commit".
2025-07-11 11:23:21 +02:00
renovate[bot]
75fe90ab6e
deps: update dependency go to v1.24.5 (#209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 00:02:06 +00:00
renovate[bot]
3e77b7e0d9
deps: update module gitlab.com/gitlab-org/api/client-go to v0.134.0 (#208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 16:50:20 +00:00
renovate[bot]
a7347bc191
deps: update module gitlab.com/gitlab-org/api/client-go to v0.133.0 (#206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 12:13:44 +00:00
renovate[bot]
6e97e0d601
deps: update module gitlab.com/gitlab-org/api/client-go to v0.132.0 (#205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 16:10:25 +00:00
renovate[bot]
48b8696efc
deps: update module gitlab.com/gitlab-org/api/client-go to v0.131.0 (#204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 19:53:10 +00:00
renovate[bot]
dfe39868ac
deps: update dependency golangci/golangci-lint to v2.2.1 (#203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-29 22:15:32 +00:00
renovate[bot]
5a273f9ab5
deps: update dependency golangci/golangci-lint to v2.2.0 (#202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 21:42:14 +00:00
fc1ee70c28
chore(main): release v0.6.0 (#189) 2025-06-14 16:47:17 +02:00
0de242a4e6
ci: only build single platform for local releaser-pleaser jobs (#200)
The image is never pushed and only executed on linux/amd64 hosts, so
building linux/arm64 is a waste of time and resources.
2025-06-14 14:34:26 +00:00
d540e2221d
docs: describe concurrency and conflict considerations (#199)
Describe the issue with concurrency, the global state and what went into
the recent changes in #196, #197 and #198.
2025-06-14 14:24:16 +00:00
2d3a960939
feat: run one job concurrently to reduce chance of conflicts (#198)
Each run of releaser-pleaser acts on the same global state in the
forge. Therefore, parallel runs are unnecessary.

This commit also communicates to the GitHub and GitLab CI pipelines that
the releaser-pleaser jobs can be cancelled as early as possible.

- On GitHub Actions this can be guaranteed through the workflow
  settings. These settings are copied into each repository that uses
  releaser-pleaser, so users need to update this manually. I will add a
  note to the release notes for this.
- On GitLab CI/CD this requires the user to configure a project level setting to
  "auto-cancel redundant pipelines". We will not recommend user to set
  this, as it is quite invasive and can break their regular CI pipelines.
2025-06-14 13:43:35 +00:00
d24ae7de98
feat: detect changed pull request description and retry process (#197)
If the release PR description was changed by a human after
releaser-pleaser fetched the PR for the first time, releaser-pleaser
would revert the users changes accidentally.

This commit introduces an additional check right before updating the
pull request description, to make sure we do not accidentally loose user
changes. There is still the potential for a conflict in between us
checking the description is the same, and updating the description. The
time window for this should be reduced from multiple seconds-minutes to
a few hundred milliseconds at most.

In case a conflict is detected, we retry the whole process up to 2
times, to make sure that the users changes are reflected as soon as
possible. This is especially important on GitLab CI/CD because a changed
pull (merge) request description does not cause another job to run.

With this change, the branch is still pushed, as the user is not
expected to make any changes to it.

Fixes #151
2025-06-14 13:23:05 +00:00
08d35f2f57
feat: graceful shutdown when CI job is cancelled (#196)
By listening on SIGINT and SIGTERM signals we can stop executing as soon
as reasonably possible. This helps to avoid uncessary work and stop the
job earlier.

Right now we have no manual checks for cancelled contexts, and rely on
the http client to check for it while making requests.
2025-06-14 13:19:34 +00:00
eae0045359
feat: colorize log output (#195)
Makes it easier to read, uses lmittmann/tint.
2025-06-14 13:11:28 +00:00
50b2762dca
fix: missing push when files were changed (#193)
The whole check to avoid pushes when they were only rebases was broken
and compared the wrong things. Unfortunately this worked for nearly all
unit tests, except for one were I used the wrong assertion.

This fixed the check by comparing the right things and inverting the
assertion in the unit test to make sure things do not break again in the
future.

Bug was introduced in #114.
2025-06-14 13:03:04 +02:00
renovate[bot]
983162d26e
deps: update module gitlab.com/gitlab-org/api/client-go to v0.130.1 (#192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 03:44:02 +00:00
renovate[bot]
e6e9779e87
deps: update module gitlab.com/gitlab-org/api/client-go to v0.130.0 (#191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 16:54:03 +00:00
5f1849106c
fix: crash when running in repo without any tags (#190)
Recent changes in v0.5.1 introduced a bug that caused releaser-pleaser
to crash when running in a repository that contained no tags at all.
This fixes the issue by checking if there is a tag before using it in
the logger.

Bug was introduced in #174.
2025-06-09 11:22:27 +02:00
81a855f5ab
feat: avoid pushing release branch only for rebasing (#114)
Right now releaser-pleaser pushes the branch even if it is only for a "rebase",
this wastes CI resources. Instead, it should only push when there are changes
to the files it owns.

- **Old**: Push when there is a diff origin/release-pr..release-pr
- **New**: Push when the these two diffs are not the same:
  
      origin/main..release-pr
      $(git merge-base origin/main origin/release-pr)..release-pr

Closes #92
2025-06-09 10:52:09 +02:00
175d6d0633
feat: real user as commit author (#187)
Previously all commits were authored and committed by

    releaser-pleaser <>

This looked weird when looking at the commit. We now check with the
Forge API for details on the currently authenticated user, and use that
name and email as the commit author. The commit committer stays the same
for now.

In GitHub, the default `$GITHUB_TOKEN` does not allow access to the
required endpoint, so for github the user `github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>` is hardcoded
when the request fails.
2025-06-09 08:06:56 +00:00
renovate[bot]
f2786c8f39
deps: update module github.com/go-git/go-git/v5 to v5.16.2 (#188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 07:06:06 +00:00
1779356543
deps: replace xanzy/go-gitlab with official client (#182) 2025-06-07 16:42:29 +00:00
renovate[bot]
ad13dc24e0
chore(config): migrate config .github/renovate.json5 (#186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 16:40:56 +00:00
renovate[bot]
d91d93fc8c
deps: update codecov/codecov-action digest to 18283e0 (#113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 18:39:31 +02:00
renovate[bot]
e3ecd8993c
chore: update golangci-lint to v2 and fix breakage (#184)
deps: update golangci/golangci-lint-action action to v8

Co-authored-by: Julian Tölle <julian.toelle97@gmail.com>
2025-06-07 16:39:18 +00:00
renovate[bot]
49855aa700
deps: update module github.com/google/go-github/v66 to v72 (#185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 18:39:05 +02:00
renovate[bot]
f49481cd92
deps: update golangci/golangci-lint-action digest to 55c2c14 (#135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 18:24:36 +02:00
renovate[bot]
359912dcc0
deps: update actions/setup-go digest to d35c59a (#122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 18:24:24 +02:00
renovate[bot]
1f882bf014
deps: update dependency go to v1.24.4 (#181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 22:07:04 +00:00
renovate[bot]
31b12b9c33
deps: update module github.com/go-git/go-git/v5 to v5.16.1 (#180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 12:24:23 +00:00
renovate[bot]
9c8b854de0
deps: update registry.gitlab.com/gitlab-org/release-cli docker tag to v0.24.0 (#179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 21:27:02 +00:00
renovate[bot]
c1b0f15e07
deps: update dependency rust-lang/mdbook to v0.4.51 (#177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 23:34:42 +00:00
renovate[bot]
58a9f1c9d5
deps: update dependency rust-lang/mdbook to v0.4.50 (#176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 16:12:43 +00:00
70 changed files with 2601 additions and 444 deletions

View file

@ -1,16 +0,0 @@
name: "Setup mdbook"
inputs:
version:
description: "mdbook version"
runs:
using: composite
steps:
- name: Setup mdbook
shell: bash
env:
url: https://github.com/rust-lang/mdbook/releases/download/${{ inputs.version }}/mdbook-${{ inputs.version }}-x86_64-unknown-linux-gnu.tar.gz
run: |
mkdir mdbook
curl -sSL "$url" | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH

View file

@ -62,13 +62,22 @@
customManagers: [
{
customType: 'regex',
fileMatch: [
'.+\\.ya?ml$',
managerFilePatterns: [
'/.+\\.ya?ml$/',
],
matchStrings: [
': (?<currentValue>.+) # renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>[^\\s]+)(?: lookupName=(?<packageName>[^\\s]+))?(?: versioning=(?<versioning>[a-z-]+))?(?: extractVersion=(?<extractVersion>[^\\s]+))?',
],
},
{
customType: 'regex',
managerFilePatterns: [
'/.+\\.toml$/'
],
matchStrings: [
'= "(?<currentValue>.+)" # renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>[^\\s]+)(?: lookupName=(?<packageName>[^\\s]+))?(?: versioning=(?<versioning>[a-z-]+))?(?: extractVersion=(?<extractVersion>[^\\s]+))?',
],
}
],
postUpdateOptions: [
'gomodUpdateImportPaths',

View file

@ -2,7 +2,7 @@ name: ci
on:
push:
branches: [main]
branches: [ main ]
pull_request:
jobs:
@ -10,48 +10,68 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5
with:
go-version-file: go.mod
- uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
with:
version: v1.64.8 # renovate: datasource=github-releases depName=golangci/golangci-lint
install-mode: none
args: --timeout 5m
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5
with:
go-version-file: go.mod
- uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3
- name: Run tests
run: go test -v -race -coverpkg=./... -coverprofile=coverage.txt ./...
- name: Upload results to Codecov
uses: codecov/codecov-action@288befbd1044bd1756afb0bdae077549e0ddb31f # v5
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: unit
test-e2e-forgejo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # 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@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: e2e
go-mod-tidy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5
with:
go-version-file: go.mod
- uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3
- name: Run go mod tidy
run: go mod tidy

View file

@ -13,13 +13,11 @@ jobs:
id-token: write # To update the deployment status
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
lfs: "true"
- uses: ./.github/actions/setup-mdbook
with:
version: v0.4.49 # renovate: datasource=github-releases depName=rust-lang/mdbook
- uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3
- name: Build Book
working-directory: docs
@ -29,7 +27,7 @@ jobs:
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- name: Upload artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
# Upload entire repository
path: "docs/book"

View file

@ -11,7 +11,7 @@ jobs:
REMOTE: mirror
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
# Need all to fetch all tags so we can push them
fetch-depth: 0

View file

@ -14,12 +14,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5
with:
go-version-file: go.mod
- uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3
- name: Prepare ko
run: |
echo "${{ github.token }}" | ko login ghcr.io --username "dummy" --password-stdin
repo=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
echo "KO_DOCKER_REPO=ghcr.io/${repo}"
echo "KO_DOCKER_REPO=ghcr.io/${repo}" >> $GITHUB_ENV
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- run: ko build --bare --tags ${{ github.ref_name }} github.com/apricote/releaser-pleaser/cmd/rp

View file

@ -2,7 +2,7 @@ name: releaser-pleaser
on:
push:
branches: [main]
branches: [ main ]
# Using pull_request_target to avoid tainting the actual release PR with code from open feature pull requests
pull_request_target:
types:
@ -10,7 +10,14 @@ on:
- labeled
- unlabeled
permissions: {}
# Only one job needs to run at a time, if a new job is started there is probably new data to include in the response, so
# it does not make sense to finish the previous job. This also helps with "data-race conflicts", where a human changes
# the PR description but releaser-pleaser was already running and overwrites the humans changes.
concurrency:
group: releaser-pleaser
cancel-in-progress: true
permissions: { }
jobs:
releaser-pleaser:
@ -18,23 +25,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: main
- name: Set up Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5
with:
go-version-file: go.mod
- uses: jdx/mise-action@5ac50f778e26fac95da98d50503682459e86d566 # v3
# Build container image from current commit and replace image ref in `action.yml`
# Without this, any new flags in `action.yml` would break the job in this repository until the new
# version is released. But a new version can only be released if this job works.
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
- run: ko build --bare --local --tags ci github.com/apricote/releaser-pleaser/cmd/rp
- run: ko build --bare --local --platform linux/amd64 --tags ci github.com/apricote/releaser-pleaser/cmd/rp
- run: mkdir -p .github/actions/releaser-pleaser
- run: "sed -i 's|image: .*$|image: docker://ghcr.io/apricote/releaser-pleaser:ci|g' action.yml"
- run: "sed -i 's|image: .*$|image: docker://ko.local:ci|g' action.yml"
# Dogfood the action to make sure it works for users.
- name: releaser-pleaser

View file

@ -6,7 +6,7 @@ stages: [ release ]
# and create a corresponding GitLab Release.
create-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:v0.23.0
image: registry.gitlab.com/gitlab-org/release-cli:v0.24.0
script: echo "Creating release $CI_COMMIT_TAG"
rules:
- if: $CI_COMMIT_TAG

View file

@ -1,14 +1,38 @@
version: "2"
linters:
presets:
- bugs
- error
- import
- metalinter
- module
- unused
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- durationcheck
- errchkjson
- errorlint
- exhaustive
- gocheckcompilerdirectives
- gochecksumtype
- gocritic
- gomoddirectives
- gomodguard
- gosec
- gosmopolitan
- loggercheck
- makezero
- musttag
- nilerr
- nilnesserr
- noctx
- protogetter
- reassign
- recvcheck
- rowserrcheck
- spancheck
- sqlclosecheck
- testifylint
- unparam
- zerologlint
- revive
disable:
# preset error
@ -18,10 +42,23 @@ linters:
# preset import
- depguard
linters-settings:
gci:
sections:
- standard
- default
- localmodule
settings:
revive:
rules:
- name: exported
disabled: true
gomoddirectives:
replace-allow-list:
- codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2
formatters:
enable:
- gci
- goimports
settings:
gci:
sections:
- standard
- default
- localmodule

View file

@ -1,5 +1,79 @@
# Changelog
## [v0.7.1](https://github.com/apricote/releaser-pleaser/releases/tag/v0.7.1)
### Bug Fixes
- using code blocks within release-notes (#275)
- no html escaping for changelog template (#277)
## [v0.7.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.7.0)
### Highlights :sparkles:
#### Update version in `package.json`
Thanks to @Mattzi it is now possible to use `releaser-pleaser` in Javascript/Node.js projects with a `package.json` file.
You can enable this with the option `updaters: packagejson` in the GitHub Actions / GitLab CI/CD config.
All updaters, including the defaults `changelog` and `generic` can now be enabled and disabled through this field. You can find a full list in the [documentation](https://apricote.github.io/releaser-pleaser/reference/updaters.html).
### Features
- add updater for package.json (#213)
- highlight breaking changes in release notes (#234)
### Bug Fixes
- filter out empty updaters in input (#235)
- **github**: duplicate release pr when process is stopped at wrong moment (#236)
## [v0.6.1](https://github.com/apricote/releaser-pleaser/releases/tag/v0.6.1)
### Bug Fixes
- **gitlab**: support fast-forward merges (#210)
## [v0.6.0](https://github.com/apricote/releaser-pleaser/releases/tag/v0.6.0)
### ✨ Highlights
#### Reduced resource usage
`releaser-pleaser` now uses less resources:
- It now skips pushing changes to the release pull request if they are only a rebase.
- The configurations for GitHub Actions and GitLab CI/CD now makes sure that only a single job is running at the same time. On GitHub unnecessary/duplicate jobs are also automatically aborted.
- It handles the stop signals from the CI environment and tries to exit quickly.
\```yaml
concurrency:
group: releaser-pleaser
cancel-in-progress: true
\```
#### Avoid losing manual edits to release pull request
Before, releaser-pleaser was prone to overwriting user changes to the release pull request if they were made after releaser-pleaser already started running. There is now an additional check right before submitting the changes to see if the description changed, and retry if it did.
#### Proper commit authorship
Before, the release commits were created by `releaser-pleaser &lt;&gt;`. This was ugly to look at. We now check for details on the API user used to talk to the forge, and use that users details instead as the commit author. The committer is still `releaser-pleaser`.
### Features
- real user as commit author (#187)
- avoid pushing release branch only for rebasing (#114)
- colorize log output (#195)
- graceful shutdown when CI job is cancelled (#196)
- detect changed pull request description and retry process (#197)
- run one job concurrently to reduce chance of conflicts (#198)
### Bug Fixes
- crash when running in repo without any tags (#190)
## [v0.5.1](https://github.com/apricote/releaser-pleaser/releases/tag/v0.5.1)
### Bug Fixes

View file

@ -14,19 +14,24 @@ inputs:
required: false
default: ${{ github.token }}
extra-files:
description: 'List of files that are scanned for version references.'
description: 'List of files that are scanned for version references by the generic updater.'
required: false
default: ""
updaters:
description: "List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic"
required: false
default: ""
# Remember to update docs/reference/github-action.md
outputs: {}
outputs: { }
runs:
using: 'docker'
image: docker://ghcr.io/apricote/releaser-pleaser:v0.5.1 # x-releaser-pleaser-version
image: docker://ghcr.io/apricote/releaser-pleaser:v0.7.1 # x-releaser-pleaser-version
args:
- run
- --forge=github
- --branch=${{ inputs.branch }}
- --extra-files="${{ inputs.extra-files }}"
- --updaters="${{ inputs.updaters }}"
env:
GITHUB_TOKEN: "${{ inputs.token }}"
GITHUB_USER: "oauth2"

View file

@ -1,20 +1,29 @@
package cmd
import (
"context"
"log/slog"
"os"
"os/signal"
"runtime/debug"
"syscall"
"github.com/spf13/cobra"
)
var logger *slog.Logger
func NewRootCmd() *cobra.Command {
var cmd = &cobra.Command{
Use: "rp",
Short: "",
Long: ``,
Version: version(),
SilenceUsage: true, // Makes it harder to find the actual error
SilenceErrors: true, // We log manually with slog
}
var rootCmd = &cobra.Command{
Use: "rp",
Short: "",
Long: ``,
Version: version(),
cmd.AddCommand(newRunCommand())
return cmd
}
func version() string {
@ -39,15 +48,33 @@ func version() string {
}
func Execute() {
err := rootCmd.Execute()
// Behaviour when cancelling jobs:
//
// GitHub Actions: https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/canceling-a-workflow#steps-github-takes-to-cancel-a-workflow-run
// 1. SIGINT
// 2. Wait 7500ms
// 3. SIGTERM
// 4. Wait 2500ms
// 5. SIGKILL
//
// GitLab CI/CD: https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/4446
// 1. SIGTERM
// 2. Wait ???
// 3. SIGKILL
//
// We therefore need to listen on SIGINT and SIGTERM
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
go func() {
// Make sure to stop listening on signals after receiving the first signal to hand control of the signal back
// to the runtime. The Go runtime implements a "force shutdown" if the signal is received again.
<-ctx.Done()
slog.InfoContext(ctx, "Received shutdown signal, stopping...")
stop()
}()
err := NewRootCmd().ExecuteContext(ctx)
if err != nil {
slog.ErrorContext(ctx, err.Error())
os.Exit(1)
}
}
func init() {
logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
}

View file

@ -2,6 +2,8 @@ package cmd
import (
"fmt"
"log/slog"
"slices"
"strings"
"github.com/spf13/cobra"
@ -9,89 +11,130 @@ import (
rp "github.com/apricote/releaser-pleaser"
"github.com/apricote/releaser-pleaser/internal/commitparser/conventionalcommits"
"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/gitlab"
"github.com/apricote/releaser-pleaser/internal/log"
"github.com/apricote/releaser-pleaser/internal/updater"
"github.com/apricote/releaser-pleaser/internal/versioning"
)
var runCmd = &cobra.Command{
Use: "run",
RunE: run,
}
func newRunCommand() *cobra.Command {
var (
flagForge string
flagBranch string
flagOwner string
flagRepo string
flagExtraFiles string
flagUpdaters []string
var (
flagForge string
flagBranch string
flagOwner string
flagRepo string
flagExtraFiles string
)
func init() {
rootCmd.AddCommand(runCmd)
runCmd.PersistentFlags().StringVar(&flagForge, "forge", "", "")
runCmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "")
runCmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
runCmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
runCmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
}
func run(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
var err error
logger.DebugContext(ctx, "run called",
"forge", flagForge,
"branch", flagBranch,
"owner", flagOwner,
"repo", flagRepo,
flagAPIURL string
flagAPIToken string
flagUsername string
)
var f forge.Forge
var cmd = &cobra.Command{
Use: "run",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
logger := log.GetLogger(cmd.ErrOrStderr())
forgeOptions := forge.Options{
Repository: flagRepo,
BaseBranch: flagBranch,
var err error
logger.DebugContext(ctx, "run called",
"forge", flagForge,
"branch", flagBranch,
"owner", flagOwner,
"repo", flagRepo,
)
var f forge.Forge
forgeOptions := forge.Options{
Repository: flagRepo,
BaseBranch: flagBranch,
}
switch flagForge {
case "gitlab":
logger.DebugContext(ctx, "using forge GitLab")
f, err = gitlab.New(logger, &gitlab.Options{
Options: forgeOptions,
Path: fmt.Sprintf("%s/%s", flagOwner, flagRepo),
})
if err != nil {
slog.ErrorContext(ctx, "failed to create client", "err", err)
return fmt.Errorf("failed to create gitlab client: %w", err)
}
case "github":
logger.DebugContext(ctx, "using forge GitHub")
f = github.New(logger, &github.Options{
Options: forgeOptions,
Owner: flagOwner,
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:
return fmt.Errorf("unknown --forge: %s", flagForge)
}
extraFiles := parseExtraFiles(flagExtraFiles)
updaterNames := parseUpdaters(flagUpdaters)
updaters := []updater.Updater{}
for _, name := range updaterNames {
switch name {
case "generic":
updaters = append(updaters, updater.Generic(extraFiles))
case "changelog":
updaters = append(updaters, updater.Changelog())
case "packagejson":
updaters = append(updaters, updater.PackageJson())
default:
return fmt.Errorf("unknown updater: %s", name)
}
}
releaserPleaser := rp.New(
f,
logger,
flagBranch,
conventionalcommits.NewParser(logger),
versioning.SemVer,
extraFiles,
updaters,
)
return releaserPleaser.Run(ctx)
},
}
switch flagForge {
case "gitlab":
logger.DebugContext(ctx, "using forge GitLab")
f, err = gitlab.New(logger, &gitlab.Options{
Options: forgeOptions,
Path: fmt.Sprintf("%s/%s", flagOwner, flagRepo),
})
if err != nil {
logger.ErrorContext(ctx, "failed to create client", "err", err)
return fmt.Errorf("failed to create gitlab client: %w", err)
}
case "github":
logger.DebugContext(ctx, "using forge GitHub")
f = github.New(logger, &github.Options{
Options: forgeOptions,
Owner: flagOwner,
Repo: flagRepo,
})
default:
return fmt.Errorf("unknown --forge: %s", flagForge)
}
cmd.PersistentFlags().StringVar(&flagForge, "forge", "", "")
cmd.PersistentFlags().StringVar(&flagBranch, "branch", "main", "")
cmd.PersistentFlags().StringVar(&flagOwner, "owner", "", "")
cmd.PersistentFlags().StringVar(&flagRepo, "repo", "", "")
cmd.PersistentFlags().StringVar(&flagExtraFiles, "extra-files", "", "")
cmd.PersistentFlags().StringSliceVar(&flagUpdaters, "updaters", []string{}, "")
extraFiles := parseExtraFiles(flagExtraFiles)
cmd.PersistentFlags().StringVar(&flagAPIURL, "api-url", "", "")
cmd.PersistentFlags().StringVar(&flagAPIToken, "api-token", "", "")
cmd.PersistentFlags().StringVar(&flagUsername, "username", "", "")
releaserPleaser := rp.New(
f,
logger,
flagBranch,
conventionalcommits.NewParser(logger),
versioning.SemVer,
extraFiles,
[]updater.NewUpdater{updater.Generic},
)
return releaserPleaser.Run(ctx)
return cmd
}
func parseExtraFiles(input string) []string {
@ -113,3 +156,26 @@ func parseExtraFiles(input string) []string {
return extraFiles
}
func parseUpdaters(input []string) []string {
names := []string{"changelog", "generic"}
for _, u := range input {
if u == "" {
continue
}
if strings.HasPrefix(u, "-") {
name := u[1:]
names = slices.DeleteFunc(names, func(existingName string) bool { return existingName == name })
} else {
names = append(names, u)
}
}
// Make sure we only have unique updaters
slices.Sort(names)
names = slices.Compact(names)
return names
}

View file

@ -57,3 +57,48 @@ dir/Chart.yaml"`,
})
}
}
func Test_parseUpdaters(t *testing.T) {
tests := []struct {
name string
input []string
want []string
}{
{
name: "empty",
input: []string{},
want: []string{"changelog", "generic"},
},
{
name: "remove defaults",
input: []string{"-changelog", "-generic"},
want: []string{},
},
{
name: "remove unknown is ignored",
input: []string{"-fooo"},
want: []string{"changelog", "generic"},
},
{
name: "add new entry",
input: []string{"bar"},
want: []string{"bar", "changelog", "generic"},
},
{
name: "duplicates are removed",
input: []string{"bar", "bar", "changelog"},
want: []string{"bar", "changelog", "generic"},
},
{
name: "remove empty entries",
input: []string{""},
want: []string{"changelog", "generic"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseUpdaters(tt.input)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -2,6 +2,7 @@ package main
import (
"github.com/apricote/releaser-pleaser/cmd/rp/cmd"
_ "github.com/apricote/releaser-pleaser/internal/log"
)
func main() {

2
codecov.yaml Normal file
View file

@ -0,0 +1,2 @@
ignore:
- "test"

View file

@ -10,6 +10,7 @@
# Explanation
- [Release Pull Request](explanation/release-pr.md)
- [Concurrency and Conflicts](explanation/concurrency-conflicts.md)
# Guides
@ -24,6 +25,7 @@
- [Pull Request Options](reference/pr-options.md)
- [GitHub Action](reference/github-action.md)
- [GitLab CI/CD Component](reference/gitlab-cicd-component.md)
- [Updaters](reference/updaters.md)
---

View file

@ -0,0 +1,65 @@
# Concurrency and Conflicts
## Why
`releaser-pleaser` works on the "shared global state" that is your project on GitHub/GitLab. Each execution reads from that state and makes changes to it. While `releaser-pleaser` is generally [idempotent](https://en.wikipedia.org/wiki/Idempotence), we still need to consider concurrent executions for two reasons: avoiding conflicts and saving resources.
### Avoiding conflicts
The [Release Pull Request](release-pr.md) is used by `releaser-pleaser` to show the current release. Users may update the PR description to add additional notes into the Changelog.
When `releaser-pleaser` is running while the user modifies the Release Pull Request description, `releaser-pleaser` may overwrite the description afterward based on its outdated local copy of the pull request.
### Saving resources
While `releaser-pleaser` is idempotent, there is no benefit to running it multiple times in parallel. In the best case, `releaser-pleaser` could be stopped as soon as a new "change" that is relevant to it comes in and restarts based on that new state.
## Measures taken
### Concurrency limits in CI environments
Our default configurations for [GitHub Actions](../tutorials/github.md) and [GitLab CI/CD](../tutorials/gitlab.md) try to limit concurrent `releaser-pleaser` jobs to a single one.
#### GitHub Actions
On GitHub Actions, we use a `concurrency.group` to restrict it to a single running job per repository.
GitHub cancels the currently running job and any other pending ones when a new one is started. This makes sure that `releaser-pleaser` always works with the latest state.
Users need to enable this in their workflow (included in our GitHub tutorial):
```yaml
concurrency:
group: releaser-pleaser
cancel-in-progress: true
```
#### GitLab
On GitLab CI/CD, we use a `resource_group: releaser-pleaser` in our GitLab CI/CD component to restrict it to a single running job per repository. This is part of the component YAML, so users do not need to set this manually.
There is no easy way to cancel the running job, so we let it proceed and rely on the other measures to safely handle the data. Users can enable "auto-cancel redundant pipelines" if they want, but should consider the ramifications for the rest of their CI carefully before doing so.
### Graceful shutdown
When GitHub Actions and GitLab CI/CD cancel jobs, they first sent a signal to the running process (`SIGINT` on GitHub and `SIGTERM` on GitLab). We listen for these signals and initiate a shutdown of the process. This helps save resources by shutting down as fast as possible, but in a controlled manner.
### Re-checking PR description for conflict
When `releaser-pleaser` prepares the Release Pull Request, the first step is to check if there is an existing PR already opened. It then reads from this PR to learn if the user modified the release in some way ([Release Notes](../guides/release-notes.md#for-the-release), [Pre-releases](../guides/pre-releases.md)). Based on this, it prepares the commit and the next iteration of the Release Pull Request description. The last step is to update the Release Pull Request description.
Depending on the time since the last release, a lot of API calls are made to learn about these changes; this can take between a few seconds and a few minutes. If the user makes any changes to the Release Pull Request in this time frame, they are not considered for the next iteration of the description. To make sure that we do not lose these changes, `releaser-pleaser` fetches the Release Pull Request description again right before updating it. In case it changed from the start of the process, the attempt is aborted, and the whole process is retried two times.
This does not fully eliminate the potential for data loss, but reduces the time frame from multiple seconds (up to minutes) to a few hundred milliseconds.
## Related Documentation
- **Explanation**
- [Release Pull Request](release-pr.md)
- **Guide**
- [Pre-releases](../guides/pre-releases.md)
- [Customizing Release Notes](../guides/release-notes.md)
- **Tutorial**
- [Getting started on GitHub](../tutorials/github.md)
- [Getting started on GitLab](../tutorials/gitlab.md)

View file

@ -43,17 +43,17 @@ The release pull request description has text fields where maintainers can add t
When you edit the description, make sure to put your desired content into the code blocks named `rp-prefix` and `rp-suffix`. Only the content of these blocks is considered.
> ```rp-prefix
> ~~~~rp-prefix
> ### Prefix
>
> This will be shown as the Prefix.
> ```
> ~~~~
>
> ```rp-suffix
> ~~~~rp-suffix
> ### Suffix
>
> This will be shown as the Suffix.
> ```
> ~~~~
To match the style of the auto-generated release notes, you should start any headings at level 3 (`### Title`).

View file

@ -10,7 +10,8 @@ In some situations it makes sense to have the current version committed in files
## Markers
The line that needs to be updated must have the marker `x-releaser-pleaser-version` somewhere after the version that should be updated.
The line that needs to be updated must have the marker
`x-releaser-pleaser-version` somewhere after the version that should be updated.
For example:
@ -28,7 +29,8 @@ You need to tell `releaser-pleaser` which files it should update. This happens t
### GitHub Action
In the GitHub Action you can set the `extra-files` input with a list of the files. They need to be formatted as a single multi-line string with one file path per line:
In the GitHub Action you can set the
`extra-files` input with a list of the files. They need to be formatted as a single multi-line string with one file path per line:
```yaml
jobs:
@ -44,7 +46,8 @@ jobs:
### GitLab CI/CD Component
In the GitLab CI/CD Component you can set the `extra-files` input with a list of files. They need to be formatted as a single multi-line string with one file path per line:
In the GitLab CI/CD Component you can set the
`extra-files` input with a list of files. They need to be formatted as a single multi-line string with one file path per line:
```yaml
include:
@ -61,3 +64,4 @@ include:
- **Reference**
- [GitHub Action](../reference/github-action.md#inputs)
- [GitLab CI/CD Component](../reference/gitlab-cicd-component.md#inputs)
- [Updaters](../reference/updaters.md#generic-updater)

View file

@ -8,17 +8,20 @@ The action is available as `apricote/releaser-pleaser` on GitHub.com.
The `apricote/releaser-pleaser` action is released together with `releaser-pleaser` and they share the version number.
The action does not support floating tags (e.g. `v1`) right now ([#31](https://github.com/apricote/releaser-pleaser/issues/31)). You have to use the full version or commit SHA instead: `apricote/releaser-pleaser@v0.2.0`.
The action does not support floating tags (e.g.
`v1`) right now ([#31](https://github.com/apricote/releaser-pleaser/issues/31)). You have to use the full version or commit SHA instead:
`apricote/releaser-pleaser@v0.2.0`.
## Inputs
The following inputs are supported by the `apricote/releaser-pleaser` GitHub Action.
| Input | Description | Default | Example |
| ------------- | :----------------------------------------------------- | --------------: | -------------------------------------------------------------------: |
| `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` | GitHub token for creating and updating release PRs | `$GITHUB_TOKEN` | `${{secrets.RELEASER_PLEASER_TOKEN}}` |
| `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| Input | Description | Default | Example |
|---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------:|---------------------------------------------------------------------:|
| `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` | GitHub token for creating and updating release PRs | `$GITHUB_TOKEN` | `${{secrets.RELEASER_PLEASER_TOKEN}}` |
| `extra-files` | List of files that are scanned for version references by the generic updater. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `updaters` | List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic | `""` | `-generic,packagejson` |
## Outputs

View file

@ -18,10 +18,11 @@ The component does not support floating tags (e.g.
The following inputs are supported by the component.
| Input | Description | Default | Example |
| ---------------------- | :-------------------------------------------------------- | ------: | -------------------------------------------------------------------: |
| `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` (**required**) | GitLab access token for creating and updating release PRs | | `$RELEASER_PLEASER_TOKEN` |
| `extra-files` | List of files that are scanned for version references. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `stage` | Stage the job runs in. Must exists. | `build` | `test` |
| `needs` | Other jobs the releaser-pleaser job depends on. | `[]` | <pre><code>- validate-foo<br>- prepare-bar</code></pre> |
| Input | Description | Default | Example |
|------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------:|---------------------------------------------------------------------:|
| `branch` | This branch is used as the target for releases. | `main` | `master` |
| `token` (**required**) | GitLab access token for creating and updating release PRs | | `$RELEASER_PLEASER_TOKEN` |
| `extra-files` | List of files that are scanned for version references by the generic updater. | `""` | <pre><code>version/version.go<br>deploy/deployment.yaml</code></pre> |
| `updaters` | List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic | `""` | `-generic,packagejson` |
| `stage` | Stage the job runs in. Must exists. | `build` | `test` |
| `needs` | Other jobs the releaser-pleaser job depends on. | `[]` | <pre><code>- validate-foo<br>- prepare-bar</code></pre> |

View file

@ -2,17 +2,21 @@
### Changelog
The Changelog is a file in the repository (`CHANGELOG.md`) that contains the [Release Notes](#release-notes) for every release of that repository. Usually, new releases are added at the top of the file.
The Changelog is a file in the repository (
`CHANGELOG.md`) that contains the [Release Notes](#release-notes) for every release of that repository. Usually, new releases are added at the top of the file.
### Conventional Commits
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) is a specification for commit messages. It is the only supported commit message schema in `releaser-pleaser`. Follow the link to learn more.
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) is a specification for commit messages. It is the only supported commit message schema in
`releaser-pleaser`. Follow the link to learn more.
### Forge
A **forge** is a web-based collaborative software platform for both developing and sharing computer applications.[^wp-forge]
A **forge
** is a web-based collaborative software platform for both developing and sharing computer applications.[^wp-forge]
Right now only **GitHub** is supported. We plan to support **GitLab** in the future ([#4](https://github.com/apricote/releaser-pleaser/issues/4)). For other forges like Forgejo or Gitea, please open an issue and submit a pull request.
Right now only **GitHub** is supported. We plan to support **GitLab
** in the future ([#4](https://github.com/apricote/releaser-pleaser/issues/4)). For other forges like Forgejo or Gitea, please open an issue and submit a pull request.
[^wp-forge]: Quote from [Wikipedia "Forge (software)"](<https://en.wikipedia.org/wiki/Forge_(software)>)
@ -24,7 +28,8 @@ In `releaser-pleaser` Markdown is used for most texts.
### Pre-release
Pre-releases are a concept of [SemVer](#semantic-versioning-semver). They follow the normal versioning schema but use a suffix out of `-alpha.X`, `-beta.X` and `-rc.X`.
Pre-releases are a concept of [SemVer](#semantic-versioning-semver). They follow the normal versioning schema but use a suffix out of
`-alpha.X`, `-beta.X` and `-rc.X`.
Pre-releases are not considered "stable" and are usually not recommended for most users.
@ -32,7 +37,9 @@ Learn more in the [Pre-releases](../guides/pre-releases.md) guide.
### Release Pull Request
A Release Pull Request is opened by `releaser-pleaser` whenever it finds releasable commits in your project. It proposes a new version number and the Changelog. Once it is merged, `releaser-pleaser` creates a matching release.
A Release Pull Request is opened by
`releaser-pleaser` whenever it finds releasable commits in your project. It proposes a new version number and the Changelog. Once it is merged,
`releaser-pleaser` creates a matching release.
Learn more in the [Release Pull Request](../explanation/release-pr.md) explanation.
@ -44,4 +51,11 @@ Learn more in the [Release Notes customization](../guides/release-notes.md) guid
### Semantic Versioning (SemVer)
[Semantic Versioning](https://semver.org/) is a specification for version numbers. It is the only supported versioning schema in `releaser-pleaser`. Follow the link to learn more.
[Semantic Versioning](https://semver.org/) is a specification for version numbers. It is the only supported versioning schema in
`releaser-pleaser`. Follow the link to learn more.
### Updater
Updaters can update or create files that will be included in [Release Pull Request](#release-pull-request). Examples of Updaters are
`changelog` for `CHANGELOG.md`, `generic` that can update arbitrary files and
`packagejson` that knows how to update Node.JS `package.json` files.

View file

@ -30,17 +30,17 @@ Any text in code blocks with these languages is being added to the start or end
**Examples**:
```rp-prefix
~~~~rp-prefix
#### Awesome new feature!
This text is at the start of the release notes.
```
~~~~
```rp-suffix
~~~~rp-suffix
#### Version Compatibility
And this at the end.
```
~~~~
### Status

View file

@ -0,0 +1,33 @@
# Updaters
There are different updater for different purposes available.
They each have a name and may be enabled by default. You can configure which updaters are used through the
`updaters` input on GitHub Actions and GitLab CI/CD. This is a comma-delimited list of updaters that should be enabled, for updaters that are enabled by default you can remove them by adding a minus before its name:
```
updaters: -generic,packagejson
```
## Changelog
- **Name**: `changelog`
- **Default**: enabled
This updater creates the `CHANGELOG.md` file and adds new release notes to it.
## Generic Updater
- **Name**: `generic`
- **Default**: enabled
This updater can update any file and only needs a marker on the line. It is enabled by default.
Learn more about this updater in ["Updating arbitrary files"](../guides/updating-arbitrary-files.md).
## Node.js `package.json` Updater
- **Name**: `packagejson`
- **Default**: disabled
This updater can update the `version` field in Node.js `package.json` files. The updater is disabled by default.

View file

@ -44,6 +44,10 @@ on:
- labeled
- unlabeled
concurrency:
group: releaser-pleaser
cancel-in-progress: true
jobs:
releaser-pleaser:
runs-on: ubuntu-latest

44
go.mod
View file

@ -1,36 +1,41 @@
module github.com/apricote/releaser-pleaser
go 1.23.2
go 1.24.0
toolchain go1.24.3
toolchain go1.25.5
require (
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0
github.com/blang/semver/v4 v4.0.0
github.com/go-git/go-git/v5 v5.16.0
github.com/google/go-github/v66 v66.0.0
github.com/go-git/go-billy/v5 v5.7.0
github.com/go-git/go-git/v5 v5.16.4
github.com/google/go-github/v74 v74.0.0
github.com/leodido/go-conventionalcommits v0.12.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/lmittmann/tint v1.1.2
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/teekennedy/goldmark-markdown v0.5.1
github.com/xanzy/go-gitlab v0.114.0
github.com/yuin/goldmark v1.7.12
github.com/yuin/goldmark v1.7.16
gitlab.com/gitlab-org/api/client-go v0.161.1
)
require (
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/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.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/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
@ -39,15 +44,16 @@ require (
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/time v0.12.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // 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

92
go.sum
View file

@ -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/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.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -27,32 +33,33 @@ 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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
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/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/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M=
github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4=
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
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-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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@ -68,6 +75,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-conventionalcommits v0.12.0 h1:pG01rl8Ze+mxnSSVB2wPdGASXyyU25EGwLUc0bWrmKc=
github.com/leodido/go-conventionalcommits v0.12.0/go.mod h1:DW+n8pQb5w/c7Vba7iGOMS3rkbPqykVlnrDykGjlsJM=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -92,63 +101,64 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/teekennedy/goldmark-markdown v0.5.1 h1:2lIlJ3AcIwaD1wFl4dflJSJFMhRTKEsEj+asVsu6M/0=
github.com/teekennedy/goldmark-markdown v0.5.1/go.mod h1:so260mNSPELuRyynZY18719dRYlD+OSnAovqsyrOMOM=
github.com/xanzy/go-gitlab v0.114.0 h1:0wQr/KBckwrZPfEMjRqpUz0HmsKKON9UhCYv9KDy19M=
github.com/xanzy/go-gitlab v0.114.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
gitlab.com/gitlab-org/api/client-go v0.161.1 h1:XX0EtVGL6cGEdNy9xnJ96CSciIzjCwAVsayItHY1YyU=
gitlab.com/gitlab-org/api/client-go v0.161.1/go.mod h1:YqKcnxyV9OPAL5U99mpwBVEgBPz1PK/3qwqq/3h6bao=
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.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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/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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View file

@ -3,9 +3,9 @@ package changelog
import (
"bytes"
_ "embed"
"html/template"
"log"
"log/slog"
"text/template"
"github.com/apricote/releaser-pleaser/internal/commitparser"
"github.com/apricote/releaser-pleaser/internal/markdown"

View file

@ -1,5 +1,5 @@
{{define "entry" -}}
- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}}
- {{ if .BreakingChange}}**BREAKING**: {{end}}{{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}}
{{ end }}
{{- if not .Formatting.HideVersionTitle }}

View file

@ -8,6 +8,7 @@ import (
"github.com/apricote/releaser-pleaser/internal/commitparser"
"github.com/apricote/releaser-pleaser/internal/git"
"github.com/apricote/releaser-pleaser/internal/testdata"
)
func ptr[T any](input T) *T {
@ -54,6 +55,23 @@ func Test_NewChangelogEntry(t *testing.T) {
want: "## [1.0.0](https://example.com/1.0.0)\n\n### Features\n\n- Foobar!\n",
wantErr: assert.NoError,
},
{
name: "single breaking change",
args: args{
analyzedCommits: []commitparser.AnalyzedCommit{
{
Commit: git.Commit{},
Type: "feat",
Description: "Foobar!",
BreakingChange: true,
},
},
version: "1.0.0",
link: "https://example.com/1.0.0",
},
want: "## [1.0.0](https://example.com/1.0.0)\n\n### Features\n\n- **BREAKING**: Foobar!\n",
wantErr: assert.NoError,
},
{
name: "single fix",
args: args{
@ -126,16 +144,9 @@ func Test_NewChangelogEntry(t *testing.T) {
},
version: "1.0.0",
link: "https://example.com/1.0.0",
prefix: "### Breaking Changes",
prefix: testdata.MustReadFileString(t, "prefix.txt"),
},
want: `## [1.0.0](https://example.com/1.0.0)
### Breaking Changes
### Bug Fixes
- Foobar!
`,
want: testdata.MustReadFileString(t, "changelog-entry-prefix.txt"),
wantErr: assert.NoError,
},
{
@ -150,18 +161,9 @@ func Test_NewChangelogEntry(t *testing.T) {
},
version: "1.0.0",
link: "https://example.com/1.0.0",
suffix: "### Compatibility\n\nThis version is compatible with flux-compensator v2.2 - v2.9.",
suffix: testdata.MustReadFileString(t, "suffix.txt"),
},
want: `## [1.0.0](https://example.com/1.0.0)
### Bug Fixes
- Foobar!
### Compatibility
This version is compatible with flux-compensator v2.2 - v2.9.
`,
want: testdata.MustReadFileString(t, "changelog-entry-suffix.txt"),
wantErr: assert.NoError,
},
}

View file

@ -17,6 +17,9 @@ type Forge interface {
GitAuth() transport.AuthMethod
// CommitAuthor returns the git author used for the release commit. It should be the user whose token is used to talk to the API.
CommitAuthor(context.Context) (git.Author, error)
// LatestTags returns the last stable tag created on the main branch. If there is a more recent pre-release tag,
// that is also returned. If no tag is found, it returns nil.
LatestTags(context.Context) (git.Releases, error)

View 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
}

View file

@ -12,7 +12,7 @@ import (
"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/google/go-github/v66/github"
"github.com/google/go-github/v74/github"
"github.com/apricote/releaser-pleaser/internal/forge"
"github.com/apricote/releaser-pleaser/internal/git"
@ -29,6 +29,13 @@ const (
EnvRepository = "GITHUB_REPOSITORY"
)
var (
gitHubActionsBotAuthor = git.Author{
Name: "github-actions[bot]",
Email: "41898282+github-actions[bot]@users.noreply.github.com",
}
)
var _ forge.Forge = &GitHub{}
type GitHub struct {
@ -61,6 +68,22 @@ func (g *GitHub) GitAuth() transport.AuthMethod {
}
}
func (g *GitHub) CommitAuthor(ctx context.Context) (git.Author, error) {
g.log.DebugContext(ctx, "getting commit author from current token user")
user, _, err := g.client.Users.Get(ctx, "")
if err != nil {
g.log.WarnContext(ctx, "failed to get commit author from API, using default github-actions[bot] user", "error", err)
return gitHubActionsBotAuthor, nil
}
return git.Author{
Name: user.GetName(),
Email: user.GetEmail(),
}, nil
}
func (g *GitHub) LatestTags(ctx context.Context) (git.Releases, error) {
g.log.DebugContext(ctx, "listing all tags in github repository")
@ -273,6 +296,13 @@ func (g *GitHub) PullRequestForBranch(ctx context.Context, branch string) (*rele
}
func (g *GitHub) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePullRequest) error {
// If the Pull Request is created without the labels releaser-pleaser will create a new PR in the run. The user may merge both and have duplicate entries in the changelog.
// We try to avoid this situation by checking for a cancelled context first, and then running both API calls without passing along any cancellations.
if ctx.Err() != nil {
return ctx.Err()
}
ctx = context.WithoutCancel(ctx)
ghPR, _, err := g.client.PullRequests.Create(
ctx, g.options.Owner, g.options.Repo,
&github.NewPullRequest{
@ -286,7 +316,6 @@ func (g *GitHub) CreatePullRequest(ctx context.Context, pr *releasepr.ReleasePul
return err
}
// TODO: String ID?
pr.ID = ghPR.GetNumber()
err = g.SetPullRequestLabels(ctx, pr, []releasepr.Label{}, pr.Labels)

View file

@ -11,7 +11,7 @@ import (
"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/xanzy/go-gitlab"
gitlab "gitlab.com/gitlab-org/api/client-go"
"github.com/apricote/releaser-pleaser/internal/forge"
"github.com/apricote/releaser-pleaser/internal/git"
@ -69,6 +69,22 @@ func (g *GitLab) GitAuth() transport.AuthMethod {
}
}
func (g *GitLab) CommitAuthor(ctx context.Context) (git.Author, error) {
g.log.DebugContext(ctx, "getting commit author from current token user")
user, _, err := g.client.Users.CurrentUser(gitlab.WithContext(ctx))
if err != nil {
return git.Author{}, err
}
// TODO: Return bot when nothing is returned?
return git.Author{
Name: user.Name,
Email: user.Email,
}, nil
}
func (g *GitLab) LatestTags(ctx context.Context) (git.Releases, error) {
g.log.DebugContext(ctx, "listing all tags in gitlab repository")
@ -174,10 +190,10 @@ func (g *GitLab) prForCommit(ctx context.Context, commit git.Commit) (*git.PullR
return nil, err
}
var mergeRequest *gitlab.MergeRequest
var mergeRequest *gitlab.BasicMergeRequest
for _, mr := range associatedMRs {
// We only look for the MR that has this commit set as the "merge/squash commit" => The result of squashing this branch onto main
if mr.MergeCommitSHA == commit.Hash || mr.SquashCommitSHA == commit.Hash {
if mr.MergeCommitSHA == commit.Hash || mr.SquashCommitSHA == commit.Hash || mr.SHA == commit.Hash {
mergeRequest = mr
break
}
@ -315,7 +331,7 @@ func (g *GitLab) ClosePullRequest(ctx context.Context, pr *releasepr.ReleasePull
}
func (g *GitLab) PendingReleases(ctx context.Context, pendingLabel releasepr.Label) ([]*releasepr.ReleasePullRequest, error) {
glMRs, err := all(func(listOptions gitlab.ListOptions) ([]*gitlab.MergeRequest, *gitlab.Response, error) {
glMRs, err := all(func(listOptions gitlab.ListOptions) ([]*gitlab.BasicMergeRequest, *gitlab.Response, error) {
return g.client.MergeRequests.ListMergeRequests(&gitlab.ListMergeRequestsOptions{
State: pointer.Pointer(PRStateMerged),
Labels: &gitlab.LabelOptions{pendingLabel.Name},
@ -369,7 +385,7 @@ func all[T any](f func(listOptions gitlab.ListOptions) ([]T, *gitlab.Response, e
}
}
func gitlabMRToPullRequest(pr *gitlab.MergeRequest) *git.PullRequest {
func gitlabMRToPullRequest(pr *gitlab.BasicMergeRequest) *git.PullRequest {
return &git.PullRequest{
ID: pr.IID,
Title: pr.Title,
@ -377,7 +393,7 @@ func gitlabMRToPullRequest(pr *gitlab.MergeRequest) *git.PullRequest {
}
}
func gitlabMRToReleasePullRequest(pr *gitlab.MergeRequest) *releasepr.ReleasePullRequest {
func gitlabMRToReleasePullRequest(pr *gitlab.BasicMergeRequest) *releasepr.ReleasePullRequest {
labels := make([]releasepr.Label, 0, len(pr.Labels))
for _, labelName := range pr.Labels {
if i := slices.IndexFunc(releasepr.KnownLabels, func(label releasepr.Label) bool {
@ -387,12 +403,15 @@ func gitlabMRToReleasePullRequest(pr *gitlab.MergeRequest) *releasepr.ReleasePul
}
}
// Commit SHA is saved in either [MergeCommitSHA] or [SquashCommitSHA] depending on which merge method was used.
// Commit SHA is saved in either [MergeCommitSHA], [SquashCommitSHA] or [SHA] depending on which merge method was used.
var releaseCommit *git.Commit
if pr.MergeCommitSHA != "" {
switch {
case pr.MergeCommitSHA != "":
releaseCommit = &git.Commit{Hash: pr.MergeCommitSHA}
} else if pr.SquashCommitSHA != "" {
case pr.SquashCommitSHA != "":
releaseCommit = &git.Commit{Hash: pr.SquashCommitSHA}
case pr.MergedAt != nil && pr.SHA != "":
releaseCommit = &git.Commit{Hash: pr.SHA}
}
return &releasepr.ReleasePullRequest{

View file

@ -13,8 +13,6 @@ import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/apricote/releaser-pleaser/internal/updater"
)
const (
@ -45,6 +43,27 @@ type Releases struct {
Stable *Tag
}
type Author struct {
Name string
Email string
}
func (a Author) signature(when time.Time) *object.Signature {
return &object.Signature{
Name: a.Name,
Email: a.Email,
When: when,
}
}
func (a Author) String() string {
return fmt.Sprintf("%s <%s>", a.Name, a.Email)
}
var (
committer = Author{Name: "releaser-pleaser", Email: ""}
)
func CloneRepo(ctx context.Context, logger *slog.Logger, cloneURL, branch string, auth transport.AuthMethod) (*Repository, error) {
dir, err := os.MkdirTemp("", "releaser-pleaser.*")
if err != nil {
@ -98,7 +117,7 @@ func (r *Repository) Checkout(_ context.Context, branch string) error {
return nil
}
func (r *Repository) UpdateFile(_ context.Context, path string, create bool, updaters []updater.Updater) error {
func (r *Repository) UpdateFile(_ context.Context, path string, create bool, updateHook func(string) (string, error)) error {
worktree, err := r.r.Worktree()
if err != nil {
return err
@ -113,20 +132,16 @@ func (r *Repository) UpdateFile(_ context.Context, path string, create bool, upd
if err != nil {
return err
}
defer file.Close()
defer file.Close() //nolint:errcheck
content, err := io.ReadAll(file)
if err != nil {
return err
}
updatedContent := string(content)
for _, update := range updaters {
updatedContent, err = update(updatedContent)
if err != nil {
return fmt.Errorf("failed to run updater on file %s", path)
}
updatedContent, err := updateHook(string(content))
if err != nil {
return fmt.Errorf("failed to run update hook on file %s", path)
}
err = file.Truncate(0)
@ -150,15 +165,17 @@ func (r *Repository) UpdateFile(_ context.Context, path string, create bool, upd
return nil
}
func (r *Repository) Commit(_ context.Context, message string) (Commit, error) {
func (r *Repository) Commit(_ context.Context, message string, author Author) (Commit, error) {
worktree, err := r.r.Worktree()
if err != nil {
return Commit{}, err
}
now := time.Now()
releaseCommitHash, err := worktree.Commit(message, &git.CommitOptions{
Author: signature(),
Committer: signature(),
Author: author.signature(now),
Committer: committer.signature(now),
})
if err != nil {
return Commit{}, fmt.Errorf("failed to commit changes: %w", err)
@ -170,8 +187,27 @@ func (r *Repository) Commit(_ context.Context, message string) (Commit, error) {
}, nil
}
func (r *Repository) HasChangesWithRemote(ctx context.Context, branch string) (bool, error) {
remoteRef, err := r.r.Reference(plumbing.NewRemoteReferenceName(remoteName, branch), false)
// HasChangesWithRemote checks if the following two diffs are equal:
//
// - **Local**: remote/main..branch
// - **Remote**: (git merge-base remote/main remote/branch)..remote/branch
//
// This is done to avoid pushing when the only change would be a rebase of remote/branch onto the current remote/main.
func (r *Repository) HasChangesWithRemote(ctx context.Context, mainBranch, prBranch string) (bool, error) {
return r.hasChangesWithRemote(ctx,
plumbing.NewRemoteReferenceName(remoteName, mainBranch),
plumbing.NewBranchReferenceName(prBranch),
plumbing.NewRemoteReferenceName(remoteName, prBranch),
)
}
func (r *Repository) hasChangesWithRemote(ctx context.Context, mainBranchRef, localPRBranchRef, remotePRBranchRef plumbing.ReferenceName) (bool, error) {
commitOnRemoteMain, err := r.commitFromRef(mainBranchRef)
if err != nil {
return false, err
}
commitOnRemotePRBranch, err := r.commitFromRef(remotePRBranchRef)
if err != nil {
if err.Error() == "reference not found" {
// No remote branch means that there are changes
@ -181,29 +217,60 @@ func (r *Repository) HasChangesWithRemote(ctx context.Context, branch string) (b
return false, err
}
remoteCommit, err := r.r.CommitObject(remoteRef.Hash())
currentRemotePRMergeBase, err := r.mergeBase(commitOnRemoteMain, commitOnRemotePRBranch)
if err != nil {
return false, err
}
if currentRemotePRMergeBase == nil {
// If there is no merge base something weird has happened with the
// remote main branch, and we should definitely push updates.
return false, nil
}
remoteDiff, err := currentRemotePRMergeBase.PatchContext(ctx, commitOnRemotePRBranch)
if err != nil {
return false, err
}
localRef, err := r.r.Reference(plumbing.NewBranchReferenceName(branch), false)
commitOnLocalPRBranch, err := r.commitFromRef(localPRBranchRef)
if err != nil {
return false, err
}
localCommit, err := r.r.CommitObject(localRef.Hash())
localDiff, err := commitOnRemoteMain.PatchContext(ctx, commitOnLocalPRBranch)
if err != nil {
return false, err
}
diff, err := localCommit.PatchContext(ctx, remoteCommit)
return remoteDiff.String() != localDiff.String(), nil
}
func (r *Repository) commitFromRef(refName plumbing.ReferenceName) (*object.Commit, error) {
ref, err := r.r.Reference(refName, false)
if err != nil {
return false, err
return nil, err
}
hasChanges := len(diff.FilePatches()) > 0
commit, err := r.r.CommitObject(ref.Hash())
if err != nil {
return nil, err
}
return hasChanges, nil
return commit, nil
}
func (r *Repository) mergeBase(a, b *object.Commit) (*object.Commit, error) {
mergeBases, err := a.MergeBase(b)
if err != nil {
return nil, err
}
if len(mergeBases) == 0 {
return nil, nil
}
// :shrug: We dont really care which commit we pick, at worst we do an unnecessary push.
return mergeBases[0], nil
}
func (r *Repository) ForcePush(ctx context.Context, branch string) error {
@ -223,11 +290,3 @@ func (r *Repository) ForcePush(ctx context.Context, branch string) error {
Auth: r.auth,
})
}
func signature() *object.Signature {
return &object.Signature{
Name: "releaser-pleaser",
Email: "",
When: time.Now(),
}
}

174
internal/git/git_test.go Normal file
View file

@ -0,0 +1,174 @@
package git
import (
"context"
"reflect"
"strconv"
"testing"
"time"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
)
func TestAuthor_signature(t *testing.T) {
now := time.Now()
tests := []struct {
author Author
want *object.Signature
}{
{author: Author{Name: "foo", Email: "bar@example.com"}, want: &object.Signature{Name: "foo", Email: "bar@example.com", When: now}},
{author: Author{Name: "bar", Email: "foo@example.com"}, want: &object.Signature{Name: "bar", Email: "foo@example.com", When: now}},
}
for i, tt := range tests {
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
if got := tt.author.signature(now); !reflect.DeepEqual(got, tt.want) {
t.Errorf("signature() = %v, want %v", got, tt.want)
}
})
}
}
func TestAuthor_String(t *testing.T) {
tests := []struct {
author Author
want string
}{
{author: Author{Name: "foo", Email: "bar@example.com"}, want: "foo <bar@example.com>"},
{author: Author{Name: "bar", Email: "foo@example.com"}, want: "bar <foo@example.com>"},
}
for i, tt := range tests {
t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
if got := tt.author.String(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
const testMainBranch = "main"
const testPRBranch = "releaser-pleaser"
func TestRepository_HasChangesWithRemote(t *testing.T) {
// go-git/v5 has a bug where it tries to delete the repo root dir (".") multiple times if there is no file left in it.
// this happens while switching branches in worktree.go rmFileAndDirsIfEmpty.
// TODO: Fix bug upstream
// For now I just make sure that there is always at least one file left in the dir by adding an empty "README.md" in the test util.
mainBranchRef := plumbing.NewBranchReferenceName(testMainBranch)
localPRBranchRef := plumbing.NewBranchReferenceName(testPRBranch)
remotePRBranchRef := plumbing.NewBranchReferenceName("remote/" + testPRBranch)
tests := []struct {
name string
repo TestRepo
want bool
wantErr assert.ErrorAssertionFunc
}{
{
name: "no remote pr branch",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(localPRBranchRef),
WithFile("VERSION", "v1.1.0"),
),
),
want: true,
wantErr: assert.NoError,
},
{
name: "remote pr branch matches local",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(remotePRBranchRef),
WithFile("VERSION", "v1.1.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(localPRBranchRef),
WithFile("VERSION", "v1.1.0"),
),
),
want: false,
wantErr: assert.NoError,
},
{
name: "remote pr only needs rebase",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(remotePRBranchRef),
WithFile("VERSION", "v1.1.0"),
),
WithCommit(
"feat: new feature on remote",
OnBranch(mainBranchRef),
WithFile("feature", "yes"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(localPRBranchRef),
WithFile("VERSION", "v1.1.0"),
),
),
want: false,
wantErr: assert.NoError,
},
{
name: "needs update",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(remotePRBranchRef),
WithFile("VERSION", "v1.1.0"),
WithFile("CHANGELOG.md", "Foo"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(mainBranchRef),
AsNewBranch(localPRBranchRef),
WithFile("VERSION", "v1.1.0"),
WithFile("CHANGELOG.md", "FooBar"),
),
),
want: true,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := tt.repo(t)
got, err := repo.hasChangesWithRemote(context.Background(), mainBranchRef, localPRBranchRef, remotePRBranchRef)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, got)
})
}
}

189
internal/git/util_test.go Normal file
View file

@ -0,0 +1,189 @@
package git
import (
"fmt"
"io"
"log/slog"
"os"
"testing"
"time"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/stretchr/testify/require"
)
var (
author = &object.Signature{
Name: "releaser-pleaser",
When: time.Date(2020, 01, 01, 01, 01, 01, 01, time.UTC),
}
)
type CommitOption func(*commitOptions)
type commitOptions struct {
cleanFiles bool
files []commitFile
tags []string
newRef plumbing.ReferenceName
parentRef plumbing.ReferenceName
}
type commitFile struct {
path string
content string
}
type TestCommit func(*testing.T, *Repository) error
type TestRepo func(*testing.T) *Repository
func WithCommit(message string, options ...CommitOption) TestCommit {
return func(t *testing.T, repo *Repository) error {
t.Helper()
require.NotEmpty(t, message, "commit message is required")
opts := &commitOptions{}
for _, opt := range options {
opt(opts)
}
wt, err := repo.r.Worktree()
require.NoError(t, err)
if opts.parentRef != "" {
checkoutOptions := &git.CheckoutOptions{}
if opts.newRef != "" {
parentRef, err := repo.r.Reference(opts.parentRef, false)
require.NoError(t, err)
checkoutOptions.Create = true
checkoutOptions.Hash = parentRef.Hash()
checkoutOptions.Branch = opts.newRef
} else {
checkoutOptions.Branch = opts.parentRef
}
err = wt.Checkout(checkoutOptions)
require.NoError(t, err)
}
// Yeet all files
if opts.cleanFiles {
files, err := wt.Filesystem.ReadDir(".")
require.NoError(t, err, "failed to get current files")
for _, fileInfo := range files {
err = wt.Filesystem.Remove(fileInfo.Name())
require.NoError(t, err, "failed to remove file %q", fileInfo.Name())
}
}
// Create new files
for _, fileInfo := range opts.files {
file, err := wt.Filesystem.Create(fileInfo.path)
require.NoError(t, err, "failed to create file %q", fileInfo.path)
_, err = file.Write([]byte(fileInfo.content))
_ = file.Close()
require.NoError(t, err, "failed to write content to file %q", fileInfo.path)
_, err = wt.Add(fileInfo.path)
require.NoError(t, err, "failed to stage changes to file %q", fileInfo.path)
}
// Commit
commitHash, err := wt.Commit(message, &git.CommitOptions{
All: true,
AllowEmptyCommits: true,
Author: author,
Committer: author,
})
require.NoError(t, err, "failed to commit")
// Create tags
for _, tagName := range opts.tags {
_, err = repo.r.CreateTag(tagName, commitHash, nil)
require.NoError(t, err, "failed to create tag %q", tagName)
}
return nil
}
}
func WithFile(path, content string) CommitOption {
return func(opts *commitOptions) {
opts.files = append(opts.files, commitFile{path: path, content: content})
}
}
// WithCleanFiles removes all previous files from the repo. Make sure to leave at least one file in the root
// directory when switching branches!
func WithCleanFiles() CommitOption {
return func(opts *commitOptions) {
opts.cleanFiles = true
}
}
func AsNewBranch(ref plumbing.ReferenceName) CommitOption {
return func(opts *commitOptions) {
opts.newRef = ref
}
}
func OnBranch(ref plumbing.ReferenceName) CommitOption {
return func(opts *commitOptions) {
opts.parentRef = ref
}
}
func WithTag(name string) CommitOption {
return func(opts *commitOptions) {
opts.tags = append(opts.tags, name)
}
}
// Can be useful to debug git issues by using it in a terminal
const useOnDiskTestRepository = false
func WithTestRepo(commits ...TestCommit) TestRepo {
return func(t *testing.T) *Repository {
t.Helper()
repo := &Repository{
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}
var err error
initOptions := git.InitOptions{DefaultBranch: plumbing.Main}
if useOnDiskTestRepository {
dir, err := os.MkdirTemp(os.TempDir(), "rp-test-repo-")
require.NoError(t, err, "failed to create temp directory")
repo.r, err = git.PlainInitWithOptions(dir, &git.PlainInitOptions{InitOptions: initOptions})
require.NoError(t, err, "failed to create fs repository")
fmt.Printf("using temp directory: %s", dir)
} else {
repo.r, err = git.InitWithOptions(memory.NewStorage(), memfs.New(), initOptions)
require.NoError(t, err, "failed to create in-memory repository")
}
// Make initial commit
err = WithCommit("chore: init", WithFile("README.md", "# git test util"))(t, repo)
require.NoError(t, err, "failed to create init commit")
for i, commit := range commits {
err = commit(t, repo)
require.NoError(t, err, "failed to create commit %d", i)
}
return repo
}
}

23
internal/log/log.go Normal file
View file

@ -0,0 +1,23 @@
package log
import (
"io"
"log/slog"
"os"
"time"
"github.com/lmittmann/tint"
)
func GetLogger(w io.Writer) *slog.Logger {
return slog.New(
tint.NewHandler(w, &tint.Options{
Level: slog.LevelDebug,
TimeFormat: time.RFC3339,
}),
)
}
func init() {
slog.SetDefault(GetLogger(os.Stderr))
}

View file

@ -98,7 +98,7 @@ func (pr *ReleasePullRequest) parseVersioningFlags(overrides ReleaseOverrides) R
overrides.NextVersionType = versioning.NextVersionTypeAlpha
case LabelReleasePending, LabelReleaseTagged:
// These labels have no effect on the versioning.
break
continue
}
}

View file

@ -15,18 +15,18 @@ If you want to modify the proposed release, add you overrides here. You can lear
This will be added to the start of the release notes.
```rp-prefix
~~~~rp-prefix
{{- if .Overrides.Prefix }}
{{ .Overrides.Prefix }}{{ end }}
```
~~~~
### Suffix / End
This will be added to the end of the release notes.
```rp-suffix
~~~~rp-suffix
{{- if .Overrides.Suffix }}
{{ .Overrides.Suffix }}{{ end }}
```
~~~~
</details>

View file

@ -1,12 +1,12 @@
package releasepr
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/apricote/releaser-pleaser/internal/git"
"github.com/apricote/releaser-pleaser/internal/testdata"
"github.com/apricote/releaser-pleaser/internal/versioning"
)
@ -38,20 +38,24 @@ func TestReleasePullRequest_GetOverrides(t *testing.T) {
name: "prefix in description",
pr: ReleasePullRequest{
PullRequest: git.PullRequest{
Description: "```rp-prefix\n## Foo\n\n- Cool thing\n```",
Description: testdata.MustReadFileString(t, "description-prefix.txt"),
},
},
want: ReleaseOverrides{Prefix: "## Foo\n\n- Cool thing"},
want: ReleaseOverrides{
Prefix: testdata.MustReadFileString(t, "prefix.txt"),
},
wantErr: assert.NoError,
},
{
name: "suffix in description",
pr: ReleasePullRequest{
PullRequest: git.PullRequest{
Description: "```rp-suffix\n## Compatibility\n\nNo compatibility guarantees.\n```",
Description: testdata.MustReadFileString(t, "description-suffix.txt"),
},
},
want: ReleaseOverrides{Suffix: "## Compatibility\n\nNo compatibility guarantees."},
want: ReleaseOverrides{
Suffix: testdata.MustReadFileString(t, "suffix.txt"),
},
wantErr: assert.NoError,
},
}
@ -59,7 +63,7 @@ func TestReleasePullRequest_GetOverrides(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.pr.GetOverrides()
if !tt.wantErr(t, err, fmt.Sprintf("GetOverrides()")) {
if !tt.wantErr(t, err, "GetOverrides()") {
return
}
assert.Equalf(t, tt.want, got, "GetOverrides()")
@ -81,30 +85,10 @@ func TestReleasePullRequest_ChangelogText(t *testing.T) {
wantErr: assert.NoError,
},
{
name: "with section",
description: `# Foobar
<!-- section-start changelog -->
This is the changelog
## Awesome
### New
#### Changes
<!-- section-end changelog -->
Suffix Things
`,
want: `This is the changelog
## Awesome
### New
#### Changes
`,
wantErr: assert.NoError,
name: "with section",
description: testdata.MustReadFileString(t, "changelog.txt"),
want: testdata.MustReadFileString(t, "changelog-content.txt"),
wantErr: assert.NoError,
},
}
for _, tt := range tests {
@ -115,7 +99,7 @@ Suffix Things
},
}
got, err := pr.ChangelogText()
if !tt.wantErr(t, err, fmt.Sprintf("ChangelogText()")) {
if !tt.wantErr(t, err, "ChangelogText()") {
return
}
assert.Equalf(t, tt.want, got, "ChangelogText()")
@ -179,75 +163,17 @@ func TestReleasePullRequest_SetDescription(t *testing.T) {
name: "no overrides",
changelogEntry: `## v1.0.0`,
overrides: ReleaseOverrides{},
want: `<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
` + "```" + `rp-prefix
` + "```" + `
### Suffix / End
This will be added to the end of the release notes.
` + "```" + `rp-suffix
` + "```" + `
</details>
`,
wantErr: assert.NoError,
want: testdata.MustReadFileString(t, "description-no-overrides.txt"),
wantErr: assert.NoError,
},
{
name: "existing overrides",
changelogEntry: `## v1.0.0`,
overrides: ReleaseOverrides{
Prefix: "This release is awesome!",
Suffix: "Fooo",
Prefix: testdata.MustReadFileString(t, "prefix.txt"),
Suffix: testdata.MustReadFileString(t, "suffix.txt"),
},
want: `<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
` + "```" + `rp-prefix
This release is awesome!
` + "```" + `
### Suffix / End
This will be added to the end of the release notes.
` + "```" + `rp-suffix
Fooo
` + "```" + `
</details>
`,
want: testdata.MustReadFileString(t, "description-overrides.txt"),
wantErr: assert.NoError,
},
}

View file

@ -0,0 +1,7 @@
This is the changelog
## Awesome
### New
#### Changes

View file

@ -0,0 +1,19 @@
## [1.0.0](https://example.com/1.0.0)
## Foo
- Cool thing
```go
// Some code example
func IsPositive(number int) error {
if number < 0 {
return fmt.Errorf("number %d is negative", number)
}
return nil
}
```
### Bug Fixes
- Foobar!

View file

@ -0,0 +1,9 @@
## [1.0.0](https://example.com/1.0.0)
### Bug Fixes
- Foobar!
## Compatibility
This version is compatible with flux-compensator v2.2 - v2.9.

13
internal/testdata/changelog.txt vendored Normal file
View file

@ -0,0 +1,13 @@
# Foobar
<!-- section-start changelog -->
This is the changelog
## Awesome
### New
#### Changes
<!-- section-end changelog -->
Suffix Things

View file

@ -0,0 +1,28 @@
<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
~~~~rp-prefix
~~~~
### Suffix / End
This will be added to the end of the release notes.
~~~~rp-suffix
~~~~
</details>

View file

@ -0,0 +1,44 @@
<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
~~~~rp-prefix
## Foo
- Cool thing
```go
// Some code example
func IsPositive(number int) error {
if number < 0 {
return fmt.Errorf("number %d is negative", number)
}
return nil
}
```
~~~~
### Suffix / End
This will be added to the end of the release notes.
~~~~rp-suffix
## Compatibility
This version is compatible with flux-compensator v2.2 - v2.9.
~~~~
</details>

View file

@ -0,0 +1,41 @@
<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
~~~~rp-prefix
## Foo
- Cool thing
```go
// Some code example
func IsPositive(number int) error {
if number < 0 {
return fmt.Errorf("number %d is negative", number)
}
return nil
}
```
~~~~
### Suffix / End
This will be added to the end of the release notes.
~~~~rp-suffix
~~~~
</details>

View file

@ -0,0 +1,31 @@
<!-- section-start changelog -->
## v1.0.0
<!-- section-end changelog -->
---
<details>
<summary><h4>PR by <a href="https://github.com/apricote/releaser-pleaser">releaser-pleaser</a> 🤖</h4></summary>
If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs.
## Release Notes
### Prefix / Start
This will be added to the start of the release notes.
~~~~rp-prefix
~~~~
### Suffix / End
This will be added to the end of the release notes.
~~~~rp-suffix
## Compatibility
This version is compatible with flux-compensator v2.2 - v2.9.
~~~~
</details>

13
internal/testdata/prefix.txt vendored Normal file
View file

@ -0,0 +1,13 @@
## Foo
- Cool thing
```go
// Some code example
func IsPositive(number int) error {
if number < 0 {
return fmt.Errorf("number %d is negative", number)
}
return nil
}
```

3
internal/testdata/suffix.txt vendored Normal file
View file

@ -0,0 +1,3 @@
## Compatibility
This version is compatible with flux-compensator v2.2 - v2.9.

19
internal/testdata/testdata.go vendored Normal file
View file

@ -0,0 +1,19 @@
package testdata
import (
"embed"
"testing"
)
//go:embed *.txt
var testdata embed.FS
func MustReadFileString(t *testing.T, name string) string {
t.Helper()
content, err := testdata.ReadFile(name)
if err != nil {
t.Fatal(err)
}
return string(content)
}

View file

@ -14,7 +14,22 @@ var (
ChangelogUpdaterHeaderRegex = regexp.MustCompile(`^# Changelog\n`)
)
func Changelog(info ReleaseInfo) Updater {
func Changelog() Updater {
return changelog{}
}
type changelog struct {
}
func (c changelog) Files() []string {
return []string{ChangelogFile}
}
func (c changelog) CreateNewFiles() bool {
return true
}
func (c changelog) Update(info ReleaseInfo) func(content string) (string, error) {
return func(content string) (string, error) {
headerIndex := ChangelogUpdaterHeaderRegex.FindStringIndex(content)
if headerIndex == nil && len(content) != 0 {

View file

@ -6,7 +6,15 @@ import (
"github.com/stretchr/testify/assert"
)
func TestChangelogUpdater_UpdateContent(t *testing.T) {
func TestChangelogUpdater_Files(t *testing.T) {
assert.Equal(t, []string{"CHANGELOG.md"}, Changelog().Files())
}
func TestChangelogUpdater_CreateNewFiles(t *testing.T) {
assert.True(t, Changelog().CreateNewFiles())
}
func TestChangelogUpdater_Update(t *testing.T) {
tests := []updaterTestCase{
{
name: "empty file",
@ -54,7 +62,7 @@ func TestChangelogUpdater_UpdateContent(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, Changelog, tt)
runUpdaterTest(t, Changelog(), tt)
})
}
}

View file

@ -7,7 +7,25 @@ import (
var GenericUpdaterSemVerRegex = regexp.MustCompile(`\d+\.\d+\.\d+(-[\w.]+)?(.*x-releaser-pleaser-version)`)
func Generic(info ReleaseInfo) Updater {
func Generic(files []string) Updater {
return generic{
files: files,
}
}
type generic struct {
files []string
}
func (g generic) Files() []string {
return g.files
}
func (g generic) CreateNewFiles() bool {
return false
}
func (g generic) Update(info ReleaseInfo) func(content string) (string, error) {
return func(content string) (string, error) {
// We strip the "v" prefix to avoid adding/removing it from the users input.
version := strings.TrimPrefix(info.Version, "v")

View file

@ -6,7 +6,15 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGenericUpdater_UpdateContent(t *testing.T) {
func TestGenericUpdater_Files(t *testing.T) {
assert.Equal(t, []string{"foo.bar", "version.txt"}, Generic([]string{"foo.bar", "version.txt"}).Files())
}
func TestGenericUpdater_CreateNewFiles(t *testing.T) {
assert.False(t, Generic([]string{}).CreateNewFiles())
}
func TestGenericUpdater_Update(t *testing.T) {
tests := []updaterTestCase{
{
name: "single line",
@ -47,7 +55,7 @@ func TestGenericUpdater_UpdateContent(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, Generic, tt)
runUpdaterTest(t, Generic([]string{"version.txt"}), tt)
})
}
}

View file

@ -0,0 +1,39 @@
package updater
import (
"regexp"
"strings"
)
// PackageJson creates an updater that modifies the version field in package.json files
func PackageJson() Updater {
return packagejson{}
}
type packagejson struct{}
func (p packagejson) Files() []string {
return []string{"package.json"}
}
func (p packagejson) CreateNewFiles() bool {
return false
}
func (p packagejson) Update(info ReleaseInfo) func(content string) (string, error) {
return func(content string) (string, error) {
// We strip the "v" prefix to match npm versioning convention
version := strings.TrimPrefix(info.Version, "v")
// Regex to match "version": "..." with flexible whitespace and quote styles
versionRegex := regexp.MustCompile(`("version"\s*:\s*)"[^"]*"`)
// Check if the file contains a version field
if !versionRegex.MatchString(content) {
return content, nil
}
// Replace the version value while preserving the original formatting
return versionRegex.ReplaceAllString(content, `${1}"`+version+`"`), nil
}
}

View file

@ -0,0 +1,62 @@
package updater
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPackageJsonUpdater_Files(t *testing.T) {
assert.Equal(t, []string{"package.json"}, PackageJson().Files())
}
func TestPackageJsonUpdater_CreateNewFiles(t *testing.T) {
assert.False(t, PackageJson().CreateNewFiles())
}
func TestPackageJsonUpdater_Update(t *testing.T) {
tests := []updaterTestCase{
{
name: "simple package.json",
content: `{"name":"test","version":"1.0.0"}`,
info: ReleaseInfo{
Version: "v2.0.5",
},
want: `{"name":"test","version":"2.0.5"}`,
wantErr: assert.NoError,
},
{
name: "complex package.json",
content: "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n \"foo\": \"^1.0.0\"\n }\n}",
info: ReleaseInfo{
Version: "v2.0.0",
},
want: "{\n \"name\": \"test\",\n \"version\": \"2.0.0\",\n \"dependencies\": {\n \"foo\": \"^1.0.0\"\n }\n}",
wantErr: assert.NoError,
},
{
name: "invalid json",
content: `not json`,
info: ReleaseInfo{
Version: "v2.0.0",
},
want: `not json`,
wantErr: assert.NoError,
},
{
name: "json without version",
content: `{"name":"test"}`,
info: ReleaseInfo{
Version: "v2.0.0",
},
want: `{"name":"test"}`,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runUpdaterTest(t, PackageJson(), tt)
})
}
}

View file

@ -5,7 +5,11 @@ type ReleaseInfo struct {
ChangelogEntry string
}
type Updater func(string) (string, error)
type Updater interface {
Files() []string
CreateNewFiles() bool
Update(info ReleaseInfo) func(content string) (string, error)
}
type NewUpdater func(ReleaseInfo) Updater

View file

@ -15,10 +15,10 @@ type updaterTestCase struct {
wantErr assert.ErrorAssertionFunc
}
func runUpdaterTest(t *testing.T, constructor NewUpdater, tt updaterTestCase) {
func runUpdaterTest(t *testing.T, u Updater, tt updaterTestCase) {
t.Helper()
got, err := constructor(tt.info)(tt.content)
got, err := u.Update(tt.info)(tt.content)
if !tt.wantErr(t, err, fmt.Sprintf("Updater(%v, %v)", tt.content, tt.info)) {
return
}

25
mise.toml Normal file
View file

@ -0,0 +1,25 @@
[tools]
go = "1.25.5"
golangci-lint = "2.8.0"
goreleaser = "v2.9.0"
mdbook = "v0.5.2" # renovate: datasource=github-releases depName=rust-lang/mdbook
ko = "v0.18.1" # renovate: datasource=github-releases depName=ko-build/ko
[settings]
# Experimental features are needed for the Go backend
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"

View file

@ -2,6 +2,7 @@ package rp
import (
"context"
"errors"
"fmt"
"log/slog"
@ -18,6 +19,14 @@ const (
PullRequestBranchFormat = "releaser-pleaser--branches--%s"
)
const (
PullRequestConflictAttempts = 3
)
var (
ErrorPullRequestConflict = errors.New("conflict: pull request description was changed while releaser-pleaser was running")
)
type ReleaserPleaser struct {
forge forge.Forge
logger *slog.Logger
@ -25,10 +34,10 @@ type ReleaserPleaser struct {
commitParser commitparser.CommitParser
versioning versioning.Strategy
extraFiles []string
updaters []updater.NewUpdater
updaters []updater.Updater
}
func New(forge forge.Forge, logger *slog.Logger, targetBranch string, commitParser commitparser.CommitParser, versioningStrategy versioning.Strategy, extraFiles []string, updaters []updater.NewUpdater) *ReleaserPleaser {
func New(forge forge.Forge, logger *slog.Logger, targetBranch string, commitParser commitparser.CommitParser, versioningStrategy versioning.Strategy, extraFiles []string, updaters []updater.Updater) *ReleaserPleaser {
return &ReleaserPleaser{
forge: forge,
logger: logger,
@ -57,7 +66,7 @@ func (rp *ReleaserPleaser) Run(ctx context.Context) error {
return fmt.Errorf("failed to create pending releases: %w", err)
}
err = rp.runReconcileReleasePR(ctx)
err = rp.runReconcileReleasePRWithRetries(ctx)
if err != nil {
return fmt.Errorf("failed to reconcile release pull request: %w", err)
}
@ -143,6 +152,36 @@ func (rp *ReleaserPleaser) createPendingRelease(ctx context.Context, pr *release
return nil
}
// runReconcileReleasePRWithRetries retries runReconcileReleasePR up to PullRequestConflictAttempts times, but only
// when a ErrorPullRequestConflict was encountered.
func (rp *ReleaserPleaser) runReconcileReleasePRWithRetries(ctx context.Context) error {
logger := rp.logger.With("method", "runReconcileReleasePRWithRetries", "totalAttempts", PullRequestConflictAttempts)
var err error
for i := range PullRequestConflictAttempts {
logger := logger.With("attempt", i+1)
logger.DebugContext(ctx, "attempting runReconcileReleasePR")
err = rp.runReconcileReleasePR(ctx)
if err != nil {
if errors.Is(err, ErrorPullRequestConflict) {
logger.WarnContext(ctx, "detected conflict while updating pull request description, retrying")
continue
}
break
}
break
}
if err != nil {
return err
}
return nil
}
func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
logger := rp.logger.With("method", "runReconcileReleasePR")
@ -242,29 +281,30 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
// Info for updaters
info := updater.ReleaseInfo{Version: nextVersion, ChangelogEntry: changelogEntry}
err = repo.UpdateFile(ctx, updater.ChangelogFile, true, updater.WithInfo(info, updater.Changelog))
if err != nil {
return fmt.Errorf("failed to update changelog file: %w", err)
}
for _, path := range rp.extraFiles {
// TODO: Check for missing files
err = repo.UpdateFile(ctx, path, false, updater.WithInfo(info, rp.updaters...))
if err != nil {
return fmt.Errorf("failed to run file updater: %w", err)
for _, u := range rp.updaters {
for _, file := range u.Files() {
err = repo.UpdateFile(ctx, file, u.CreateNewFiles(), u.Update(info))
if err != nil {
return fmt.Errorf("failed to run updater %T: %w", u, err)
}
}
}
releaseCommitAuthor, err := rp.forge.CommitAuthor(ctx)
if err != nil {
return fmt.Errorf("failed to get commit author: %w", err)
}
releaseCommitMessage := fmt.Sprintf("chore(%s): release %s", rp.targetBranch, nextVersion)
releaseCommit, err := repo.Commit(ctx, releaseCommitMessage)
releaseCommit, err := repo.Commit(ctx, releaseCommitMessage, releaseCommitAuthor)
if err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommit.Hash, "commit.message", releaseCommit.Message)
logger.InfoContext(ctx, "created release commit", "commit.hash", releaseCommit.Hash, "commit.message", releaseCommit.Message, "commit.author", releaseCommitAuthor)
// Check if anything changed in comparison to the remote branch (if exists)
newReleasePRChanges, err := repo.HasChangesWithRemote(ctx, rpBranch)
newReleasePRChanges, err := repo.HasChangesWithRemote(ctx, rp.targetBranch, rpBranch)
if err != nil {
return err
}
@ -300,6 +340,23 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
}
logger.InfoContext(ctx, "opened pull request", "pr.title", pr.Title, "pr.id", pr.ID, "pr.url", rp.forge.PullRequestURL(pr.ID))
} else {
// Check if the pull request was updated while releaser-pleaser was running.
// This avoids a conflict where the user updated the PR while releaser-pleaser already pulled the info, and
// releaser-pleaser subsequently reverts the users changes. There is still a minimal time window for this to
// happen between us checking the PR again and submitting our changes.
logger.DebugContext(ctx, "checking for conflict in pr description", "pr.id", pr.ID)
recheckPR, err := rp.forge.PullRequestForBranch(ctx, rpBranch)
if err != nil {
return err
}
if recheckPR == nil {
return fmt.Errorf("PR was deleted while releaser-pleaser was running")
}
if recheckPR.Description != pr.Description {
return ErrorPullRequestConflict
}
pr.SetTitle(rp.targetBranch, nextVersion)
overrides, err := pr.GetOverrides()
@ -322,7 +379,11 @@ func (rp *ReleaserPleaser) runReconcileReleasePR(ctx context.Context) error {
}
func (rp *ReleaserPleaser) analyzedCommitsSince(ctx context.Context, since *git.Tag) ([]commitparser.AnalyzedCommit, error) {
logger := rp.logger.With("method", "analyzedCommitsSince", "tag.hash", since.Hash, "tag.name", since.Name)
logger := rp.logger.With("method", "analyzedCommitsSince")
if since != nil {
logger = rp.logger.With("tag.hash", since.Hash, "tag.name", since.Name)
}
commits, err := rp.forge.CommitsSince(ctx, since)
if err != nil {

View file

@ -9,7 +9,11 @@ spec:
description: "GitLab token for creating and updating release MRs."
extra-files:
description: 'List of files that are scanned for version references.'
description: 'List of files that are scanned for version references by the generic updater.'
default: ""
updaters:
description: "List of updaters that are run. Default updaters can be removed by specifying them as -name. Multiple updaters should be concatenated with a comma. Default Updaters: changelog,generic"
default: ""
stage:
@ -26,11 +30,21 @@ spec:
releaser-pleaser:
stage: $[[ inputs.stage ]]
needs: $[[ inputs.needs ]]
rules:
# There is no way to run a pipeline when the MR description is updated :(
- if: $CI_COMMIT_BRANCH == "$[[ inputs.branch ]]"
# If a newer releaser-pleaser job runs, this one may be cancelled without problem, releaser-pleaser is idempotent.
# This only works if the user enables "auto-cancel redundant pipelines", which we do tell them to, because this is
# intrusive and up to the user.
interruptible: true
# No need to have multiple releaser-pleaser jobs running at the same time. They all act on the same global state.
resource_group: releaser-pleaser
image:
name: ghcr.io/apricote/releaser-pleaser:v0.5.1 # x-releaser-pleaser-version
name: ghcr.io/apricote/releaser-pleaser:v0.7.1 # x-releaser-pleaser-version
entrypoint: [ "" ]
variables:
GITLAB_TOKEN: $[[ inputs.token ]]
@ -39,4 +53,5 @@ releaser-pleaser:
rp run \
--forge=gitlab \
--branch=$[[ inputs.branch ]] \
--extra-files="$[[ inputs.extra-files ]]"
--extra-files="$[[ inputs.extra-files ]]" \
--updaters="$[[ inputs.updaters ]]"

19
test/e2e/forge.go Normal file
View 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
View 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

View 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
View 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),
}
}

View 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
View 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
}