Files
gitea-actions/bao-secrets/action.yml
T
Joseph SpikerandClaude Opus 4.8 db71896903 feat: add bao-secrets composite action (public, no secrets) (#545)
Shared actions live in a PUBLIC repo so CI runners can clone them anonymously.
They can't clone the private spikersoft-infrastructure repo (a job token is
scoped to its own repo), which is why the notifications Bao cutover failed with
"Repository not found". bao-secrets is pure curl/jq logic — role_id/secret_id
come from the caller's Actions secrets — so nothing sensitive is exposed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:50:44 +00:00

82 lines
3.5 KiB
YAML

name: "Fetch OpenBao secrets"
description: "AppRole-login to OpenBao and export requested KV v2 secrets into the job env (masked). Phase 1, spikersoft-issues#545."
inputs:
role-id:
description: "AppRole role_id — the repo's BAO_ROLE_ID Actions secret."
required: true
secret-id:
description: "AppRole secret_id — the repo's BAO_SECRET_ID Actions secret."
required: true
bao-addr:
description: "OpenBao address."
required: false
default: "https://bao.spikersoft.com"
secrets:
description: |
One mapping per line: ENV_NAME <kv-v2-path> <field>
The path is exactly what you'd pass to `bao kv get` (mount + logical path).
Blank lines and `#` comments are ignored. Example:
DOCKER_PASSWORD secret/ci/shared/registry password
VIDEOS_S3_KEY secret/ci/backend/minio/videos secret_key
required: true
runs:
using: "composite"
steps:
- shell: bash
env:
BAO_ADDR: ${{ inputs.bao-addr }}
BAO_ROLE_ID: ${{ inputs.role-id }}
BAO_SECRET_ID: ${{ inputs.secret-id }}
BAO_SECRETS: ${{ inputs.secrets }}
run: |
set -euo pipefail
command -v curl >/dev/null || { echo "::error::bao-secrets needs 'curl' on the runner"; exit 1; }
command -v jq >/dev/null || { echo "::error::bao-secrets needs 'jq' on the runner"; exit 1; }
# 1) AppRole login -> short-lived (15m) token. No -f so we can read the
# error body; the token check below is the real gate.
login=$(curl -s --max-time 15 -X POST \
--data "{\"role_id\":\"${BAO_ROLE_ID}\",\"secret_id\":\"${BAO_SECRET_ID}\"}" \
"${BAO_ADDR}/v1/auth/approle/login") || true
TOKEN=$(printf '%s' "$login" | jq -r '.auth.client_token // empty')
if [ -z "$TOKEN" ]; then
echo "::error::OpenBao AppRole login failed: $(printf '%s' "$login" | jq -rc '.errors // "no response / network error"')"
exit 1
fi
echo "::add-mask::$TOKEN"
# 2) fetch each requested secret into $GITHUB_ENV (masked, multiline-safe)
while IFS= read -r line; do
line="${line%%#*}"
# shellcheck disable=SC2086
set -- $line
[ "$#" -eq 0 ] && continue
if [ "$#" -ne 3 ]; then
echo "::error::bad 'secrets' line (need: ENV_NAME <kv-v2-path> <field>): ${line}"
exit 1
fi
env_name="$1"; path="$2"; field="$3"
mount="${path%%/*}"; rest="${path#*/}" # KV v2: <mount>/data/<rest>
resp=$(curl -s --max-time 15 -H "X-Vault-Token: $TOKEN" \
"${BAO_ADDR}/v1/${mount}/data/${rest}") || true
val=$(printf '%s' "$resp" | jq -r --arg f "$field" '.data.data[$f] // empty')
if [ -z "$val" ]; then
echo "::error::no value at '${path}' field '${field}' (wrong path/field, or this role's policy denies it): $(printf '%s' "$resp" | jq -rc '.errors // "empty"')"
exit 1
fi
# mask every line of the value (handles multiline secrets like keys)
while IFS= read -r vline; do [ -n "$vline" ] && echo "::add-mask::$vline"; done <<< "$val"
{
echo "${env_name}<<__BAO_EOF__"
printf '%s\n' "$val"
echo "__BAO_EOF__"
} >> "$GITHUB_ENV"
echo " ✓ ${env_name} <- ${path}#${field}"
done <<< "$BAO_SECRETS"
# 3) drop the token (short-lived anyway — just tidy)
curl -s --max-time 10 -H "X-Vault-Token: $TOKEN" \
-X POST "${BAO_ADDR}/v1/auth/token/revoke-self" >/dev/null 2>&1 || true