Skip to content

Commit e2f6723

Browse files
committed
feat(go-licenses): add reusable workflows for license check and report
Adds two reusable workflows that consolidate the go-licenses-check and go-licenses workflows currently duplicated across vcluster, vcluster-pro, and loft-enterprise. Business logic lives in a shellcheck-clean script with 17 bats tests; both workflows sparse-checkout it from the reusable workflow's own ref. - package-mode input supports both './...' + --ignore (go-licenses >= v1.6.0) and go.work-based filtering (required for v1.0.0 which lacks --ignore) - optional gh-access-token for callers with private loft-sh dependencies - built-in fork guard (if: github.repository_owner == 'loft-sh') - fail-on-error escape hatch for transient upstream breakage (vcluster-pro) - renovate customManager bumps the default go-licenses version
1 parent a9d1c21 commit e2f6723

7 files changed

Lines changed: 585 additions & 2 deletions

File tree

.github/scripts/go-licenses/run.sh

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Runs `go-licenses check` or `go-licenses report` with consistent handling of
5+
# package selection and ignored packages across `go.mod` and `go.work`
6+
# projects. Called from the go-licenses-check / go-licenses-report reusable
7+
# workflows so the branching logic lives in a shellcheck-clean script instead
8+
# of inline YAML.
9+
#
10+
# Usage:
11+
# run.sh check
12+
# run.sh report
13+
#
14+
# Required environment variables:
15+
# PACKAGE_MODE "all" — pass ./... and --ignore flags to go-licenses.
16+
# "go-work" — enumerate workspace modules from go.work and
17+
# filter out ignored prefixes at the package list level.
18+
# Use this for go-licenses versions < v1.6.0 (no --ignore
19+
# flag) and for monorepos whose root does not compile.
20+
# IGNORED_PACKAGES Comma-separated list of package path prefixes to skip.
21+
# In "all" mode they become --ignore flags (matching the
22+
# import path prefix). In "go-work" mode they are matched
23+
# as substrings against go.work DiskPaths.
24+
#
25+
# Check-mode variables:
26+
# FAIL_ON_ERROR "true" (default) or "false". When "false", non-zero exit
27+
# codes from go-licenses are logged as a workflow warning
28+
# and the step still succeeds — used only as a temporary
29+
# escape hatch when upstream go-licenses is broken.
30+
#
31+
# Report-mode variables:
32+
# TEMPLATE_PATH Path to the go-licenses .tmpl template (required).
33+
# OUTPUT_PATH File to write the rendered report to (required). Any
34+
# missing parent directories are created.
35+
36+
SUBCOMMAND="${1:?subcommand is required: check or report}"
37+
PACKAGE_MODE="${PACKAGE_MODE:-all}"
38+
IGNORED_PACKAGES="${IGNORED_PACKAGES:-}"
39+
40+
# Parse IGNORED_PACKAGES into an array of trimmed, non-empty prefixes.
41+
IGNORE=()
42+
if [ -n "${IGNORED_PACKAGES}" ]; then
43+
IFS=',' read -ra raw_ignore <<< "${IGNORED_PACKAGES}"
44+
for prefix in "${raw_ignore[@]}"; do
45+
trimmed="${prefix// /}"
46+
if [ -n "${trimmed}" ]; then
47+
IGNORE+=("${trimmed}")
48+
fi
49+
done
50+
fi
51+
52+
# Build the go-licenses argument vector based on PACKAGE_MODE.
53+
ARGS=()
54+
case "${PACKAGE_MODE}" in
55+
all)
56+
ARGS+=("./...")
57+
for prefix in ${IGNORE[@]+"${IGNORE[@]}"}; do
58+
ARGS+=("--ignore" "${prefix}")
59+
done
60+
;;
61+
go-work)
62+
mapfile -t PKGS < <(go work edit -json | jq -r '.Use[].DiskPath + "/..."')
63+
for prefix in ${IGNORE[@]+"${IGNORE[@]}"}; do
64+
FILTERED=()
65+
for pkg in ${PKGS[@]+"${PKGS[@]}"}; do
66+
case "${pkg}" in
67+
*"${prefix}"*) ;;
68+
*) FILTERED+=("${pkg}") ;;
69+
esac
70+
done
71+
PKGS=(${FILTERED[@]+"${FILTERED[@]}"})
72+
done
73+
if [ "${#PKGS[@]}" -eq 0 ]; then
74+
echo "::error::no packages to check after filtering IGNORED_PACKAGES" >&2
75+
exit 1
76+
fi
77+
ARGS+=(${PKGS[@]+"${PKGS[@]}"})
78+
;;
79+
*)
80+
echo "::error::invalid PACKAGE_MODE '${PACKAGE_MODE}' (expected: all, go-work)" >&2
81+
exit 1
82+
;;
83+
esac
84+
85+
case "${SUBCOMMAND}" in
86+
check)
87+
FAIL_ON_ERROR="${FAIL_ON_ERROR:-true}"
88+
if [ "${FAIL_ON_ERROR}" = "true" ]; then
89+
go-licenses check "${ARGS[@]}"
90+
else
91+
if ! go-licenses check "${ARGS[@]}"; then
92+
echo "::warning::go-licenses check reported errors (ignored because fail-on-error=false)"
93+
fi
94+
fi
95+
;;
96+
report)
97+
: "${TEMPLATE_PATH:?TEMPLATE_PATH env var is required for report}"
98+
: "${OUTPUT_PATH:?OUTPUT_PATH env var is required for report}"
99+
mkdir -p "$(dirname "${OUTPUT_PATH}")"
100+
go-licenses report --template "${TEMPLATE_PATH}" "${ARGS[@]}" > "${OUTPUT_PATH}"
101+
;;
102+
*)
103+
echo "::error::invalid subcommand '${SUBCOMMAND}' (expected: check, report)" >&2
104+
exit 1
105+
;;
106+
esac
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#!/usr/bin/env bats
2+
# Tests for run.sh
3+
#
4+
# Stubs `go-licenses` and `go` so we can assert on the argument vector the
5+
# script builds without needing a real Go toolchain or real Go sources.
6+
7+
SCRIPT="$BATS_TEST_DIRNAME/../run.sh"
8+
9+
setup() {
10+
MOCK_DIR=$(mktemp -d)
11+
export MOCK_DIR
12+
export GO_LICENSES_ARGS_FILE="$MOCK_DIR/go_licenses_args"
13+
export GO_LICENSES_EXIT_CODE_FILE="$MOCK_DIR/go_licenses_exit_code"
14+
export GO_LICENSES_STDOUT_FILE="$MOCK_DIR/go_licenses_stdout"
15+
export GO_WORK_JSON_FILE="$MOCK_DIR/go_work_json"
16+
17+
echo "0" > "$GO_LICENSES_EXIT_CODE_FILE"
18+
: > "$GO_LICENSES_STDOUT_FILE"
19+
20+
# Stub go-licenses: record its arguments, optionally emit stdout, exit with
21+
# the configured exit code.
22+
cat > "$MOCK_DIR/go-licenses" <<'MOCK'
23+
#!/usr/bin/env bash
24+
printf '%s\n' "$@" > "$GO_LICENSES_ARGS_FILE"
25+
if [ -s "$GO_LICENSES_STDOUT_FILE" ]; then
26+
cat "$GO_LICENSES_STDOUT_FILE"
27+
fi
28+
exit "$(cat "$GO_LICENSES_EXIT_CODE_FILE")"
29+
MOCK
30+
chmod +x "$MOCK_DIR/go-licenses"
31+
32+
# Stub `go` so `go work edit -json` returns a fixture. Any other `go`
33+
# invocation fails loudly — the script should not call `go` outside the
34+
# go-work mode path.
35+
cat > "$MOCK_DIR/go" <<'MOCK'
36+
#!/usr/bin/env bash
37+
if [ "$1" = "work" ] && [ "$2" = "edit" ] && [ "$3" = "-json" ]; then
38+
cat "$GO_WORK_JSON_FILE"
39+
exit 0
40+
fi
41+
echo "unexpected go invocation: $*" >&2
42+
exit 99
43+
MOCK
44+
chmod +x "$MOCK_DIR/go"
45+
46+
export PATH="$MOCK_DIR:$PATH"
47+
}
48+
49+
teardown() {
50+
rm -rf "$MOCK_DIR"
51+
}
52+
53+
# --- Subcommand / PACKAGE_MODE validation ---
54+
55+
@test "fails when subcommand is missing" {
56+
run bash "$SCRIPT"
57+
[ "$status" -ne 0 ]
58+
}
59+
60+
@test "fails on invalid subcommand" {
61+
run bash "$SCRIPT" invalid
62+
[ "$status" -ne 0 ]
63+
[[ "$output" == *"invalid subcommand"* ]]
64+
}
65+
66+
@test "fails on invalid PACKAGE_MODE" {
67+
PACKAGE_MODE=bogus run bash "$SCRIPT" check
68+
[ "$status" -ne 0 ]
69+
[[ "$output" == *"invalid PACKAGE_MODE"* ]]
70+
}
71+
72+
# --- Mode: all (./... + --ignore) ---
73+
74+
@test "all mode with no ignores passes ./..." {
75+
run bash "$SCRIPT" check
76+
[ "$status" -eq 0 ]
77+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "check
78+
./..." ]
79+
}
80+
81+
@test "all mode appends --ignore for each comma-separated prefix" {
82+
IGNORED_PACKAGES="github.com/loft-sh,modernc.org/mathutil" \
83+
run bash "$SCRIPT" check
84+
[ "$status" -eq 0 ]
85+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "check
86+
./...
87+
--ignore
88+
github.com/loft-sh
89+
--ignore
90+
modernc.org/mathutil" ]
91+
}
92+
93+
@test "all mode trims whitespace and drops empty entries" {
94+
IGNORED_PACKAGES=" github.com/loft-sh , , modernc.org/mathutil " \
95+
run bash "$SCRIPT" check
96+
[ "$status" -eq 0 ]
97+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "check
98+
./...
99+
--ignore
100+
github.com/loft-sh
101+
--ignore
102+
modernc.org/mathutil" ]
103+
}
104+
105+
# --- Mode: go-work (enumerate + filter) ---
106+
107+
@test "go-work mode enumerates DiskPaths from go.work" {
108+
cat > "$GO_WORK_JSON_FILE" <<'JSON'
109+
{"Use":[{"DiskPath":"."},{"DiskPath":"staging/src/github.com/loft-sh/api"}]}
110+
JSON
111+
PACKAGE_MODE=go-work run bash "$SCRIPT" check
112+
[ "$status" -eq 0 ]
113+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "check
114+
./...
115+
staging/src/github.com/loft-sh/api/..." ]
116+
}
117+
118+
@test "go-work mode filters out DiskPaths matching ignored prefixes" {
119+
cat > "$GO_WORK_JSON_FILE" <<'JSON'
120+
{"Use":[{"DiskPath":"."},{"DiskPath":"staging/src/github.com/loft-sh/api"},{"DiskPath":"staging/src/github.com/loft-sh/agentapi"}]}
121+
JSON
122+
PACKAGE_MODE=go-work \
123+
IGNORED_PACKAGES="github.com/loft-sh" \
124+
run bash "$SCRIPT" check
125+
[ "$status" -eq 0 ]
126+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "check
127+
./..." ]
128+
}
129+
130+
@test "go-work mode fails when all packages are filtered out" {
131+
cat > "$GO_WORK_JSON_FILE" <<'JSON'
132+
{"Use":[{"DiskPath":"staging/src/github.com/loft-sh/api"}]}
133+
JSON
134+
PACKAGE_MODE=go-work \
135+
IGNORED_PACKAGES="github.com/loft-sh" \
136+
run bash "$SCRIPT" check
137+
[ "$status" -ne 0 ]
138+
[[ "$output" == *"no packages to check"* ]]
139+
}
140+
141+
# --- Check subcommand: fail-on-error ---
142+
143+
@test "check fails when go-licenses exits non-zero and fail-on-error=true" {
144+
echo "1" > "$GO_LICENSES_EXIT_CODE_FILE"
145+
run bash "$SCRIPT" check
146+
[ "$status" -ne 0 ]
147+
}
148+
149+
@test "check succeeds with warning when fail-on-error=false" {
150+
echo "1" > "$GO_LICENSES_EXIT_CODE_FILE"
151+
FAIL_ON_ERROR=false run bash "$SCRIPT" check
152+
[ "$status" -eq 0 ]
153+
[[ "$output" == *"::warning::"* ]]
154+
[[ "$output" == *"fail-on-error=false"* ]]
155+
}
156+
157+
@test "check succeeds silently when go-licenses exits zero" {
158+
run bash "$SCRIPT" check
159+
[ "$status" -eq 0 ]
160+
[[ "$output" != *"::warning::"* ]]
161+
}
162+
163+
# --- Report subcommand ---
164+
165+
@test "report requires TEMPLATE_PATH" {
166+
OUTPUT_PATH="$MOCK_DIR/out.mdx" run bash "$SCRIPT" report
167+
[ "$status" -ne 0 ]
168+
[[ "$output" == *"TEMPLATE_PATH"* ]]
169+
}
170+
171+
@test "report requires OUTPUT_PATH" {
172+
TEMPLATE_PATH="$MOCK_DIR/tpl.tmpl" run bash "$SCRIPT" report
173+
[ "$status" -ne 0 ]
174+
[[ "$output" == *"OUTPUT_PATH"* ]]
175+
}
176+
177+
@test "report writes go-licenses stdout to OUTPUT_PATH" {
178+
echo "rendered licenses" > "$GO_LICENSES_STDOUT_FILE"
179+
touch "$MOCK_DIR/tpl.tmpl"
180+
TEMPLATE_PATH="$MOCK_DIR/tpl.tmpl" \
181+
OUTPUT_PATH="$MOCK_DIR/nested/dir/out.mdx" \
182+
run bash "$SCRIPT" report
183+
[ "$status" -eq 0 ]
184+
[ "$(cat "$MOCK_DIR/nested/dir/out.mdx")" = "rendered licenses" ]
185+
}
186+
187+
@test "report passes report subcommand and template path to go-licenses" {
188+
touch "$MOCK_DIR/tpl.tmpl"
189+
TEMPLATE_PATH="$MOCK_DIR/tpl.tmpl" \
190+
OUTPUT_PATH="$MOCK_DIR/out.mdx" \
191+
IGNORED_PACKAGES="github.com/loft-sh" \
192+
run bash "$SCRIPT" report
193+
[ "$status" -eq 0 ]
194+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "report
195+
--template
196+
$MOCK_DIR/tpl.tmpl
197+
./...
198+
--ignore
199+
github.com/loft-sh" ]
200+
}
201+
202+
@test "report works in go-work mode" {
203+
cat > "$GO_WORK_JSON_FILE" <<'JSON'
204+
{"Use":[{"DiskPath":"."},{"DiskPath":"staging/src/github.com/loft-sh/api"}]}
205+
JSON
206+
echo "rendered" > "$GO_LICENSES_STDOUT_FILE"
207+
touch "$MOCK_DIR/tpl.tmpl"
208+
PACKAGE_MODE=go-work \
209+
IGNORED_PACKAGES="github.com/loft-sh" \
210+
TEMPLATE_PATH="$MOCK_DIR/tpl.tmpl" \
211+
OUTPUT_PATH="$MOCK_DIR/out.mdx" \
212+
run bash "$SCRIPT" report
213+
[ "$status" -eq 0 ]
214+
[ "$(cat "$MOCK_DIR/out.mdx")" = "rendered" ]
215+
[ "$(cat "$GO_LICENSES_ARGS_FILE")" = "report
216+
--template
217+
$MOCK_DIR/tpl.tmpl
218+
./..." ]
219+
}

0 commit comments

Comments
 (0)