skillhub/scripts/tests/validate-release-config-test.sh
XiaoSeS dd82c8a62c feat(deploy): wire Feishu credentials into the release surfaces
.env.release.example advertised OAUTH2_FEISHU_* knobs that no deployment
path could actually deliver. compose.release.yml has no env_file, so every
variable must be listed explicitly, and the Helm chart and k8s base only
mapped the GitHub secret keys. Setting the documented variables therefore
did nothing.

Adds Feishu to compose.release.yml, the Helm secret template and values,
the k8s deployment and its secret example. GitLab had the identical gap, so
it is wired at the same time rather than leaving the example file half true.

validate-release-config.sh only checked that GitHub's id and secret appear
together. A half-configured provider renders a login button whose exchange
then fails, so the check now loops over all three providers. Its test gained
both-directions cases per provider plus a fully configured pass; reverting
the loop to GitHub-only makes them fail.

Also adds the provider's only failure log. Nothing downstream records a
Feishu userinfo failure -- OAuth2LoginFailureHandler does not log either --
so the previous code was silent on error. Logs the exception class and
Feishu's own error code, never the upstream msg, which can quote the access
token; a test asserts the code is present and the token is not.

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-09-21 10:18:37 +08:00

292 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$REPO_ROOT/scripts/validate-release-config.sh"
TMP_DIRS=()
cleanup() {
local d
for d in "${TMP_DIRS[@]+"${TMP_DIRS[@]}"}"; do
rm -rf "$d"
done
}
trap cleanup EXIT
new_tmp() {
local d
d="$(mktemp -d)"
TMP_DIRS+=("$d")
echo "$d"
}
fail() {
echo "FAIL: $*" >&2
exit 1
}
write_env() {
local file="$1"
local secret="${2:-}"
local include_secret="${3:-yes}"
cat >"$file" <<EOF
SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com
POSTGRES_DB=skillhub
POSTGRES_USER=skillhub
POSTGRES_PASSWORD=strong-postgres-password
SESSION_COOKIE_SECURE=true
BOOTSTRAP_ADMIN_ENABLED=false
SKILLHUB_TRUST_FORWARDED_PROTO=false
SKILLHUB_BUILTIN_SKILLS_ENABLED=true
SKILLHUB_STORAGE_PROVIDER=s3
SKILLHUB_STORAGE_S3_ENDPOINT=https://storage.example.com
SKILLHUB_STORAGE_S3_BUCKET=skillhub
SKILLHUB_STORAGE_S3_ACCESS_KEY=release-access-key
SKILLHUB_STORAGE_S3_SECRET_KEY=release-secret-key
SKILLHUB_STORAGE_S3_REGION=us-east-1
SKILLHUB_STORAGE_S3_FORCE_PATH_STYLE=false
SKILLHUB_STORAGE_S3_AUTO_CREATE_BUCKET=false
EOF
if [[ "$include_secret" == "yes" ]]; then
printf 'SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=%s\n' "$secret" >>"$file"
fi
}
expect_fail() {
local file="$1"
local expected="$2"
local output
if output="$("$SCRIPT" "$file" 2>&1)"; then
fail "expected validation to fail for $file"
fi
if [[ "$output" != *"$expected"* ]]; then
fail "expected output to contain '$expected', got: $output"
fi
}
tmp="$(new_tmp)"
valid_env="$tmp/valid.env"
write_env "$valid_env" "release-download-secret-32-bytes-minimum"
"$SCRIPT" "$valid_env" >/dev/null
disabled_builtin_skills_env="$tmp/disabled-builtin-skills.env"
write_env "$disabled_builtin_skills_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_BUILTIN_SKILLS_ENABLED=false" >>"$disabled_builtin_skills_env"
"$SCRIPT" "$disabled_builtin_skills_env" >/dev/null
relative_api_base_env="$tmp/relative-api-base.env"
write_env "$relative_api_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_API_BASE_URL=/skillhub\n' >>"$relative_api_base_env"
"$SCRIPT" "$relative_api_base_env" >/dev/null
sub_path_env="$tmp/sub-path.env"
write_env "$sub_path_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/skillhub' \
'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/skillhub' \
>"$tmp/sub-path-overrides"
cat "$tmp/sub-path-overrides" >>"$sub_path_env"
"$SCRIPT" "$sub_path_env" >/dev/null
missing_public_sub_path_env="$tmp/missing-public-sub-path.env"
write_env "$missing_public_sub_path_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/skillhub' \
>>"$missing_public_sub_path_env"
expect_fail "$missing_public_sub_path_env" "SKILLHUB_PUBLIC_BASE_URL path"
# A public URL that merely ends with the base path but serves it under a different path
# (/other/skillhub) must be rejected: suffix match is not enough, the path must be exact.
wrong_public_path_env="$tmp/wrong-public-path.env"
write_env "$wrong_public_path_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/skillhub' \
'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/other/skillhub' \
>>"$wrong_public_path_env"
expect_fail "$wrong_public_path_env" "must equal SKILLHUB_WEB_BASE_PATH without its trailing slash"
# A query or fragment in SKILLHUB_PUBLIC_BASE_URL corrupts concatenated URLs
# (e.g. ${SKILLHUB_PUBLIC_BASE_URL}/cli/auth). Reject it independently of the
# base path, so even a root deployment (no SKILLHUB_WEB_BASE_PATH) is covered.
public_query_env="$tmp/public-query.env"
write_env "$public_query_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' 'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/skillhub?ref=1' \
>>"$public_query_env"
expect_fail "$public_query_env" "must not contain a query"
public_fragment_env="$tmp/public-fragment.env"
write_env "$public_fragment_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' 'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com#frag' \
>>"$public_fragment_env"
expect_fail "$public_fragment_env" "must not contain a query"
# A scheme with no host (https://) passes a naive prefix check but concatenates
# into an invalid URL like https:///cli/auth. Must be rejected.
public_no_host_env="$tmp/public-no-host.env"
write_env "$public_no_host_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' 'SKILLHUB_PUBLIC_BASE_URL=https://' >>"$public_no_host_env"
expect_fail "$public_no_host_env" "must include a host"
# Base path format must be rejected here, matching the runtime entrypoint check,
# rather than passing config validation and only failing at container start.
dot_segment_base_env="$tmp/dot-segment-base.env"
write_env "$dot_segment_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=/foo/../bar/\n' >>"$dot_segment_base_env"
expect_fail "$dot_segment_base_env" "must not contain '.' or '..' path segments"
double_slash_base_env="$tmp/double-slash-base.env"
write_env "$double_slash_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=/foo//bar/\n' >>"$double_slash_base_env"
expect_fail "$double_slash_base_env" "SKILLHUB_WEB_BASE_PATH contains unsupported characters"
missing_trailing_base_env="$tmp/missing-trailing-base.env"
write_env "$missing_trailing_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=/skillhub\n' >>"$missing_trailing_base_env"
expect_fail "$missing_trailing_base_env" "must be '/' or start and end with '/'"
# A base path whose first segment collides with a server Nginx location (/api/,
# /oauth2/, ...) must be rejected: it would shadow the real route and break the app.
for reserved in /api/ /oauth2/ /login/ /assets/ /registry/ /nginx-health/ /.well-known/ /runtime-config.js/ /api/nested/; do
reserved_base_env="$tmp/reserved-base.env"
write_env "$reserved_base_env" "release-download-secret-32-bytes-minimum"
printf 'SKILLHUB_WEB_BASE_PATH=%s\n' "$reserved" >>"$reserved_base_env"
expect_fail "$reserved_base_env" "reserved by the SkillHub server"
done
# A same-origin API base that disagrees with the web base path routes to the wrong prefix.
mismatch_api_base_env="$tmp/mismatch-api-base.env"
write_env "$mismatch_api_base_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' \
'SKILLHUB_WEB_BASE_PATH=/skillhub/' \
'SKILLHUB_WEB_API_BASE_URL=/other' \
'SKILLHUB_PUBLIC_BASE_URL=https://skillhub.example.com/skillhub' \
>>"$mismatch_api_base_env"
expect_fail "$mismatch_api_base_env" "must equal SKILLHUB_WEB_BASE_PATH without its trailing slash"
missing_env="$tmp/missing.env"
write_env "$missing_env" "" no
expect_fail "$missing_env" "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET is required"
placeholder_env="$tmp/placeholder.env"
write_env "$placeholder_env" "change-me-in-production"
expect_fail "$placeholder_env" "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET still uses placeholder/default value"
short_env="$tmp/short.env"
write_env "$short_env" "too-short"
expect_fail "$short_env" "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET must be at least 32 characters"
invalid_forwarded_proto_env="$tmp/invalid-forwarded-proto.env"
write_env "$invalid_forwarded_proto_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_TRUST_FORWARDED_PROTO=yes" >>"$invalid_forwarded_proto_env"
expect_fail "$invalid_forwarded_proto_env" "SKILLHUB_TRUST_FORWARDED_PROTO must be true or false"
invalid_builtin_skills_env="$tmp/invalid-builtin-skills.env"
write_env "$invalid_builtin_skills_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_BUILTIN_SKILLS_ENABLED=yes" >>"$invalid_builtin_skills_env"
expect_fail "$invalid_builtin_skills_env" "SKILLHUB_BUILTIN_SKILLS_ENABLED must be true or false"
valid_redis_cluster_env="$tmp/valid-redis-cluster.env"
write_env "$valid_redis_cluster_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_redis_cluster_env" <<'EOF'
SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:6379,redis-b.example.com:6380
SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=5
SPRING_DATA_REDIS_SSL_ENABLED=true
EOF
"$SCRIPT" "$valid_redis_cluster_env" >/dev/null
invalid_redis_cluster_node_env="$tmp/invalid-redis-cluster-node.env"
write_env "$invalid_redis_cluster_node_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com" >>"$invalid_redis_cluster_node_env"
expect_fail "$invalid_redis_cluster_node_env" "SPRING_DATA_REDIS_CLUSTER_NODES entries must use host:port"
invalid_redis_cluster_port_env="$tmp/invalid-redis-cluster-port.env"
write_env "$invalid_redis_cluster_port_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:65536" >>"$invalid_redis_cluster_port_env"
expect_fail "$invalid_redis_cluster_port_env" "SPRING_DATA_REDIS_CLUSTER_NODES port must be between 1 and 65535"
invalid_redis_redirects_env="$tmp/invalid-redis-redirects.env"
write_env "$invalid_redis_redirects_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS=-1" >>"$invalid_redis_redirects_env"
expect_fail "$invalid_redis_redirects_env" "SPRING_DATA_REDIS_CLUSTER_MAX_REDIRECTS must be a non-negative integer"
invalid_redis_database_env="$tmp/invalid-redis-database.env"
write_env "$invalid_redis_database_env" "release-download-secret-32-bytes-minimum"
cat >>"$invalid_redis_database_env" <<'EOF'
SPRING_DATA_REDIS_CLUSTER_NODES=redis-a.example.com:6379
SPRING_DATA_REDIS_DATABASE=1
EOF
expect_fail "$invalid_redis_database_env" "SPRING_DATA_REDIS_DATABASE must be 0 when SPRING_DATA_REDIS_CLUSTER_NODES is set"
valid_redis_sentinel_env="$tmp/valid-redis-sentinel.env"
write_env "$valid_redis_sentinel_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_redis_sentinel_env" <<'EOF'
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:26379,sentinel-b.example.com:26379
SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=false
EOF
"$SCRIPT" "$valid_redis_sentinel_env" >/dev/null
missing_redis_sentinel_nodes_env="$tmp/missing-redis-sentinel-nodes.env"
write_env "$missing_redis_sentinel_nodes_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster" >>"$missing_redis_sentinel_nodes_env"
expect_fail "$missing_redis_sentinel_nodes_env" "SPRING_DATA_REDIS_SENTINEL_NODES is required when SPRING_DATA_REDIS_SENTINEL_MASTER is set"
missing_redis_sentinel_master_env="$tmp/missing-redis-sentinel-master.env"
write_env "$missing_redis_sentinel_master_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:26379" >>"$missing_redis_sentinel_master_env"
expect_fail "$missing_redis_sentinel_master_env" "SPRING_DATA_REDIS_SENTINEL_MASTER is required when SPRING_DATA_REDIS_SENTINEL_NODES is set"
invalid_redis_sentinel_port_env="$tmp/invalid-redis-sentinel-port.env"
write_env "$invalid_redis_sentinel_port_env" "release-download-secret-32-bytes-minimum"
cat >>"$invalid_redis_sentinel_port_env" <<'EOF'
SPRING_DATA_REDIS_SENTINEL_MASTER=mymaster
SPRING_DATA_REDIS_SENTINEL_NODES=sentinel-a.example.com:70000
EOF
expect_fail "$invalid_redis_sentinel_port_env" "SPRING_DATA_REDIS_SENTINEL_NODES port must be between 1 and 65535"
invalid_redis_sentinel_check_env="$tmp/invalid-redis-sentinel-check.env"
write_env "$invalid_redis_sentinel_check_env" "release-download-secret-32-bytes-minimum"
printf '%s\n' "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST=yes" >>"$invalid_redis_sentinel_check_env"
expect_fail "$invalid_redis_sentinel_check_env" "SKILLHUB_REDIS_SENTINEL_CHECK_SENTINELS_LIST must be true or false"
# An OAuth client id without its secret (or vice versa) leaves the provider half-configured:
# the login button renders but the exchange fails. Checked for every supported provider.
for provider in GITHUB GITLAB FEISHU; do
missing_oauth_secret_env="$tmp/missing-oauth-secret.env"
write_env "$missing_oauth_secret_env" "release-download-secret-32-bytes-minimum"
printf 'OAUTH2_%s_CLIENT_ID=real-client-id\n' "$provider" >>"$missing_oauth_secret_env"
expect_fail "$missing_oauth_secret_env" "OAUTH2_${provider}_CLIENT_SECRET is required"
missing_oauth_id_env="$tmp/missing-oauth-id.env"
write_env "$missing_oauth_id_env" "release-download-secret-32-bytes-minimum"
printf 'OAUTH2_%s_CLIENT_SECRET=real-client-secret\n' "$provider" >>"$missing_oauth_id_env"
expect_fail "$missing_oauth_id_env" "OAUTH2_${provider}_CLIENT_ID is required"
done
# A fully configured provider pair must pass.
valid_oauth_env="$tmp/valid-oauth.env"
write_env "$valid_oauth_env" "release-download-secret-32-bytes-minimum"
cat >>"$valid_oauth_env" <<'EOF'
OAUTH2_FEISHU_CLIENT_ID=cli_release_example
OAUTH2_FEISHU_CLIENT_SECRET=release-feishu-secret
EOF
"$SCRIPT" "$valid_oauth_env" >/dev/null
draft_env="$tmp/draft.env"
while IFS= read -r line || [[ -n "$line" ]]; do
case "$line" in
SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=*)
printf '%s\n' "SKILLHUB_DOWNLOAD_ANON_COOKIE_SECRET=release-download-secret-32-bytes-minimum"
;;
*)
printf '%s\n' "$line"
;;
esac
done <"$REPO_ROOT/.env.release.draft" >"$draft_env"
expect_fail "$draft_env" "POSTGRES_PASSWORD"
echo "validate-release-config-test passed"