Skip to content

Commit 2182ddd

Browse files
committed
feat: introduced mono-repo workflows
* go-test: factorized multi-modules operations leveraging specialized github composite actions * release: new mono-repo release workflows Release notes for monorepos now contain 2 sections: one general and the other module-specific. Releasing remains a bit simplified at this moment: * lacks the logic to tag only changed modules * lacks the logic to publish release notes only on changed modules Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 37f687e commit 2182ddd

11 files changed

Lines changed: 1998 additions & 80 deletions

.claude/skills/golang-monorepo.md

Lines changed: 1109 additions & 0 deletions
Large diffs are not rendered by default.

.cliff-monorepo.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{%- if version %}
2+
## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
3+
{%- else %}
4+
## [unreleased]
5+
{%- endif %}
6+
{%- if message %}
7+
{%- raw %}
8+
{% endraw %}
9+
{{ message }}
10+
{%- raw %}
11+
{% endraw %}
12+
{%- endif %}
13+
14+
---
15+
16+
{%- for group, commits in commits | group_by(attribute="group") %}
17+
{%- raw %}
18+
{% endraw %}
19+
### {{ group | upper_first }}
20+
{%- raw %}
21+
{% endraw %}
22+
{%- for commit in commits %}
23+
{%- if commit.remote.pr_title %}
24+
{%- set commit_message = commit.remote.pr_title %}
25+
{%- else %}
26+
{%- set commit_message = commit.message %}
27+
{%- endif %}
28+
* {{ commit_message | split(pat="\n") | first | trim }}
29+
{%- if commit.remote.username %}
30+
{%- raw %} {% endraw %}by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }})
31+
{%- endif %}
32+
{%- if commit.remote.pr_number %}
33+
{%- raw %} {% endraw %}in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})
34+
{%- endif %}
35+
{%- raw %} {% endraw %}[...]({{ self::remote_url() }}/commit/{{ commit.id }})
36+
{%- endfor %}
37+
{%- endfor %}
38+
39+
{%- macro remote_url() -%}
40+
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
41+
{%- endmacro -%}

