diff --git a/.github/skills/scripts/_environment_helpers.sh b/.github/skills/scripts/_environment_helpers.sh index 8126b910..390ce527 100755 --- a/.github/skills/scripts/_environment_helpers.sh +++ b/.github/skills/scripts/_environment_helpers.sh @@ -192,6 +192,101 @@ get_project_root() { return 1 } +# ensure_charon_encryption_key: Ensure CHARON_ENCRYPTION_KEY is present and valid +# for backend tests. Generates an ephemeral base64-encoded 32-byte key when +# missing or invalid. +ensure_charon_encryption_key() { + local key_source="existing" + local decoded_key_hex="" + local decoded_key_bytes=0 + + generate_key() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 32 | tr -d '\n' + return + fi + + if command -v python3 >/dev/null 2>&1; then + python3 - <<'PY' +import base64 +import os + +print(base64.b64encode(os.urandom(32)).decode()) +PY + return + fi + + echo "" + } + + if [[ -z "${CHARON_ENCRYPTION_KEY:-}" ]]; then + key_source="generated" + CHARON_ENCRYPTION_KEY="$(generate_key)" + fi + + if [[ -z "${CHARON_ENCRYPTION_KEY:-}" ]]; then + if declare -f log_error >/dev/null 2>&1; then + log_error "Could not auto-provision CHARON_ENCRYPTION_KEY (requires openssl or python3)" + else + echo "[ERROR] Could not auto-provision CHARON_ENCRYPTION_KEY (requires openssl or python3)" >&2 + fi + return 1 + fi + + if ! decoded_key_hex=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + key_source="regenerated" + CHARON_ENCRYPTION_KEY="$(generate_key)" + if ! decoded_key_hex=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + if declare -f log_error >/dev/null 2>&1; then + log_error "CHARON_ENCRYPTION_KEY is invalid and regeneration failed" + else + echo "[ERROR] CHARON_ENCRYPTION_KEY is invalid and regeneration failed" >&2 + fi + return 1 + fi + fi + + decoded_key_bytes=$(( ${#decoded_key_hex} / 2 )) + if [[ "$decoded_key_bytes" -ne 32 ]]; then + key_source="regenerated" + CHARON_ENCRYPTION_KEY="$(generate_key)" + if ! decoded_key_hex=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + if declare -f log_error >/dev/null 2>&1; then + log_error "CHARON_ENCRYPTION_KEY has invalid length and regeneration failed" + else + echo "[ERROR] CHARON_ENCRYPTION_KEY has invalid length and regeneration failed" >&2 + fi + return 1 + fi + + decoded_key_bytes=$(( ${#decoded_key_hex} / 2 )) + if [[ "$decoded_key_bytes" -ne 32 ]]; then + if declare -f log_error >/dev/null 2>&1; then + log_error "Could not provision a valid 32-byte CHARON_ENCRYPTION_KEY" + else + echo "[ERROR] Could not provision a valid 32-byte CHARON_ENCRYPTION_KEY" >&2 + fi + return 1 + fi + fi + + export CHARON_ENCRYPTION_KEY + + if [[ "$key_source" == "generated" ]]; then + if declare -f log_info >/dev/null 2>&1; then + log_info "CHARON_ENCRYPTION_KEY not set; generated ephemeral test key" + fi + elif [[ "$key_source" == "regenerated" ]]; then + if declare -f log_warn >/dev/null 2>&1; then + log_warn "CHARON_ENCRYPTION_KEY invalid; generated ephemeral test key" + elif declare -f log_info >/dev/null 2>&1; then + log_info "CHARON_ENCRYPTION_KEY invalid; generated ephemeral test key" + fi + fi + + return 0 +} + # Export functions export -f validate_go_environment export -f validate_python_environment @@ -200,3 +295,4 @@ export -f validate_docker_environment export -f set_default_env export -f validate_project_structure export -f get_project_root +export -f ensure_charon_encryption_key diff --git a/.github/skills/test-backend-coverage.SKILL.md b/.github/skills/test-backend-coverage.SKILL.md index 4131cbcf..aaa609be 100644 --- a/.github/skills/test-backend-coverage.SKILL.md +++ b/.github/skills/test-backend-coverage.SKILL.md @@ -25,6 +25,10 @@ requirements: version: ">=3.8" optional: false environment_variables: + - name: "CHARON_ENCRYPTION_KEY" + description: "Encryption key for backend test runtime. Auto-generated ephemerally by the script if missing/invalid." + default: "(auto-generated for test run)" + required: false - name: "CHARON_MIN_COVERAGE" description: "Minimum coverage percentage required (overrides default)" default: "85" @@ -125,6 +129,7 @@ For use in GitHub Actions or other CI/CD pipelines: | Variable | Required | Default | Description | |----------|----------|---------|-------------| +| CHARON_ENCRYPTION_KEY | No | auto-generated for test run | Backend test encryption key. If missing/invalid, an ephemeral 32-byte base64 key is generated for the run. | | CHARON_MIN_COVERAGE | No | 85 | Minimum coverage percentage required for success | | CPM_MIN_COVERAGE | No | 85 | Legacy name for minimum coverage (fallback) | | PERF_MAX_MS_GETSTATUS_P95 | No | 25ms | Max P95 latency for GetStatus endpoint | diff --git a/.github/skills/test-backend-unit-scripts/run.sh b/.github/skills/test-backend-unit-scripts/run.sh index 8b2e50dd..c763b53b 100755 --- a/.github/skills/test-backend-unit-scripts/run.sh +++ b/.github/skills/test-backend-unit-scripts/run.sh @@ -24,6 +24,7 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" # Validate environment log_step "ENVIRONMENT" "Validating prerequisites" validate_go_environment "1.23" || error_exit "Go 1.23+ is required" +ensure_charon_encryption_key || error_exit "Failed to provision CHARON_ENCRYPTION_KEY for backend tests" # Validate project structure log_step "VALIDATION" "Checking project structure" diff --git a/.github/skills/test-backend-unit.SKILL.md b/.github/skills/test-backend-unit.SKILL.md index 2c342cd9..1a2bc4f3 100644 --- a/.github/skills/test-backend-unit.SKILL.md +++ b/.github/skills/test-backend-unit.SKILL.md @@ -21,7 +21,11 @@ requirements: - name: "go" version: ">=1.23" optional: false -environment_variables: [] +environment_variables: + - name: "CHARON_ENCRYPTION_KEY" + description: "Encryption key for backend test runtime. Auto-generated ephemerally if missing/invalid." + default: "(auto-generated for test run)" + required: false parameters: - name: "verbose" type: "boolean" @@ -106,7 +110,9 @@ For use in GitHub Actions or other CI/CD pipelines: ## Environment Variables -No environment variables are required for this skill. +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| CHARON_ENCRYPTION_KEY | No | auto-generated for test run | Backend test encryption key. If missing/invalid, an ephemeral 32-byte base64 key is generated for the run. | ## Outputs diff --git a/scripts/go-test-coverage.sh b/scripts/go-test-coverage.sh index 3f7ff3c8..cf0b27a7 100755 --- a/scripts/go-test-coverage.sh +++ b/scripts/go-test-coverage.sh @@ -13,25 +13,78 @@ BACKEND_DIR="$ROOT_DIR/backend" COVERAGE_FILE="$BACKEND_DIR/coverage.txt" MIN_COVERAGE="${CHARON_MIN_COVERAGE:-${CPM_MIN_COVERAGE:-85}}" -if [[ -z "${CHARON_ENCRYPTION_KEY:-}" ]]; then - echo "Error: CHARON_ENCRYPTION_KEY is required for backend tests." - echo "Set CHARON_ENCRYPTION_KEY to a base64-encoded 32-byte key before running this script." - exit 1 -fi +generate_test_encryption_key() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 32 | tr -d '\n' + return + fi -DECODED_KEY_HEX="" -if ! DECODED_KEY_HEX=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then - echo "Error: CHARON_ENCRYPTION_KEY is not valid base64." - echo "Provide a base64-encoded key whose decoded length is exactly 32 bytes." - exit 1 -fi + if command -v python3 >/dev/null 2>&1; then + python3 - <<'PY' +import base64 +import os -DECODED_KEY_BYTES=$(( ${#DECODED_KEY_HEX} / 2 )) -if [[ "$DECODED_KEY_BYTES" -ne 32 ]]; then - echo "Error: CHARON_ENCRYPTION_KEY decoded length is ${DECODED_KEY_BYTES} bytes; expected 32 bytes." - echo "Regenerate key (example): openssl rand -base64 32" - exit 1 -fi +print(base64.b64encode(os.urandom(32)).decode()) +PY + return + fi + + echo "" +} + +ensure_encryption_key() { + local key_source="existing" + local decoded_key_hex="" + local decoded_key_bytes=0 + + if [[ -z "${CHARON_ENCRYPTION_KEY:-}" ]]; then + key_source="generated" + CHARON_ENCRYPTION_KEY="$(generate_test_encryption_key)" + fi + + if [[ -z "${CHARON_ENCRYPTION_KEY:-}" ]]; then + echo "Error: Could not provision CHARON_ENCRYPTION_KEY automatically." + echo "Install openssl or python3, or set CHARON_ENCRYPTION_KEY manually to a base64-encoded 32-byte key." + exit 1 + fi + + if ! decoded_key_hex=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + key_source="regenerated" + CHARON_ENCRYPTION_KEY="$(generate_test_encryption_key)" + if ! decoded_key_hex=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + echo "Error: CHARON_ENCRYPTION_KEY could not be decoded and regeneration failed." + echo "Set CHARON_ENCRYPTION_KEY to a valid base64-encoded 32-byte key." + exit 1 + fi + fi + + decoded_key_bytes=$(( ${#decoded_key_hex} / 2 )) + if [[ "$decoded_key_bytes" -ne 32 ]]; then + key_source="regenerated" + CHARON_ENCRYPTION_KEY="$(generate_test_encryption_key)" + if ! decoded_key_hex=$(printf '%s' "$CHARON_ENCRYPTION_KEY" | base64 --decode 2>/dev/null | od -An -tx1 -v | tr -d ' \n'); then + echo "Error: CHARON_ENCRYPTION_KEY has invalid length and regeneration failed." + echo "Set CHARON_ENCRYPTION_KEY to a valid base64-encoded 32-byte key." + exit 1 + fi + + decoded_key_bytes=$(( ${#decoded_key_hex} / 2 )) + if [[ "$decoded_key_bytes" -ne 32 ]]; then + echo "Error: Could not provision a valid 32-byte CHARON_ENCRYPTION_KEY." + exit 1 + fi + fi + + export CHARON_ENCRYPTION_KEY + + if [[ "$key_source" == "generated" ]]; then + echo "Info: CHARON_ENCRYPTION_KEY was not set; generated an ephemeral test key." + elif [[ "$key_source" == "regenerated" ]]; then + echo "Warning: CHARON_ENCRYPTION_KEY was invalid; generated an ephemeral test key." + fi +} + +ensure_encryption_key # Perf asserts are sensitive to -race overhead; loosen defaults for hook runs export PERF_MAX_MS_GETSTATUS_P95="${PERF_MAX_MS_GETSTATUS_P95:-25ms}"