diff --git a/.gitea/workflows/cd-dev.yaml b/.gitea/workflows/cd-dev.yaml index 525f1158b..6f2bf5e78 100644 --- a/.gitea/workflows/cd-dev.yaml +++ b/.gitea/workflows/cd-dev.yaml @@ -87,11 +87,18 @@ jobs: echo "✅ API 測試通過" - name: Login to Harbor - uses: docker/login-action@v3 - with: - registry: ${{ env.HARBOR }} - username: ${{ secrets.HARBOR_USERNAME }} - password: ${{ secrets.HARBOR_PASSWORD }} + run: | + HARBOR_USERNAME="$(cat <<'AWOOOI_SECRET_HARBOR_USERNAME' + ${{ secrets.HARBOR_USERNAME }} + AWOOOI_SECRET_HARBOR_USERNAME + )" + HARBOR_PASSWORD="$(cat <<'AWOOOI_SECRET_HARBOR_PASSWORD' + ${{ secrets.HARBOR_PASSWORD }} + AWOOOI_SECRET_HARBOR_PASSWORD + )" + printf '%s' "$HARBOR_PASSWORD" | docker login "${{ env.HARBOR }}" \ + -u "$HARBOR_USERNAME" \ + --password-stdin # Dev API 鏡像:強制重建,不用 cache(確保 models.json 等配置文件更新) - name: Build and Push API (Dev) @@ -107,16 +114,37 @@ jobs: # 注入 Dev K8s Secrets - name: Inject Dev K8s Secrets - env: - SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} - TG_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} run: | + secret_b64() { + python3 -c 'import base64, sys; data=sys.stdin.buffer.read(); data=data[:-1] if data.endswith(b"\n") else data; sys.stdout.write(base64.b64encode(data).decode())' + } + write_deploy_key() { + mkdir -p ~/.ssh + umask 077 + cat > ~/.ssh/deploy_key <<'AWOOOI_DEPLOY_KEY' + ${{ secrets.DEPLOY_SSH_KEY }} + AWOOOI_DEPLOY_KEY + chmod 600 ~/.ssh/deploy_key + } + TG_BOT_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_BOT_TOKEN' + ${{ secrets.TELEGRAM_BOT_TOKEN }} + AWOOOI_SECRET_TG_BOT_TOKEN + )" + TG_CHAT_ID_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_CHAT_ID' + ${{ secrets.TELEGRAM_CHAT_ID }} + AWOOOI_SECRET_TG_CHAT_ID + )" + NVIDIA_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_NVIDIA_API_KEY' + ${{ secrets.NVIDIA_API_KEY }} + AWOOOI_SECRET_NVIDIA_API_KEY + )" + GEMINI_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_GEMINI_API_KEY' + ${{ secrets.GEMINI_API_KEY }} + AWOOOI_SECRET_GEMINI_API_KEY + )" + mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key + write_deploy_key # 2026-05-05 Codex: kubectl runs on 120 control-plane. 121 is a # worker and its local kubeconfig points at 127.0.0.1:6443. ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.120 << SECRETS @@ -124,19 +152,19 @@ jobs: export KUBECONFIG=/etc/rancher/k3s/k3s.yaml sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[ - {"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'"$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)"'"}, - {"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'"$(echo -n "${TG_CHAT_ID}" | base64 -w 0)"'"} + {"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"${TG_BOT_TOKEN_B64}"}, + {"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"${TG_CHAT_ID_B64}"} ]' || echo "⚠️ Telegram Secrets patch 跳過" - if [ -n "${NVIDIA_API_KEY}" ]; then + if [ -n "${NVIDIA_API_KEY_B64}" ]; then sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[ - {"op":"replace","path":"/data/NVIDIA_API_KEY","value":"'"$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)"'"} + {"op":"replace","path":"/data/NVIDIA_API_KEY","value":"${NVIDIA_API_KEY_B64}"} ]' && echo "✅ NVIDIA_API_KEY 已注入 dev" fi - if [ -n "${GEMINI_API_KEY}" ]; then + if [ -n "${GEMINI_API_KEY_B64}" ]; then sudo kubectl patch secret awoooi-secrets -n awoooi-dev --type='json' -p='[ - {"op":"replace","path":"/data/GEMINI_API_KEY","value":"'"$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)"'"} + {"op":"replace","path":"/data/GEMINI_API_KEY","value":"${GEMINI_API_KEY_B64}"} ]' && echo "✅ GEMINI_API_KEY 已注入 dev" fi @@ -145,8 +173,6 @@ jobs: # 部署到 awoooi-dev - name: Deploy to Dev K8s - env: - SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | cat k8s/awoooi-dev/02-configmap.yaml | \ ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key wooo@192.168.0.120 \ diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 5e629db10..5f3b381ed 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -76,6 +76,9 @@ jobs: - uses: actions/checkout@v4 + - name: Guard Workflow Secret Surfaces + run: ruby scripts/ci/check-gitea-step-env-secrets.rb + # 2026-03-31 ogt: 優化告警格式 - 提高可讀性 - name: Get Commit Info id: commit @@ -434,44 +437,121 @@ jobs: # 2026-03-31 ogt: P0-1 Secrets 自動注入 (ADR-035 強制) # 2026-03-31 ogt: 加入 AI API Keys (修復 mock_fallback 問題) - name: Inject K8s Secrets - env: - SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} - TG_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} - GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - # 2026-04-01 Claude Code: Langfuse LLMOps keys (Phase 15.1 補齊 CD 注入) - LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} - LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }} - # 2026-04-02 Claude Code: Telegram 白名單 (授權簽核用) - TG_USER_WHITELIST: ${{ secrets.OPENCLAW_TG_USER_WHITELIST }} - # Phase O-4.1 2026-04-02: Sentry API Token (Wave A.1 ADR-037) - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - # ADR-059 2026-04-05: Gitea Webhook Secret (GITEA_ 前綴為保留字,改用 AWOOOI_ 前綴) - GITEA_WEBHOOK_SECRET: ${{ secrets.AWOOOI_GITEA_WEBHOOK_SECRET }} - # MCP Phase 3: ArgoCD API Token (2026-04-11 Claude Sonnet 4.6) - ARGOCD_API_TOKEN: ${{ secrets.ARGOCD_API_TOKEN }} - # 2026-04-18 ogt + Claude Opus 4.7: ADR-090-B L3-only 升級 L2(永久連線串 + 應用 secret) - DATABASE_URL: ${{ secrets.DATABASE_URL }} - MIGRATION_DATABASE_URL: ${{ secrets.MIGRATION_DATABASE_URL }} - REDIS_URL: ${{ secrets.REDIS_URL }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - JWT_ALGORITHM: ${{ secrets.JWT_ALGORITHM }} - WEBHOOK_HMAC_SECRET: ${{ secrets.WEBHOOK_HMAC_SECRET }} - AWOOOP_OPERATOR_API_KEY: ${{ secrets.AWOOOP_OPERATOR_API_KEY }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} - # AWOOOI_ 前綴避開 Gitea 保留字(同 AWOOOI_GITEA_WEBHOOK_SECRET 模式) - GITEA_API_TOKEN: ${{ secrets.AWOOOI_GITEA_API_TOKEN }} - NEMOTRON_BOT_TOKEN: ${{ secrets.NEMOTRON_BOT_TOKEN }} - OPENCLAW_BOT_TOKEN: ${{ secrets.OPENCLAW_BOT_TOKEN }} - SMTP_HOST: ${{ secrets.SMTP_HOST }} - SRE_GROUP_CHAT_ID: ${{ secrets.SRE_GROUP_CHAT_ID }} run: | + # 2026-05-18 Codex: 不把 secrets 放進 step-level env。 + # Gitea/act_runner 的 job log 可能展開 env;這裡只在 shell 內短暫轉 + # base64,並避免輸出原值。 + secret_b64() { + python3 -c 'import base64, sys; data=sys.stdin.buffer.read(); data=data[:-1] if data.endswith(b"\n") else data; sys.stdout.write(base64.b64encode(data).decode())' + } + write_deploy_key() { + mkdir -p "${HOME}/.ssh" + umask 077 + cat > "${HOME}/.ssh/deploy_key" <<'AWOOOI_DEPLOY_KEY' + ${{ secrets.DEPLOY_SSH_KEY }} + AWOOOI_DEPLOY_KEY + chmod 600 "${HOME}/.ssh/deploy_key" + } + + TG_BOT_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_BOT_TOKEN' + ${{ secrets.TELEGRAM_BOT_TOKEN }} + AWOOOI_SECRET_TG_BOT_TOKEN + )" + TG_CHAT_ID_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_CHAT_ID' + ${{ secrets.TELEGRAM_CHAT_ID }} + AWOOOI_SECRET_TG_CHAT_ID + )" + NVIDIA_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_NVIDIA_API_KEY' + ${{ secrets.NVIDIA_API_KEY }} + AWOOOI_SECRET_NVIDIA_API_KEY + )" + GEMINI_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_GEMINI_API_KEY' + ${{ secrets.GEMINI_API_KEY }} + AWOOOI_SECRET_GEMINI_API_KEY + )" + LANGFUSE_PUBLIC_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_LANGFUSE_PUBLIC_KEY' + ${{ secrets.LANGFUSE_PUBLIC_KEY }} + AWOOOI_SECRET_LANGFUSE_PUBLIC_KEY + )" + LANGFUSE_SECRET_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_LANGFUSE_SECRET_KEY' + ${{ secrets.LANGFUSE_SECRET_KEY }} + AWOOOI_SECRET_LANGFUSE_SECRET_KEY + )" + TG_USER_WHITELIST_B64="$(secret_b64 <<'AWOOOI_SECRET_TG_USER_WHITELIST' + ${{ secrets.OPENCLAW_TG_USER_WHITELIST }} + AWOOOI_SECRET_TG_USER_WHITELIST + )" + SENTRY_AUTH_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_SENTRY_AUTH_TOKEN' + ${{ secrets.SENTRY_AUTH_TOKEN }} + AWOOOI_SECRET_SENTRY_AUTH_TOKEN + )" + GITEA_WEBHOOK_SECRET_B64="$(secret_b64 <<'AWOOOI_SECRET_GITEA_WEBHOOK_SECRET' + ${{ secrets.AWOOOI_GITEA_WEBHOOK_SECRET }} + AWOOOI_SECRET_GITEA_WEBHOOK_SECRET + )" + ARGOCD_API_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_ARGOCD_API_TOKEN' + ${{ secrets.ARGOCD_API_TOKEN }} + AWOOOI_SECRET_ARGOCD_API_TOKEN + )" + DATABASE_URL_B64="$(secret_b64 <<'AWOOOI_SECRET_DATABASE_URL' + ${{ secrets.DATABASE_URL }} + AWOOOI_SECRET_DATABASE_URL + )" + MIGRATION_DATABASE_URL_B64="$(secret_b64 <<'AWOOOI_SECRET_MIGRATION_DATABASE_URL' + ${{ secrets.MIGRATION_DATABASE_URL }} + AWOOOI_SECRET_MIGRATION_DATABASE_URL + )" + REDIS_URL_B64="$(secret_b64 <<'AWOOOI_SECRET_REDIS_URL' + ${{ secrets.REDIS_URL }} + AWOOOI_SECRET_REDIS_URL + )" + JWT_SECRET_B64="$(secret_b64 <<'AWOOOI_SECRET_JWT_SECRET' + ${{ secrets.JWT_SECRET }} + AWOOOI_SECRET_JWT_SECRET + )" + JWT_ALGORITHM_B64="$(secret_b64 <<'AWOOOI_SECRET_JWT_ALGORITHM' + ${{ secrets.JWT_ALGORITHM }} + AWOOOI_SECRET_JWT_ALGORITHM + )" + WEBHOOK_HMAC_SECRET_B64="$(secret_b64 <<'AWOOOI_SECRET_WEBHOOK_HMAC_SECRET' + ${{ secrets.WEBHOOK_HMAC_SECRET }} + AWOOOI_SECRET_WEBHOOK_HMAC_SECRET + )" + AWOOOP_OPERATOR_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_AWOOOP_OPERATOR_API_KEY' + ${{ secrets.AWOOOP_OPERATOR_API_KEY }} + AWOOOI_SECRET_AWOOOP_OPERATOR_API_KEY + )" + SENTRY_DSN_B64="$(secret_b64 <<'AWOOOI_SECRET_SENTRY_DSN' + ${{ secrets.SENTRY_DSN }} + AWOOOI_SECRET_SENTRY_DSN + )" + CLAUDE_API_KEY_B64="$(secret_b64 <<'AWOOOI_SECRET_CLAUDE_API_KEY' + ${{ secrets.CLAUDE_API_KEY }} + AWOOOI_SECRET_CLAUDE_API_KEY + )" + GITEA_API_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_GITEA_API_TOKEN' + ${{ secrets.AWOOOI_GITEA_API_TOKEN }} + AWOOOI_SECRET_GITEA_API_TOKEN + )" + NEMOTRON_BOT_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_NEMOTRON_BOT_TOKEN' + ${{ secrets.NEMOTRON_BOT_TOKEN }} + AWOOOI_SECRET_NEMOTRON_BOT_TOKEN + )" + OPENCLAW_BOT_TOKEN_B64="$(secret_b64 <<'AWOOOI_SECRET_OPENCLAW_BOT_TOKEN' + ${{ secrets.OPENCLAW_BOT_TOKEN }} + AWOOOI_SECRET_OPENCLAW_BOT_TOKEN + )" + SMTP_HOST_B64="$(secret_b64 <<'AWOOOI_SECRET_SMTP_HOST' + ${{ secrets.SMTP_HOST }} + AWOOOI_SECRET_SMTP_HOST + )" + SRE_GROUP_CHAT_ID_B64="$(secret_b64 <<'AWOOOI_SECRET_SRE_GROUP_CHAT_ID' + ${{ secrets.SRE_GROUP_CHAT_ID }} + AWOOOI_SECRET_SRE_GROUP_CHAT_ID + )" + # S1/S2: 統一命名 deploy_key,改用 ssh-keyscan(比 StrictHostKeyChecking=no 更安全) - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > "${HOME}/.ssh/deploy_key" - chmod 600 "${HOME}/.ssh/deploy_key" + write_deploy_key # 2026-05-13 Codex: keyscan must include ED25519 explicitly. Some # OpenSSH builds otherwise record only RSA/ECDSA, then strict deploy # SSH fails with "No ED25519 host key is known" after image push. @@ -485,69 +565,69 @@ jobs: # 注入 Telegram Secrets (ADR-035 鐵律) \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)'"}, - {"op":"add","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64 -w 0)'"} + {"op":"add","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"${TG_BOT_TOKEN_B64}"}, + {"op":"add","path":"/data/OPENCLAW_TG_CHAT_ID","value":"${TG_CHAT_ID_B64}"} ]' || { echo "❌ Telegram Secrets patch 失敗 — ADR-035 鐵律"; exit 1; } # 2026-03-31 ogt: 注入 AI API Keys (修復 NVIDIA/Gemini mock_fallback) # 2026-04-01 Claude Code: base64 -w 0 防止長 key 換行破壞 JSON # NVIDIA NIM (免費 tier) - if [ -n "${NVIDIA_API_KEY}" ] && [ "${NVIDIA_API_KEY}" != "" ]; then + if [ -n "${NVIDIA_API_KEY_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)'"} + {"op":"add","path":"/data/NVIDIA_API_KEY","value":"${NVIDIA_API_KEY_B64}"} ]' && echo "✅ NVIDIA_API_KEY 已注入" || echo "⚠️ NVIDIA_API_KEY patch 失敗" else echo "⚠️ NVIDIA_API_KEY 未設定,跳過" fi # Gemini (備援) - if [ -n "${GEMINI_API_KEY}" ] && [ "${GEMINI_API_KEY}" != "" ]; then + if [ -n "${GEMINI_API_KEY_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)'"} + {"op":"add","path":"/data/GEMINI_API_KEY","value":"${GEMINI_API_KEY_B64}"} ]' && echo "✅ GEMINI_API_KEY 已注入" || echo "⚠️ GEMINI_API_KEY patch 失敗" else echo "⚠️ GEMINI_API_KEY 未設定,跳過" fi # 2026-04-01 Claude Code: Langfuse LLMOps keys (補齊 CD 注入,之前只有手動設定) - if [ -n "${LANGFUSE_PUBLIC_KEY}" ] && [ -n "${LANGFUSE_SECRET_KEY}" ]; then + if [ -n "${LANGFUSE_PUBLIC_KEY_B64}" ] && [ -n "${LANGFUSE_SECRET_KEY_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/LANGFUSE_PUBLIC_KEY","value":"'$(echo -n "${LANGFUSE_PUBLIC_KEY}" | base64 -w 0)'"}, - {"op":"add","path":"/data/LANGFUSE_SECRET_KEY","value":"'$(echo -n "${LANGFUSE_SECRET_KEY}" | base64 -w 0)'"} + {"op":"add","path":"/data/LANGFUSE_PUBLIC_KEY","value":"${LANGFUSE_PUBLIC_KEY_B64}"}, + {"op":"add","path":"/data/LANGFUSE_SECRET_KEY","value":"${LANGFUSE_SECRET_KEY_B64}"} ]' && echo "✅ LANGFUSE keys 已注入" || echo "⚠️ LANGFUSE keys patch 失敗" else echo "⚠️ LANGFUSE_PUBLIC_KEY/SECRET_KEY 未設定,跳過 (現有 K8s secret 值維持不變)" fi # 2026-04-02 Claude Code: Telegram Whitelist (授權簽核用戶 ID) - if [ -n "${TG_USER_WHITELIST}" ]; then + if [ -n "${TG_USER_WHITELIST_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/OPENCLAW_TG_USER_WHITELIST","value":"'$(echo -n "${TG_USER_WHITELIST}" | base64 -w 0)'"} + {"op":"add","path":"/data/OPENCLAW_TG_USER_WHITELIST","value":"${TG_USER_WHITELIST_B64}"} ]' && echo "✅ TG_USER_WHITELIST 已注入" || echo "⚠️ TG_USER_WHITELIST patch 失敗" fi # Phase O-4.1 2026-04-02: Sentry Auth Token (Wave A.1 ADR-037) - if [ -n "${SENTRY_AUTH_TOKEN}" ]; then + if [ -n "${SENTRY_AUTH_TOKEN_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/SENTRY_AUTH_TOKEN","value":"'$(echo -n "${SENTRY_AUTH_TOKEN}" | base64 -w 0)'"} + {"op":"add","path":"/data/SENTRY_AUTH_TOKEN","value":"${SENTRY_AUTH_TOKEN_B64}"} ]' && echo "✅ SENTRY_AUTH_TOKEN 已注入" || echo "⚠️ SENTRY_AUTH_TOKEN patch 失敗" else echo "⚠️ SENTRY_AUTH_TOKEN 未設定,Sentry Comment API 將跳過" fi # ADR-059 2026-04-05 Claude Code: Gitea Webhook Secret - if [ -n "${GITEA_WEBHOOK_SECRET}" ]; then + if [ -n "${GITEA_WEBHOOK_SECRET_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/GITEA_WEBHOOK_SECRET","value":"'$(echo -n "${GITEA_WEBHOOK_SECRET}" | base64 -w 0)'"} + {"op":"add","path":"/data/GITEA_WEBHOOK_SECRET","value":"${GITEA_WEBHOOK_SECRET_B64}"} ]' && echo "✅ GITEA_WEBHOOK_SECRET 已注入" || echo "⚠️ GITEA_WEBHOOK_SECRET patch 失敗" else echo "⚠️ GITEA_WEBHOOK_SECRET 未設定,Gitea Webhook 簽章驗證將在 prod 失效" fi # MCP Phase 3: ArgoCD API Token (2026-04-11 Claude Sonnet 4.6) - if [ -n "${ARGOCD_API_TOKEN}" ]; then + if [ -n "${ARGOCD_API_TOKEN_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/ARGOCD_API_TOKEN","value":"'$(echo -n "${ARGOCD_API_TOKEN}" | base64 -w 0)'"} + {"op":"add","path":"/data/ARGOCD_API_TOKEN","value":"${ARGOCD_API_TOKEN_B64}"} ]' && echo "✅ ARGOCD_API_TOKEN 已注入" || echo "⚠️ ARGOCD_API_TOKEN patch 失敗" else echo "⚠️ ARGOCD_API_TOKEN 未設定,ArgoCD MCP 將使用空 token" @@ -560,98 +640,98 @@ jobs: # 注意: 每個 block 與上方維持相同結構(if guard + base64 -w 0 + json patch) # DATABASE_URL — PG 應用連線串(2026-04-18 輪替) - if [ -n "${DATABASE_URL}" ]; then + if [ -n "${DATABASE_URL_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/DATABASE_URL","value":"'$(echo -n "${DATABASE_URL}" | base64 -w 0)'"} + {"op":"add","path":"/data/DATABASE_URL","value":"${DATABASE_URL_B64}"} ]' && echo "✅ DATABASE_URL 已注入" || echo "⚠️ DATABASE_URL patch 失敗" else echo "⚠️ DATABASE_URL 未設定,awoooi-api 將無法連 PG" fi # MIGRATION_DATABASE_URL — CI migration 用 awoooi_migrator 限權帳號(ADR-090-B) - if [ -n "${MIGRATION_DATABASE_URL}" ]; then + if [ -n "${MIGRATION_DATABASE_URL_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/MIGRATION_DATABASE_URL","value":"'$(echo -n "${MIGRATION_DATABASE_URL}" | base64 -w 0)'"} + {"op":"add","path":"/data/MIGRATION_DATABASE_URL","value":"${MIGRATION_DATABASE_URL_B64}"} ]' && echo "✅ MIGRATION_DATABASE_URL 已注入" || echo "⚠️ MIGRATION_DATABASE_URL patch 失敗" fi # REDIS_URL — Redis 連線(6380 on 188) - if [ -n "${REDIS_URL}" ]; then + if [ -n "${REDIS_URL_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/REDIS_URL","value":"'$(echo -n "${REDIS_URL}" | base64 -w 0)'"} + {"op":"add","path":"/data/REDIS_URL","value":"${REDIS_URL_B64}"} ]' && echo "✅ REDIS_URL 已注入" || echo "⚠️ REDIS_URL patch 失敗" else echo "⚠️ REDIS_URL 未設定" fi # JWT_SECRET / JWT_ALGORITHM — API 認證 - if [ -n "${JWT_SECRET}" ]; then + if [ -n "${JWT_SECRET_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/JWT_SECRET","value":"'$(echo -n "${JWT_SECRET}" | base64 -w 0)'"} + {"op":"add","path":"/data/JWT_SECRET","value":"${JWT_SECRET_B64}"} ]' && echo "✅ JWT_SECRET 已注入" || echo "⚠️ JWT_SECRET patch 失敗" fi - if [ -n "${JWT_ALGORITHM}" ]; then + if [ -n "${JWT_ALGORITHM_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/JWT_ALGORITHM","value":"'$(echo -n "${JWT_ALGORITHM}" | base64 -w 0)'"} + {"op":"add","path":"/data/JWT_ALGORITHM","value":"${JWT_ALGORITHM_B64}"} ]' && echo "✅ JWT_ALGORITHM 已注入" || echo "⚠️ JWT_ALGORITHM patch 失敗" fi # WEBHOOK_HMAC_SECRET — Alertmanager webhook HMAC 簽章 - if [ -n "${WEBHOOK_HMAC_SECRET}" ]; then + if [ -n "${WEBHOOK_HMAC_SECRET_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/WEBHOOK_HMAC_SECRET","value":"'$(echo -n "${WEBHOOK_HMAC_SECRET}" | base64 -w 0)'"} + {"op":"add","path":"/data/WEBHOOK_HMAC_SECRET","value":"${WEBHOOK_HMAC_SECRET_B64}"} ]' && echo "✅ WEBHOOK_HMAC_SECRET 已注入" || echo "⚠️ WEBHOOK_HMAC_SECRET patch 失敗" fi # AWOOOP_OPERATOR_API_KEY — AwoooP Operator mutation endpoints - if [ -n "${AWOOOP_OPERATOR_API_KEY}" ]; then + if [ -n "${AWOOOP_OPERATOR_API_KEY_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/AWOOOP_OPERATOR_API_KEY","value":"'$(echo -n "${AWOOOP_OPERATOR_API_KEY}" | base64 -w 0)'"} + {"op":"add","path":"/data/AWOOOP_OPERATOR_API_KEY","value":"${AWOOOP_OPERATOR_API_KEY_B64}"} ]' && echo "✅ AWOOOP_OPERATOR_API_KEY 已注入" || echo "⚠️ AWOOOP_OPERATOR_API_KEY patch 失敗" fi # SENTRY_DSN — Sentry 錯誤追蹤(不是 auth token) - if [ -n "${SENTRY_DSN}" ]; then + if [ -n "${SENTRY_DSN_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/SENTRY_DSN","value":"'$(echo -n "${SENTRY_DSN}" | base64 -w 0)'"} + {"op":"add","path":"/data/SENTRY_DSN","value":"${SENTRY_DSN_B64}"} ]' && echo "✅ SENTRY_DSN 已注入" || echo "⚠️ SENTRY_DSN patch 失敗" fi # CLAUDE_API_KEY — Claude 備援 LLM - if [ -n "${CLAUDE_API_KEY}" ]; then + if [ -n "${CLAUDE_API_KEY_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/CLAUDE_API_KEY","value":"'$(echo -n "${CLAUDE_API_KEY}" | base64 -w 0)'"} + {"op":"add","path":"/data/CLAUDE_API_KEY","value":"${CLAUDE_API_KEY_B64}"} ]' && echo "✅ CLAUDE_API_KEY 已注入" || echo "⚠️ CLAUDE_API_KEY patch 失敗" fi # GITEA_API_TOKEN — Gitea API Token(從 AWOOOI_GITEA_API_TOKEN 映射) - if [ -n "${GITEA_API_TOKEN}" ]; then + if [ -n "${GITEA_API_TOKEN_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/GITEA_API_TOKEN","value":"'$(echo -n "${GITEA_API_TOKEN}" | base64 -w 0)'"} + {"op":"add","path":"/data/GITEA_API_TOKEN","value":"${GITEA_API_TOKEN_B64}"} ]' && echo "✅ GITEA_API_TOKEN 已注入" || echo "⚠️ GITEA_API_TOKEN patch 失敗" fi # NEMOTRON_BOT_TOKEN / OPENCLAW_BOT_TOKEN — 多 Bot 架構 - if [ -n "${NEMOTRON_BOT_TOKEN}" ]; then + if [ -n "${NEMOTRON_BOT_TOKEN_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/NEMOTRON_BOT_TOKEN","value":"'$(echo -n "${NEMOTRON_BOT_TOKEN}" | base64 -w 0)'"} + {"op":"add","path":"/data/NEMOTRON_BOT_TOKEN","value":"${NEMOTRON_BOT_TOKEN_B64}"} ]' && echo "✅ NEMOTRON_BOT_TOKEN 已注入" || echo "⚠️ NEMOTRON_BOT_TOKEN patch 失敗" fi - if [ -n "${OPENCLAW_BOT_TOKEN}" ]; then + if [ -n "${OPENCLAW_BOT_TOKEN_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/OPENCLAW_BOT_TOKEN","value":"'$(echo -n "${OPENCLAW_BOT_TOKEN}" | base64 -w 0)'"} + {"op":"add","path":"/data/OPENCLAW_BOT_TOKEN","value":"${OPENCLAW_BOT_TOKEN_B64}"} ]' && echo "✅ OPENCLAW_BOT_TOKEN 已注入" || echo "⚠️ OPENCLAW_BOT_TOKEN patch 失敗" fi # SMTP_HOST / SRE_GROUP_CHAT_ID - if [ -n "${SMTP_HOST}" ]; then + if [ -n "${SMTP_HOST_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/SMTP_HOST","value":"'$(echo -n "${SMTP_HOST}" | base64 -w 0)'"} + {"op":"add","path":"/data/SMTP_HOST","value":"${SMTP_HOST_B64}"} ]' && echo "✅ SMTP_HOST 已注入" || echo "⚠️ SMTP_HOST patch 失敗" fi - if [ -n "${SRE_GROUP_CHAT_ID}" ]; then + if [ -n "${SRE_GROUP_CHAT_ID_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ - {"op":"add","path":"/data/SRE_GROUP_CHAT_ID","value":"'$(echo -n "${SRE_GROUP_CHAT_ID}" | base64 -w 0)'"} + {"op":"add","path":"/data/SRE_GROUP_CHAT_ID","value":"${SRE_GROUP_CHAT_ID_B64}"} ]' && echo "✅ SRE_GROUP_CHAT_ID 已注入" || echo "⚠️ SRE_GROUP_CHAT_ID patch 失敗" fi @@ -707,13 +787,18 @@ jobs: # 4. 等待 ArgoCD sync + rollout 完成 # 5. Health Check - name: Deploy to K8s (ArgoCD GitOps) - env: - SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }} - GITEA_TOKEN: ${{ secrets.CD_PUSH_TOKEN }} run: | + write_deploy_key() { + mkdir -p "${HOME}/.ssh" + umask 077 + cat > "${HOME}/.ssh/deploy_key" <<'AWOOOI_DEPLOY_KEY' + ${{ secrets.DEPLOY_SSH_KEY }} + AWOOOI_DEPLOY_KEY + chmod 600 "${HOME}/.ssh/deploy_key" + } + mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > "${HOME}/.ssh/deploy_key" - chmod 600 "${HOME}/.ssh/deploy_key" + write_deploy_key # 2026-05-13 Codex: mirror Inject K8s Secrets host-key handling so the # deploy job never reaches SSH with a known_hosts file missing ED25519. ssh-keyscan -T 5 -t ed25519,rsa,ecdsa "${K8S_SSH_HOST}" > "${HOME}/.ssh/known_hosts" 2>/dev/null @@ -761,7 +846,7 @@ jobs: git commit -m "chore(cd): deploy ${IMAGE_TAG::7} [skip ci]" # 用 token 推送(避免 SSH key 需要額外設定 push 權限) git remote remove gitea 2>/dev/null || true - git remote add gitea http://wooo:${GITEA_TOKEN}@192.168.0.110:3001/wooo/awoooi.git + git remote add gitea "http://wooo:${{ secrets.CD_PUSH_TOKEN }}@192.168.0.110:3001/wooo/awoooi.git" # 先 rebase 避免 non-fast-forward (其他 commit 在 CI 期間已推入) # 2026-04-17 ogt: -X theirs — kustomization.yaml 衝突時採用當次部署的 image tag git fetch gitea main diff --git a/.gitea/workflows/code-review.yaml b/.gitea/workflows/code-review.yaml index 4ba1e2cad..e5695d4fc 100644 --- a/.gitea/workflows/code-review.yaml +++ b/.gitea/workflows/code-review.yaml @@ -30,6 +30,9 @@ jobs: with: fetch-depth: 50 + - name: Guard Workflow Secret Surfaces + run: ruby scripts/ci/check-gitea-step-env-secrets.rb + - name: Skip Stale Main Push id: stale run: | @@ -102,7 +105,6 @@ jobs: - name: Notify Code Review Start if: steps.stale.outputs.skip != 'true' env: - TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TG_CHAT_ID: ${{ env.TELEGRAM_ALERT_CHAT_ID }} SHORT_SHA: ${{ steps.ctx.outputs.short_sha }} BRANCH: ${{ steps.ctx.outputs.branch }} @@ -110,6 +112,10 @@ jobs: FILES_DISPLAY: ${{ steps.ctx.outputs.files_display }} run: | set -euo pipefail + TG_BOT_TOKEN="$(cat <<'AWOOOI_SECRET_TG_BOT_TOKEN' + ${{ secrets.TELEGRAM_BOT_TOKEN }} + AWOOOI_SECRET_TG_BOT_TOKEN + )" html_escape() { sed 's/&/\&/g; s//\>/g'; } COMMIT_ESC="$(printf '%s' "$COMMIT_MSG" | html_escape)" FILES_ESC="$(printf '%s\n' "$FILES_DISPLAY" | html_escape)" @@ -150,11 +156,14 @@ jobs: - name: Notify Code Review Completion if: always() && steps.stale.outputs.skip != 'true' env: - TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TG_CHAT_ID: ${{ env.TELEGRAM_ALERT_CHAT_ID }} SHORT_SHA: ${{ steps.ctx.outputs.short_sha }} run: | set -euo pipefail + TG_BOT_TOKEN="$(cat <<'AWOOOI_SECRET_TG_BOT_TOKEN' + ${{ secrets.TELEGRAM_BOT_TOKEN }} + AWOOOI_SECRET_TG_BOT_TOKEN + )" REPORT=/tmp/code-review-report.json if [ ! -s "$REPORT" ]; then cat > "$REPORT" <<'JSON' diff --git a/.gitea/workflows/deploy-alerts.yaml b/.gitea/workflows/deploy-alerts.yaml index 70f37f348..c220036ec 100644 --- a/.gitea/workflows/deploy-alerts.yaml +++ b/.gitea/workflows/deploy-alerts.yaml @@ -37,7 +37,10 @@ jobs: - name: Setup SSH key run: | mkdir -p ~/.ssh - echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 + umask 077 + cat > ~/.ssh/id_ed25519 <<'AWOOOI_DEPLOY_KEY' + ${{ secrets.DEPLOY_SSH_KEY }} + AWOOOI_DEPLOY_KEY chmod 600 ~/.ssh/id_ed25519 ssh-keyscan 192.168.0.110 >> ~/.ssh/known_hosts diff --git a/.gitea/workflows/run-migration.yml b/.gitea/workflows/run-migration.yml index bc12b58d9..cb2002fc8 100644 --- a/.gitea/workflows/run-migration.yml +++ b/.gitea/workflows/run-migration.yml @@ -72,14 +72,19 @@ jobs: - name: Apply new migrations if: steps.diff.outputs.new_files != '' - env: - # 從 Gitea secrets 取,不直接明碼輸出。 - # MIGRATION_DATABASE_URL 是限權帳號;DATABASE_URL 只在 PostgreSQL - # 明確回報「必須是 table owner」時作為受控 fallback。 - PGURL: ${{ secrets.MIGRATION_DATABASE_URL }} - OWNER_PGURL: ${{ secrets.DATABASE_URL }} run: | set -euo pipefail + # 從 Gitea secrets 取,不放 step-level env,避免 runner log 展開。 + # MIGRATION_DATABASE_URL 是限權帳號;DATABASE_URL 只在 PostgreSQL + # 明確回報「必須是 table owner」時作為受控 fallback。 + PGURL="$(cat <<'AWOOOI_SECRET_MIGRATION_DATABASE_URL' + ${{ secrets.MIGRATION_DATABASE_URL }} + AWOOOI_SECRET_MIGRATION_DATABASE_URL + )" + OWNER_PGURL="$(cat <<'AWOOOI_SECRET_DATABASE_URL' + ${{ secrets.DATABASE_URL }} + AWOOOI_SECRET_DATABASE_URL + )" if [ -z "$PGURL" ]; then echo "::error::MIGRATION_DATABASE_URL secret not set in Gitea" exit 1 @@ -121,11 +126,16 @@ jobs: - name: Seed asset_discovery_run (audit) if: steps.diff.outputs.new_files != '' - env: - PGURL: ${{ secrets.MIGRATION_DATABASE_URL }} - OWNER_PGURL: ${{ secrets.DATABASE_URL }} run: | set -euo pipefail + PGURL="$(cat <<'AWOOOI_SECRET_MIGRATION_DATABASE_URL' + ${{ secrets.MIGRATION_DATABASE_URL }} + AWOOOI_SECRET_MIGRATION_DATABASE_URL + )" + OWNER_PGURL="$(cat <<'AWOOOI_SECRET_DATABASE_URL' + ${{ secrets.DATABASE_URL }} + AWOOOI_SECRET_DATABASE_URL + )" if [ -z "$PGURL" ]; then echo "::error::MIGRATION_DATABASE_URL secret not set in Gitea" exit 1 @@ -179,9 +189,12 @@ jobs: - name: Notify Telegram (if configured) if: always() env: - TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TG_CHAT: ${{ env.TELEGRAM_ALERT_CHAT_ID }} run: | + TG_TOKEN="$(cat <<'AWOOOI_SECRET_TG_TOKEN' + ${{ secrets.TELEGRAM_BOT_TOKEN }} + AWOOOI_SECRET_TG_TOKEN + )" STATUS="${{ job.status }}" CICD_STATUS="success" [ "$STATUS" != "success" ] && CICD_STATUS="failed" diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index c8179fb7b..2174b8107 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,51 @@ +## 2026-05-18 | T44 Gitea workflow secret env 泄漏面收斂 + +**背景**:T43 production verification 時,Gitea job log 顯示 step-level env 有 secret 展開風險。這會讓部署 SSH key、DB URL、Telegram token、AI provider key 等敏感值在 CI log 中留下痕跡,會直接破壞 token hygiene,也會污染後續 AwoooP / SignOz / log matching 的可信度。 + +**修正**: +- `.gitea/workflows/cd.yaml` + - `Inject K8s Secrets` 不再把 secrets 掛在 step-level `env:`。 + - 改用 shell-local heredoc 讀取 secret,短暫轉 base64 後送入 K8s secret patch。 + - `Deploy to K8s (ArgoCD GitOps)` 不再以 step env 傳 `DEPLOY_SSH_KEY` / `CD_PUSH_TOKEN`。 + - CD tests job 新增 `Guard Workflow Secret Surfaces`。 +- `.gitea/workflows/cd-dev.yaml` + - Dev secret injection 移除 `DEPLOY_SSH_KEY` / Telegram / NVIDIA / Gemini step env。 + - Harbor login 改為 shell-local heredoc,不再把 username/password 放 action `with:`。 +- `.gitea/workflows/code-review.yaml` + - Telegram token 不再放 step env。 + - workflow 開頭新增 secret surface guard。 +- `.gitea/workflows/run-migration.yml` + - `MIGRATION_DATABASE_URL` / `DATABASE_URL` / Telegram token 不再放 step env。 + - `Seed asset_discovery_run (audit)` 維持目前 JSON SQL 寫法,避免舊版 `:'commit_sha'` psql bind syntax 錯誤復發。 +- `.gitea/workflows/deploy-alerts.yaml` + - deploy key 改用 heredoc 寫入,不再用 `echo "${{ secrets.DEPLOY_SSH_KEY }}"`。 +- 新增 `scripts/ci/check-gitea-step-env-secrets.rb`: + - 掃描 `.gitea/workflows/*.{yml,yaml}`。 + - 若任何 step `env:` 或 action `with:` 出現 `${{ secrets.* }}`,直接 fail。 + +**verification**: +- `ruby scripts/ci/check-gitea-step-env-secrets.rb`:no Gitea step env/with secrets。 +- `ruby -e 'require "yaml"; Dir[".gitea/workflows/*.{yml,yaml}"].each { |p| YAML.load_file(p) }; puts "workflow yaml ok"'`:pass。 +- workflow `run:` block 經 `${{ ... }}` dummy 替換後 `bash -n`:pass。 +- `rg -n ': \$\{\{ secrets\.' .gitea/workflows`:0 matches。 +- `git diff --check`:pass。 + +**風險與後續**: +- 這一輪修掉 repo 可控的 step env/action input 泄漏面,但已出現在歷史 job log 的 secret 不能靠 commit 消失;需要在 Gitea/runner 端輪換相關 key,並清理或縮短敏感 log retention。 +- workflow run script 內仍會在執行時讀取 Gitea secrets,這是 CI 必要行為;下一波要補的是 runner-level masking policy 與 secret rotation checklist,而不是把 secret 再搬回 env。 + +**目前整體進度**: +- Alertmanager 低風險自動修復主線:約 98%。 +- 完整 AI 自動化管理產品化:約 99%。 +- 告警詳情/歷史/主卡/前端 deep-link 可追溯:約 99%。 +- Telegram approval / reject callback 閉環:約 96%。 +- Truth-chain 對「自動修復成功但驗證降級」的判讀:約 99%。 +- AwoooP MCP 使用可見性:約 90%。 +- 188 OpenClaw runtime hygiene:約 90%。 +- Token hygiene:約 65%。 +- CI/CD secret masking hygiene:約 60%(repo 可控 env/with 泄漏面已擋;仍需外部輪換與 runner log retention)。 +- Gitea infra-lint 可執行性:100%。 + ## 2026-05-18 | T43 AwoooP Run Detail 顯示 legacy/self-built MCP 證據 **背景**:Telegram 截圖與前端 Run detail 雖已能顯示 AwoooP Gateway / channel dossier / remediation dry-run,但仍有一個可見性缺口:許多既有自建 MCP / legacy MCP 呼叫寫在 `mcp_audit_log`,Run detail 只讀 `awooop_mcp_gateway_audit`。結果是 operator 容易誤判「沒有真的用 MCP」,即使 truth-chain API 其實能查到 legacy MCP audit。 diff --git a/scripts/ci/check-gitea-step-env-secrets.rb b/scripts/ci/check-gitea-step-env-secrets.rb new file mode 100755 index 000000000..0172e899b --- /dev/null +++ b/scripts/ci/check-gitea-step-env-secrets.rb @@ -0,0 +1,32 @@ +#!/usr/bin/env ruby +# Guard against putting secrets in Gitea step env/with blocks. +# Gitea/act_runner logs may render those blocks before masking is effective. + +require "yaml" + +workflow_glob = File.expand_path("../../.gitea/workflows/*.{yml,yaml}", __dir__) +violations = [] + +Dir[workflow_glob].sort.each do |path| + doc = YAML.load_file(path) + jobs = doc.fetch("jobs", {}) + jobs.each do |job_name, job| + Array(job["steps"]).each_with_index do |step, index| + %w[env with].each do |section| + Hash(step[section]).each do |key, value| + next unless value.to_s.include?("${{ secrets.") + + violations << "#{path}:#{job_name}:step#{index + 1}:#{section}.#{key}" + end + end + end + end +end + +if violations.any? + warn "Gitea workflow exposes secrets through step env/with:" + violations.each { |violation| warn " - #{violation}" } + exit 1 +end + +puts "no Gitea step env/with secrets"