Add multi-module release launcher example

This commit is contained in:
2026-10-10 03:46:12 +00:00
parent c9879cffa2
commit cd828dc06d
12 changed files with 464 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
# Multi-module release example
This example maps selectors and complete event tags to a physical Go module
directory and a repository-root profile. The library derives the release tag
from the selected module declaration and physical location; the table only
selects the corresponding profile and checkout directory.
Copy the contents of this directory into a consumer repository root, keeping
the listed module directories and profile paths at those locations, then run
the launcher from that checkout.
`release-modules.tsv` is literal tab-separated text with four columns:
1. Local selector.
2. Shell pattern matched against a complete event tag.
3. Repository-relative physical module directory (`.` means the root module).
4. Repository-relative profile path.
The checked-in patterns are `v[01].*`, `integrations/ckconfig/v*`, and
`integrations/core/v2.*`. The helper passes pattern text to the shell's `case`
matcher without `eval`. A tag must match exactly one row; missing and ambiguous
matches are configuration errors. Module and profile paths may contain spaces.
The `core-v2` row places the Go module physically under `integrations/core/v2`
and declares a `/v2` module path. ReleaseKit derives the versioned tag under
`integrations/core`, while the table retains the full physical path.
Before running candidate checks offline, prepare each module's dependencies in
its own directory:
```sh
go -C . mod download
go -C integrations/ckconfig mod download
go -C integrations/core/v2 mod download
```
Then select a module and semantic version with the shared launcher:
```sh
./release.sh root v1.2.3
./release.sh ckconfig v1.2.3
./release.sh core-v2 v2.1.0
```
The launcher copies the selected module's `go.mod` and `go.sum` to temporary
alternate module files outside the checkout, resolves ReleaseKit from that
module's local dependency selection, and builds the tool there. It invokes
`publish` with the repository profile and `--expected-module` so the library
can verify that the profile agrees with the selected table row. The temporary
files and tool are removed on exit. As with any `publish` invocation, commit the
selected module's release note and prepare its repository before publishing.
If an integration depends on a sibling release, publish the dependency first,
then publish the integration with the version that consumes it. For example,
publish the core release before an integration that requires that release.
This declaration does not infer dependency order or rerun the standard suite.

View File

@@ -0,0 +1,5 @@
module example.com/multi-module-consumer
go 1.25.0
require gitea.maximumdirect.net/eric/ReleaseKit v0.2.197

View File

@@ -0,0 +1,5 @@
module example.com/multi-module-consumer/integrations/ckconfig
go 1.25.0
require gitea.maximumdirect.net/eric/ReleaseKit v0.2.197

View File

@@ -0,0 +1,5 @@
module example.com/multi-module-consumer/integrations/core/v2
go 1.25.0
require gitea.maximumdirect.net/eric/ReleaseKit v0.2.197

View File

@@ -0,0 +1,4 @@
# selector<TAB>tag pattern<TAB>module directory<TAB>profile path
root v[01].* . releasekit-root.json
ckconfig integrations/ckconfig/v* integrations/ckconfig releasekit-ckconfig.json
core-v2 integrations/core/v2.* integrations/core/v2 releasekit-core-v2.json
1 # selector<TAB>tag pattern<TAB>module directory<TAB>profile path
2 root v[01].* . releasekit-root.json
3 ckconfig integrations/ckconfig/v* integrations/ckconfig releasekit-ckconfig.json
4 core-v2 integrations/core/v2.* integrations/core/v2 releasekit-core-v2.json

View File

@@ -0,0 +1,48 @@
#!/bin/sh
set -eu
usage() {
echo "usage: release.sh SELECTOR VERSION" >&2
exit 2
}
[ "$#" -eq 2 ] || usage
selector=$1
version=$2
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
repository_dir=$(git -C "$script_dir" rev-parse --show-toplevel)
bootstrap_dir=$(mktemp -d "$(dirname "$repository_dir")/.releasekit-bootstrap.XXXXXX")
selection_file=$bootstrap_dir/module-selection.tsv
trap 'rm -rf "$bootstrap_dir"' EXIT
"$script_dir/select-module.sh" selector "$selector" > "$selection_file"
tab=$(printf '\t')
IFS="$tab" read -r selected_selector tag_pattern module_dir profile_path < "$selection_file"
module_path=$repository_dir
if [ "$module_dir" != . ]; then
module_path=$repository_dir/$module_dir
fi
cp "$module_path/go.mod" "$bootstrap_dir/releasekit.mod"
if [ -f "$module_path/go.sum" ]; then
cp "$module_path/go.sum" "$bootstrap_dir/releasekit.sum"
else
: > "$bootstrap_dir/releasekit.sum"
fi
releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit
(
cd "$module_path"
export GOWORK=off
export GOTOOLCHAIN=local
go list -mod=mod -modfile="$bootstrap_dir/releasekit.mod" -m "$releasekit_module"
go build -mod=mod -modfile="$bootstrap_dir/releasekit.mod" \
-o "$bootstrap_dir/releasekit" "$releasekit_module/cmd/releasekit"
)
"$bootstrap_dir/releasekit" publish \
--repo "$repository_dir" \
--config "$profile_path" \
--expected-module "$module_dir" \
"$version"

View File

@@ -0,0 +1,4 @@
{
"product": "Example Consumer Integration",
"module_dir": "integrations/ckconfig"
}

View File

@@ -0,0 +1,4 @@
{
"product": "Example Consumer Core v2",
"module_dir": "integrations/core/v2"
}

View File

@@ -0,0 +1,4 @@
{
"product": "Example Consumer",
"module_dir": "."
}

View File

@@ -0,0 +1,70 @@
#!/bin/sh
set -eu
usage() {
echo "usage: select-module.sh selector|tag VALUE" >&2
exit 2
}
[ "$#" -eq 2 ] || usage
mode=$1
value=$2
case "$mode" in
selector|tag) ;;
*) usage ;;
esac
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
mapping=${RELEASEKIT_MODULE_MAP:-$script_dir/release-modules.tsv}
[ -f "$mapping" ] || { echo "module map not found: $mapping" >&2; exit 2; }
tab=$(printf '\t')
matches=0
selected_selector=
selected_pattern=
selected_module=
selected_profile=
while IFS="$tab" read -r selector pattern module profile || [ -n "${selector:-}${pattern:-}${module:-}${profile:-}" ]; do
case "${selector:-}" in
''|\#*) continue ;;
esac
fields=$(awk -F '\t' '{ print NF }' <<EOF
$selector$tab$pattern$tab$module$tab$profile
EOF
)
[ "$fields" -eq 4 ] || { echo "module map row must have four tab-separated fields" >&2; exit 2; }
[ -n "$pattern" ] && [ -n "$module" ] && [ -n "$profile" ] || {
echo "module map row has an empty field for selector $selector" >&2
exit 2
}
matched=false
if [ "$mode" = selector ]; then
[ "$selector" = "$value" ] && matched=true
else
# The value remains a shell pattern in this case statement; it is never eval'd.
case "$value" in
$pattern) matched=true ;;
esac
fi
if [ "$matched" = true ]; then
matches=$((matches + 1))
selected_selector=$selector
selected_pattern=$pattern
selected_module=$module
selected_profile=$profile
fi
done < "$mapping"
if [ "$matches" -ne 1 ]; then
if [ "$matches" -eq 0 ]; then
echo "no module map row matches $mode $value" >&2
else
echo "module map has ambiguous rows for $mode $value" >&2
fi
exit 2
fi
printf '%s\t%s\t%s\t%s\n' \
"$selected_selector" "$selected_pattern" "$selected_module" "$selected_profile"