.github/workflows/auto-merge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ jobs:
8181
run: gh pr review --approve "$PR_URL"
8282
-
8383
name: Wait for all workflow runs to complete
84-
uses: go-openapi/gh-actions/ci-jobs/wait-pending-jobs@eb161ed408645b24aaf6120cd5e4a893cf2c0af2 # v1.3.1
84+
uses: go-openapi/gh-actions/ci-jobs/wait-pending-jobs@0ec18ca1ddc1e2257097ae8d57952733109d80a1 # v1.4.0
8585
with:
8686
pr-url: ${{ env.PR_URL }}
8787
github-token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
name: Bump Release [monorepo]
2+
3+
permissions:
4+
contents: read
5+
6+
# description: |
7+
# Manual action to bump the current version and cut a release for mono-repo projects.
8+
#
9+
# Determine which version to bump.
10+
# Generate release notes for each module.
11+
# Tag all modules with the new version.
12+
# Build a github release on pushed tag.
13+
14+
defaults:
15+
run:
16+
shell: bash
17+
18+
on:
19+
workflow_call:
20+
inputs:
21+
bump-patch:
22+
description: Bump a patch version release
23+
type: string
24+
required: false
25+
default: 'true'
26+
bump-minor:
27+
description: Bump a minor version release
28+
type: string
29+
required: false
30+
default: 'false'
31+
bump-major:
32+
description: Bump a major version release
33+
type: string
34+
required: false
35+
default: 'false'
36+
tag-message-title:
37+
description: Tag message title to prepend to the release notes
38+
required: false
39+
type: string
40+
tag-message-body:
41+
description: |
42+
Tag message body to prepend to the release notes.
43+
(use "|" to replace end of line).
44+
required: false
45+
type: string
46+
enable-tag-signing:
47+
description: |
48+
Enable PGP tag-signing by a bot user.
49+
50+
When enabled, you must pass the GPG secrets to this workflow.
51+
required: false
52+
type: string
53+
default: 'true'
54+
cliff-config:
55+
type: string
56+
required: false
57+
default: '.cliff.toml'
58+
description: 'Path to the git-cliff config file in the caller repository'
59+
cliff-config-url:
60+
type: string
61+
required: false
62+
default: 'https://raw.githubusercontent.com/go-openapi/ci-workflows/refs/heads/master/.cliff.toml'
63+
description: 'URL to the remote git-cliff config file (used if local config does not exist)'
64+
monorepo-cliff-template:
65+
type: string
66+
required: false
67+
default: '.cliff-monorepo.toml'
68+
description: 'Path to the git-cliff template used to generate module-specific release notes'
69+
monorepo-cliff-template-url:
70+
type: string
71+
required: false
72+
default: https://raw.githubusercontent.com/go-openapi/ci-workflows/refs/heads/master/.cliff-monorepo.toml
73+
description: 'URL to the remote git-cliff template used to generate module-specific release notes'
74+
secrets:
75+
gpg-private-key:
76+
description: |
77+
GPG private key in armored format for signing tags.
78+
79+
Default for go-openapi: CI_BOT_GPG_PRIVATE_KEY
80+
81+
Required when enable-tag-signing is true.
82+
required: false
83+
gpg-passphrase:
84+
description: |
85+
Passphrase to unlock the GPG private key.
86+
87+
Default for go-openapi: CI_BOT_GPG_PASSPHRASE
88+
89+
Required when enable-tag-signing is true.
90+
required: false
91+
gpg-fingerprint:
92+
description: |
93+
Fingerprint of the GPG signing key (spaces removed).
94+
95+
Default for go-openapi: CI_BOT_SIGNING_KEY
96+
97+
Required when enable-tag-signing is true.
98+
required: false
99+
100+
jobs:
101+
detect-modules:
102+
name: Detect mono-repo modules
103+
runs-on: ubuntu-latest
104+
outputs:
105+
is_monorepo: ${{ steps.detect-monorepo.outputs.is_monorepo }}
106+
names: ${{ steps.detect-monorepo.outputs.names }}
107+
bash-relative-names: ${{ steps.detect-monorepo.outputs.bash-relative-names }}
108+
steps:
109+
-
110+
name: Checkout code
111+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
112+
with:
113+
fetch-depth: 0
114+
-
115+
name: Setup Go
116+
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
117+
with:
118+
go-version: stable
119+
check-latest: true
120+
cache: true
121+
cache-dependency-path: '**/go.sum'
122+
-
123+
name: Detect go mono-repo
124+
id: detect-monorepo
125+
uses: go-openapi/gh-actions/ci-jobs/detect-go-monorepo@0ec18ca1ddc1e2257097ae8d57952733109d80a1 # v1.4.0
126+
127+
bump-release-single:
128+
name: Bump release (single module)
129+
needs: [detect-modules]
130+
if: ${{ needs.detect-modules.outputs.is-monorepo != 'true' }}
131+
permissions:
132+
contents: write
133+
uses: ./.github/workflows/bump-release.yml
134+
with:
135+
bump-patch: ${{ inputs.bump-patch }}
136+
bump-minor: ${{ inputs.bump-minor }}
137+
bump-major: ${{ inputs.bump-major }}
138+
tag-message-title: ${{ inputs.tag-message-title }}
139+
tag-message-body: ${{ inputs.tag-message-body }}
140+
enable-tag-signing: ${{ inputs.enable-tag-signing }}
141+
cliff-config: ${{ inputs.cliff-config }}
142+
cliff-config-url: ${{ inputs.cliff-config-url }}
143+
secrets: inherit
144+
145+
determine-next-tag:
146+
name: Determine next tag [monorepo]
147+
needs: [detect-modules]
148+
if: ${{ needs.detect-modules.outputs.is-monorepo == 'true' }}
149+
runs-on: ubuntu-latest
150+
outputs:
151+
next-tag: ${{ steps.bump-release.outputs.next-tag }}
152+
steps:
153+
-
154+
name: Checkout code
155+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
156+
with:
157+
fetch-depth: 0
158+
-
159+
name: Determine next tag
160+
id: bump-release
161+
uses: go-openapi/gh-actions/ci-jobs/next-tag@0ec18ca1ddc1e2257097ae8d57952733109d80a1 # v1.4.0
162+
with:
163+
bump-patch: ${{ inputs.bump-patch }}
164+
bump-minor: ${{ inputs.bump-minor }}
165+
bump-major: ${{ inputs.bump-major }}
166+
167+
prepare-modules:
168+
name: Prepare module updates [monorepo]
169+
needs: [detect-modules, determine-next-tag]
170+
if: ${{ needs.detect-modules.outputs.is-monorepo == 'true' }}
171+
permissions:
172+
contents: write
173+
pull-requests: write
174+
uses: ./.github/workflows/prepare-release-monorepo.yml
175+
with:
176+
target-tag: ${{ needs.determine-next-tag.outputs.next-tag }}
177+
enable-commit-signing: 'true'
178+
secrets: inherit
179+
180+
wait-for-merge:
181+
name: Wait for PR merge [monorepo]
182+
needs: [detect-modules, prepare-modules]
183+
if: ${{ needs.detect-modules.outputs.is-monorepo == 'true' }}
184+
runs-on: ubuntu-latest
185+
env:
186+
PR_URL: ${{ needs.prepare-modules.outputs.pull-request-url }}
187+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
188+
steps:
189+
-
190+
name: Checkout repository
191+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
192+
-
193+
name: Wait for PR to be merged
194+
run: |
195+
echo "::notice title=waiting-for-merge::Waiting for PR ${PR_URL} to be merged"
196+
197+
MAX_WAIT=1800 # 30 minutes maximum wait time
198+
POLL_INTERVAL=30 # Check every 30 seconds
199+
elapsed=0
200+
201+
while [ $elapsed -lt $MAX_WAIT ]; do
202+
# Check PR state
203+
PR_STATE=$(gh pr view "$PR_URL" --json state --jq '.state')
204+
205+
if [[ "$PR_STATE" == "MERGED" ]]; then
206+
echo "::notice title=pr-merged::PR has been merged successfully"
207+
exit 0
208+
elif [[ "$PR_STATE" == "CLOSED" ]]; then
209+
echo "::error title=pr-closed::PR was closed without merging"
210+
exit 1
211+
fi
212+
213+
echo "::notice title=polling::PR state: ${PR_STATE}, waiting... (${elapsed}s elapsed)"
214+
sleep $POLL_INTERVAL
215+
elapsed=$((elapsed + POLL_INTERVAL))
216+
done
217+
218+
echo "::error title=timeout::Timed out waiting for PR to be merged after ${MAX_WAIT}s"
219+
exit 1
220+
221+
tag-release-monorepo:
222+
name: Tag release (mono-repo)
223+
needs: [detect-modules, determine-next-tag, wait-for-merge]
224+
if: ${{ needs.detect-modules.outputs.is-monorepo == 'true' }}
225+
runs-on: ubuntu-latest
226+
permissions:
227+
contents: write
228+
outputs:
229+
next-tag: ${{ needs.determine-next-tag.outputs.next-tag }}
230+
all-tags: ${{ steps.tag-modules.outputs.all-tags }}
231+
steps:
232+
-
233+
name: Checkout code (fresh after PR merge)
234+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
235+
with:
236+
fetch-depth: 0
237+
# Fetch the latest code after the PR has been merged
238+
ref: ${{ github.ref }}
239+
-
240+
name: Setup Go
241+
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
242+
with:
243+
go-version: stable
244+
check-latest: true
245+
cache: true
246+
cache-dependency-path: '**/go.sum'
247+
-
248+
name: Configure bot credentials
249+
if: ${{ inputs.enable-tag-signing == 'true' }}
250+
uses: go-openapi/gh-actions/ci-jobs/bot-credentials@0ec18ca1ddc1e2257097ae8d57952733109d80a1 # v1.4.0
251+
# This is using the GPG signature of bot-go-openapi.
252+
#
253+
# For go-openapi repos (using secrets: inherit):
254+
# Falls back to: CI_BOT_GPG_PRIVATE_KEY, CI_BOT_GPG_PASSPHRASE, CI_BOT_SIGNING_KEY
255+
#
256+
# For other orgs: explicitly pass secrets with your custom names
257+
# NOTE(fredbi): extracted w/ gpg -K --homedir gnupg --keyid-format LONG --with-keygrip --fingerprint --with-subkey-fingerprint
258+
with:
259+
enable-gpg-signing: 'true'
260+
gpg-private-key: ${{ secrets.gpg-private-key || secrets.CI_BOT_GPG_PRIVATE_KEY }}
261+
gpg-passphrase: ${{ secrets.gpg-passphrase || secrets.CI_BOT_GPG_PASSPHRASE }}
262+
gpg-fingerprint: ${{ secrets.gpg-fingerprint || secrets.CI_BOT_SIGNING_KEY }}
263+
enable-tag-signing: 'true'
264+
enable-commit-signing: 'false'
265+
-
266+
name: Tag all modules
267+
id: tag-modules
268+
env:
269+
NEXT_TAG: ${{ needs.determine-next-tag.outputs.next-tag }}
270+
MESSAGE_TITLE: ${{ inputs.tag-message-title }}
271+
MESSAGE_BODY: ${{ inputs.tag-message-body }}
272+
run: |
273+
# Tag all modules similar to hack/tag_modules.sh
274+
# Note: The PR with updated go.mod files has been merged at this point
275+
root="$(git rev-parse --show-toplevel)"
276+
declare -a all_tags
277+
278+
cd "${root}"
279+
280+
# Construct the tag message
281+
MESSAGE="${MESSAGE_TITLE}"
282+
if [[ -n "${MESSAGE_BODY}" ]] ; then
283+
BODY=$(echo "${MESSAGE_BODY}"|tr '|' '\n')
284+
MESSAGE=$(printf "%s\n%s\n" "${MESSAGE}" "${BODY}")
285+
fi
286+
287+
echo "::notice title=tag-message::Tagging all modules for ${NEXT_TAG}"
288+
289+
SIGNED=""
290+
if [[ '${{ inputs.enable-tag-signing }}' == 'true' ]] ; then
291+
SIGNED="-s"
292+
fi
293+
294+
# Tag all modules
295+
while read -r module_relative_name ; do
296+
if [[ -z "${module_relative_name}" ]] ; then
297+
module_tag="${NEXT_TAG}" # e.g. "v0.24.0"
298+
else
299+
module_tag="${module_relative_name}/${NEXT_TAG}" # e.g. "mangling/v0.24.0"
300+
fi
301+
302+
all_tags+=("${module_tag}")
303+
echo "::notice title=tagging::Creating tag: ${module_tag}"
304+
305+
git tag ${SIGNED} -m "${MESSAGE}" "${module_tag}"
306+
307+
if [[ -n "${SIGNED}" ]] ; then
308+
git tag -v "${module_tag}"
309+
fi
310+
done < <(echo ${{ needs.detect-modules.outputs.bash-relative-names }})
311+
312+
# Save all tags for output
313+
echo "all-tags=${all_tags[@]}" >> "${GITHUB_OUTPUT}"
314+
315+
# Push all tags to origin
316+
echo "::notice title=pushing-tags::Pushing tags: ${all_tags[@]}"
317+
git push origin ${all_tags[@]}
318+
319+
gh-release-monorepo:
320+
# trigger release creation explicitly.
321+
#
322+
# The previous tagging action does not trigger the normal release workflow
323+
# (github prevents cascading triggers from happening).
324+
name: Create release [monorepo]
325+
needs: [detect-modules, tag-release-monorepo]
326+
if: ${{ needs.detect-modules.outputs.is-monorepo == 'true' }}
327+
permissions:
328+
contents: write
329+
uses: ./.github/workflows/release.yml
330+
with:
331+
tag: ${{ needs.tag-release-monorepo.outputs.next-tag }}
332+
is-monorepo: 'true'
333+
cliff-config: ${{ inputs.cliff-config }}
334+
cliff-config-url: ${{ inputs.cliff-config-url }}
335+
monorepo-cliff-template: ${{ inputs.monorepo-cliff-template }}
336+
monorepo-cliff-template-url: ${{ inputs.monorepo-cliff-temlate-url }}
337+
secrets: inherit

0 commit comments

Comments
 (0)