From 7672a9bcdc894696df0b174acc7530dfa1ea5adf Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 08:24:12 +0800 Subject: [PATCH] fix(ci): keep secrets out of gitea run logs --- .gitea/workflows/cd-dev.yaml | 69 +++---- .gitea/workflows/cd.yaml | 219 ++++++++++----------- .gitea/workflows/code-review.yaml | 10 +- .gitea/workflows/deploy-alerts.yaml | 10 +- .gitea/workflows/e2e-health.yaml | 12 +- .gitea/workflows/run-migration.yml | 39 ++-- scripts/ci/check-gitea-step-env-secrets.js | 21 +- 7 files changed, 179 insertions(+), 201 deletions(-) diff --git a/.gitea/workflows/cd-dev.yaml b/.gitea/workflows/cd-dev.yaml index e39bcd6f2..c5429ec2c 100644 --- a/.gitea/workflows/cd-dev.yaml +++ b/.gitea/workflows/cd-dev.yaml @@ -38,6 +38,8 @@ jobs: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT - name: Notify Dev Deploy Start + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | MSG="🔧 [DEV] 部署開始 ├ 📝 ${{ steps.commit.outputs.message }} @@ -51,7 +53,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "Dev deploy start notification mirrored through AWOOI API" else - printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text@-" @@ -87,17 +89,12 @@ jobs: echo "✅ API 測試通過" - name: Login to Harbor + env: + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} 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" \ + printf '%s\n' "${HARBOR_PASSWORD}" | docker login "${{ env.HARBOR }}" \ + -u "${HARBOR_USERNAME}" \ --password-stdin # Dev API 鏡像:強制重建,不用 cache(確保 models.json 等配置文件更新) @@ -114,34 +111,34 @@ jobs: # 注入 Dev K8s Secrets - name: Inject Dev K8s Secrets + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + SRE_GROUP_CHAT_ID: ${{ secrets.SRE_GROUP_CHAT_ID }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} 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())' + secret_b64_env() { + local env_name="$1" + SECRET_ENV_NAME="${env_name}" python3 - <<'PY' + import base64 + import os + + data = os.environ.get(os.environ["SECRET_ENV_NAME"], "").encode() + data = data[:-1] if data.endswith(b"\n") else data + print(base64.b64encode(data).decode(), end="") + PY } write_deploy_key() { mkdir -p ~/.ssh umask 077 - cat > ~/.ssh/deploy_key <<'AWOOOI_DEPLOY_KEY' - ${{ secrets.DEPLOY_SSH_KEY }} - AWOOOI_DEPLOY_KEY + printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/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_SRE_GROUP_CHAT_ID_COMPAT' - ${{ secrets.SRE_GROUP_CHAT_ID }} - AWOOOI_SECRET_SRE_GROUP_CHAT_ID_COMPAT - )" - 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 - )" + TG_BOT_TOKEN_B64="$(secret_b64_env TELEGRAM_BOT_TOKEN)" + TG_CHAT_ID_B64="$(secret_b64_env SRE_GROUP_CHAT_ID)" + NVIDIA_API_KEY_B64="$(secret_b64_env NVIDIA_API_KEY)" + GEMINI_API_KEY_B64="$(secret_b64_env GEMINI_API_KEY)" mkdir -p ~/.ssh write_deploy_key @@ -219,6 +216,8 @@ jobs: DEPLOY - name: Notify Dev Deploy Success + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | END_TIME=$(date +%s) DURATION=$((END_TIME - ${{ steps.commit.outputs.start_time }})) @@ -238,7 +237,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "Dev deploy success notification mirrored through AWOOI API" else - printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text@-" @@ -246,6 +245,8 @@ jobs: - name: Notify Dev Deploy Failure if: failure() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | MSG="❌ [DEV] 部署失敗 ├ 📝 ${{ steps.commit.outputs.message }} @@ -259,7 +260,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "Dev deploy failure notification mirrored through AWOOI API" else - printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + printf '%b' "$MSG" | curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text@-" diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index b8242f6c4..ff16ce085 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -124,6 +124,8 @@ jobs: - name: Notify Pipeline Start # 2026-04-16 ogt + Claude Sonnet 4.6: 改用 HTML 結構化格式,提升可讀性 + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | COMMIT_MSG="${{ steps.commit.outputs.message }}" SHORT_SHA="${{ steps.commit.outputs.short_sha }}" @@ -142,7 +144,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "✅ CI/CD start notification mirrored through AWOOI API" else - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text=${MSG}" || echo "TG notify failed (non-fatal): exit=$?" @@ -779,6 +781,8 @@ jobs: - name: Notify Pipeline Failure # 2026-04-30 Codex: tests job failure notifier; no jq dependency for host parity. if: failure() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | COMMIT_MSG="${{ steps.commit.outputs.message }}" SHORT_SHA="${{ steps.commit.outputs.short_sha }}" @@ -794,7 +798,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "✅ CI/CD tests failure notification mirrored through AWOOI API" else - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text=${MSG}" || echo "TG notify failed (non-fatal): exit=$?" @@ -848,10 +852,13 @@ jobs: fi - name: Login to Harbor + env: + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} run: | - echo "${{ secrets.HARBOR_PASSWORD }}" | \ + printf '%s\n' "${HARBOR_PASSWORD}" | \ docker login "${{ env.HARBOR }}" \ - -u "${{ secrets.HARBOR_USERNAME }}" \ + -u "${HARBOR_USERNAME}" \ --password-stdin # 2026-05-21 Codex: AWOOI workflow concurrency and the Docker network @@ -1013,125 +1020,91 @@ jobs: # 2026-03-31 ogt: P0-1 Secrets 自動注入 (ADR-035 強制) # 2026-03-31 ogt: 加入 AI API Keys (修復 mock_fallback 問題) - name: Inject K8s Secrets + env: + ARGOCD_API_TOKEN: ${{ secrets.ARGOCD_API_TOKEN }} + AWOOOI_GITEA_API_TOKEN: ${{ secrets.AWOOOI_GITEA_API_TOKEN }} + AWOOOI_GITEA_WEBHOOK_SECRET: ${{ secrets.AWOOOI_GITEA_WEBHOOK_SECRET }} + AWOOOP_OPERATOR_API_KEY: ${{ secrets.AWOOOP_OPERATOR_API_KEY }} + CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + JWT_ALGORITHM: ${{ secrets.JWT_ALGORITHM }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} + LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }} + MIGRATION_DATABASE_URL: ${{ secrets.MIGRATION_DATABASE_URL }} + NEMOTRON_BOT_TOKEN: ${{ secrets.NEMOTRON_BOT_TOKEN }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + OPENCLAW_BOT_TOKEN: ${{ secrets.OPENCLAW_BOT_TOKEN }} + OPENCLAW_TG_USER_WHITELIST: ${{ secrets.OPENCLAW_TG_USER_WHITELIST }} + REDIS_URL: ${{ secrets.REDIS_URL }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SMTP_HOST: ${{ secrets.SMTP_HOST }} + SRE_GROUP_CHAT_ID: ${{ secrets.SRE_GROUP_CHAT_ID }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + WEBHOOK_HMAC_SECRET: ${{ secrets.WEBHOOK_HMAC_SECRET }} run: | - # 2026-05-18 Codex: 不把 secrets 放進 step-level env。 - # Gitea/act_runner 的 job log 可能展開 env;這裡只在 shell 內短暫轉 - # base64,並避免輸出原值。 - secret_b64() { + # 2026-06-30 Codex: do not inline action secret expressions in run scripts. + # Gitea renders the shell body into job logs before masking. Keep + # secret values in process env, then read by variable name only. + secret_b64_env() { + local env_name="$1" if command -v python3.11 >/dev/null 2>&1; then - python3.11 -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())' + SECRET_ENV_NAME="${env_name}" python3.11 - <<'PY' + import base64 + import os + + data = os.environ.get(os.environ["SECRET_ENV_NAME"], "").encode() + data = data[:-1] if data.endswith(b"\n") else data + print(base64.b64encode(data).decode(), end="") + PY elif command -v python3 >/dev/null 2>&1; then - 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())' + SECRET_ENV_NAME="${env_name}" python3 - <<'PY' + import base64 + import os + + data = os.environ.get(os.environ["SECRET_ENV_NAME"], "").encode() + data = data[:-1] if data.endswith(b"\n") else data + print(base64.b64encode(data).decode(), end="") + PY else - secret_value="$(cat)" + secret_value="$(printenv "${env_name}" || true)" printf '%s' "${secret_value}" | base64 | tr -d '\n' fi } write_deploy_key() { mkdir -p "${HOME}/.ssh" umask 077 - cat > "${HOME}/.ssh/deploy_key" <<'AWOOOI_DEPLOY_KEY' - ${{ secrets.DEPLOY_SSH_KEY }} - AWOOOI_DEPLOY_KEY + printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/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_SRE_GROUP_CHAT_ID_COMPAT' - ${{ secrets.SRE_GROUP_CHAT_ID }} - AWOOOI_SECRET_SRE_GROUP_CHAT_ID_COMPAT - )" - 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 - )" + TG_BOT_TOKEN_B64="$(secret_b64_env TELEGRAM_BOT_TOKEN)" + TG_CHAT_ID_B64="$(secret_b64_env SRE_GROUP_CHAT_ID)" + NVIDIA_API_KEY_B64="$(secret_b64_env NVIDIA_API_KEY)" + GEMINI_API_KEY_B64="$(secret_b64_env GEMINI_API_KEY)" + LANGFUSE_PUBLIC_KEY_B64="$(secret_b64_env LANGFUSE_PUBLIC_KEY)" + LANGFUSE_SECRET_KEY_B64="$(secret_b64_env LANGFUSE_SECRET_KEY)" + TG_USER_WHITELIST_B64="$(secret_b64_env OPENCLAW_TG_USER_WHITELIST)" + SENTRY_AUTH_TOKEN_B64="$(secret_b64_env SENTRY_AUTH_TOKEN)" + GITEA_WEBHOOK_SECRET_B64="$(secret_b64_env AWOOOI_GITEA_WEBHOOK_SECRET)" + ARGOCD_API_TOKEN_B64="$(secret_b64_env ARGOCD_API_TOKEN)" + DATABASE_URL_B64="$(secret_b64_env DATABASE_URL)" + MIGRATION_DATABASE_URL_B64="$(secret_b64_env MIGRATION_DATABASE_URL)" + REDIS_URL_B64="$(secret_b64_env REDIS_URL)" + JWT_SECRET_B64="$(secret_b64_env JWT_SECRET)" + JWT_ALGORITHM_B64="$(secret_b64_env JWT_ALGORITHM)" + WEBHOOK_HMAC_SECRET_B64="$(secret_b64_env WEBHOOK_HMAC_SECRET)" + AWOOOP_OPERATOR_API_KEY_B64="$(secret_b64_env AWOOOP_OPERATOR_API_KEY)" + SENTRY_DSN_B64="$(secret_b64_env SENTRY_DSN)" + CLAUDE_API_KEY_B64="$(secret_b64_env CLAUDE_API_KEY)" + GITEA_API_TOKEN_B64="$(secret_b64_env AWOOOI_GITEA_API_TOKEN)" + NEMOTRON_BOT_TOKEN_B64="$(secret_b64_env NEMOTRON_BOT_TOKEN)" + OPENCLAW_BOT_TOKEN_B64="$(secret_b64_env OPENCLAW_BOT_TOKEN)" + SMTP_HOST_B64="$(secret_b64_env SMTP_HOST)" + SRE_GROUP_CHAT_ID_B64="$(secret_b64_env SRE_GROUP_CHAT_ID)" # S1/S2: 統一命名 deploy_key,改用 ssh-keyscan 與強制 host key 驗證。 write_deploy_key @@ -1377,13 +1350,14 @@ jobs: # 4. 等待 ArgoCD sync + rollout 完成 # 5. Health Check - name: Deploy to K8s (ArgoCD GitOps) + env: + CD_PUSH_TOKEN: ${{ secrets.CD_PUSH_TOKEN }} + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} 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 + printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/deploy_key" chmod 600 "${HOME}/.ssh/deploy_key" } @@ -1468,7 +1442,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:${{ secrets.CD_PUSH_TOKEN }}@192.168.0.110:3001/wooo/awoooi.git" + git remote add gitea "http://wooo:${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 @@ -1735,6 +1709,8 @@ jobs: - name: Notify Pipeline Failure if: failure() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | COMMIT_MSG="${{ steps.commit.outputs.message }}" SHORT_SHA="${{ steps.commit.outputs.short_sha }}" @@ -1751,7 +1727,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "✅ CI/CD build failure notification mirrored through AWOOI API" else - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text=${MSG}" || echo "TG notify failed (non-fatal): exit=$?" @@ -1811,13 +1787,13 @@ jobs: # evidence and notification signal, but no longer blocks CD completion. - name: Alert Chain Smoke Test id: alert_chain_smoke + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} 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 + printf '%s\n' "${DEPLOY_SSH_KEY}" > "${HOME}/.ssh/deploy_key" chmod 600 "${HOME}/.ssh/deploy_key" } collect_observability_statuses() { @@ -2091,6 +2067,7 @@ jobs: - name: Notify Health Check Success env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} SMOKE_RESULT: ${{ steps.smoke.outputs.smoke_status == 'pass' && '✅' || '⚠️' }} ALERT_CHAIN_RESULT: ${{ steps.alert_chain_smoke.outputs.alert_chain_status == 'pass' && '✅' || '⚠️' }} MONITORING_RESULT: ${{ steps.monitoring_coverage.outputs.coverage_status == 'pass' && '✅' || '⚠️' }} @@ -2114,7 +2091,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "✅ CI/CD success notification mirrored through AWOOI API" else - printf '%b' "$TG_MSG" | curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + printf '%b' "$TG_MSG" | curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ --data-urlencode "text@-" || echo "TG notify warning (non-fatal)" fi @@ -2122,6 +2099,8 @@ jobs: - name: Notify Pipeline Failure # 2026-04-16 ogt + Claude Sonnet 4.6: 改用 HTML 結構化格式 if: failure() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | COMMIT_MSG="${{ steps.commit.outputs.message }}" SHORT_SHA="${{ steps.commit.outputs.short_sha }}" @@ -2137,7 +2116,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "✅ CI/CD post-deploy failure notification mirrored through AWOOI API" else - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ -d "parse_mode=HTML" \ --data-urlencode "text=${MSG}" || echo "TG notify failed (non-fatal): exit=$?" diff --git a/.gitea/workflows/code-review.yaml b/.gitea/workflows/code-review.yaml index 165978348..41edc67c2 100644 --- a/.gitea/workflows/code-review.yaml +++ b/.gitea/workflows/code-review.yaml @@ -108,12 +108,9 @@ jobs: BRANCH: ${{ steps.ctx.outputs.branch }} COMMIT_MSG: ${{ steps.ctx.outputs.commit_msg }} FILES_DISPLAY: ${{ steps.ctx.outputs.files_display }} + TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} 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)" @@ -156,12 +153,9 @@ jobs: env: SRE_GROUP_CHAT_ID: ${{ env.SRE_GROUP_CHAT_ID }} SHORT_SHA: ${{ steps.ctx.outputs.short_sha }} + TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} 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 e5fcdcdb1..29ed0022f 100644 --- a/.gitea/workflows/deploy-alerts.yaml +++ b/.gitea/workflows/deploy-alerts.yaml @@ -31,12 +31,12 @@ jobs: python3 -c "import yaml; yaml.safe_load(open('ops/monitoring/slo-rules.yml')); print('SLO YAML OK')" - name: Setup SSH key + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | mkdir -p ~/.ssh umask 077 - cat > ~/.ssh/id_ed25519 <<'AWOOOI_DEPLOY_KEY' - ${{ secrets.DEPLOY_SSH_KEY }} - AWOOOI_DEPLOY_KEY + printf '%s\n' "${DEPLOY_SSH_KEY}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keyscan 192.168.0.110 >> ~/.ssh/known_hosts @@ -45,6 +45,8 @@ jobs: - name: Notify deploy result if: always() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | STATUS="${{ job.status }}" EMOJI="✅" @@ -62,7 +64,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "Alert rule deploy notification mirrored through AWOOI API" else - curl -fS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + curl -fS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${{ env.SRE_GROUP_CHAT_ID }}" \ --data-urlencode "text=${MSG}" || true fi diff --git a/.gitea/workflows/e2e-health.yaml b/.gitea/workflows/e2e-health.yaml index 388c27369..1749ddf10 100644 --- a/.gitea/workflows/e2e-health.yaml +++ b/.gitea/workflows/e2e-health.yaml @@ -52,15 +52,13 @@ jobs: exit 1 - name: Source Provider Freshness Smoke + env: + AWOOOP_OPERATOR_API_KEY: ${{ secrets.AWOOOP_OPERATOR_API_KEY }} run: | SOURCE_CANARY_RUN_REF="gitea-e2e-${GITHUB_RUN_ID:-manual}-${GITHUB_RUN_ATTEMPT:-1}" echo "SOURCE_CANARY_RUN_REF=${SOURCE_CANARY_RUN_REF}" >> "$GITHUB_ENV" echo "SOURCE_LINK_CANARY_WORK_ITEM_ID=source-evidence:sentry:upstream_canary:awoooi-source-link-canary-${SOURCE_CANARY_RUN_REF}" >> "$GITHUB_ENV" - OPERATOR_KEY="$(cat <<'AWOOOI_SECRET_AWOOOP_OPERATOR_API_KEY' - ${{ secrets.AWOOOP_OPERATOR_API_KEY }} - AWOOOI_SECRET_AWOOOP_OPERATOR_API_KEY - )" - AWOOOP_OPERATOR_API_KEY="${OPERATOR_KEY}" \ + AWOOOP_OPERATOR_API_KEY="${AWOOOP_OPERATOR_API_KEY}" \ AWOOOP_OPERATOR_ID=gitea-e2e-health \ python3 scripts/alert_chain_smoke_test.py \ --api-url https://awoooi.wooo.work \ @@ -85,6 +83,8 @@ jobs: - name: Notify Telegram on Failure if: failure() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | MSG="E2E Health Check 失敗;API 健康檢查未通過" if AWOOI_CICD_STATUS=failed \ @@ -95,7 +95,7 @@ jobs: scripts/ci/notify-awoooi-cicd.sh; then echo "E2E failure notification mirrored through AWOOI API" else - curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d chat_id="${{ env.SRE_GROUP_CHAT_ID }}" \ -d parse_mode="HTML" \ -d text="🔴 [E2E Health Check] 失敗%0A%0A📅 $(TZ=Asia/Taipei date '+%Y-%m-%d %H:%M')%0A🔗 API 健康檢查未通過%0A%0A請檢查 K3s 叢集狀態" diff --git a/.gitea/workflows/run-migration.yml b/.gitea/workflows/run-migration.yml index 261b195f8..8157e8db7 100644 --- a/.gitea/workflows/run-migration.yml +++ b/.gitea/workflows/run-migration.yml @@ -70,19 +70,16 @@ jobs: - name: Apply new migrations if: steps.diff.outputs.new_files != '' + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + MIGRATION_DATABASE_URL: ${{ secrets.MIGRATION_DATABASE_URL }} run: | set -euo pipefail - # 從 Gitea secrets 取,不放 step-level env,避免 runner log 展開。 + # 2026-06-30 Codex: keep action secret expressions out of run scripts. # 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 - )" + PGURL="${MIGRATION_DATABASE_URL:-}" + OWNER_PGURL="${DATABASE_URL:-}" if [ -z "$PGURL" ]; then echo "::error::MIGRATION_DATABASE_URL secret not set in Gitea" exit 1 @@ -124,16 +121,13 @@ jobs: - name: Seed asset_discovery_run (audit) if: steps.diff.outputs.new_files != '' + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + MIGRATION_DATABASE_URL: ${{ secrets.MIGRATION_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 - )" + PGURL="${MIGRATION_DATABASE_URL:-}" + OWNER_PGURL="${DATABASE_URL:-}" if [ -z "$PGURL" ]; then echo "::error::MIGRATION_DATABASE_URL secret not set in Gitea" exit 1 @@ -186,11 +180,10 @@ jobs: - name: Notify Telegram (if configured) if: always() + env: + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} run: | - TG_TOKEN="$(cat <<'AWOOOI_SECRET_TG_TOKEN' - ${{ secrets.TELEGRAM_BOT_TOKEN }} - AWOOOI_SECRET_TG_TOKEN - )" + TG_TOKEN="${TELEGRAM_BOT_TOKEN:-}" STATUS="${{ job.status }}" CICD_STATUS="success" [ "$STATUS" != "success" ] && CICD_STATUS="failed" @@ -203,10 +196,10 @@ jobs: echo "Migration notification mirrored through AWOOI API" exit 0 fi - if [ -n "$TG_TOKEN" ] && [ -n "${{ env.SRE_GROUP_CHAT_ID }}" ]; then + if [ -n "$TG_TOKEN" ] && [ -n "${SRE_GROUP_CHAT_ID:-}" ]; then MSG="🗄️ Migration CI: \`${STATUS}\` — commit ${{ github.sha }}" curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ - -d chat_id="${{ env.SRE_GROUP_CHAT_ID }}" \ + -d chat_id="${SRE_GROUP_CHAT_ID}" \ -d parse_mode="Markdown" \ -d text="${MSG}" || true fi diff --git a/scripts/ci/check-gitea-step-env-secrets.js b/scripts/ci/check-gitea-step-env-secrets.js index 24b440026..0ee9e78c4 100755 --- a/scripts/ci/check-gitea-step-env-secrets.js +++ b/scripts/ci/check-gitea-step-env-secrets.js @@ -1,7 +1,10 @@ #!/usr/bin/env node /* - * Guard against putting secrets in Gitea step env/with blocks. - * Gitea/act_runner logs may render those blocks before masking is effective. + * Guard against putting secrets directly in executable Gitea workflow text. + * Gitea/act_runner renders `run:` scripts before masking is effective, so + * `${{ secrets.* }}` must stay out of shell bodies and action `with:` inputs. + * Narrow step `env:` is the least-bad transport here because the printed shell + * body can reference only variable names. */ const fs = require("fs"); @@ -11,6 +14,7 @@ const root = path.resolve(__dirname, "../.."); const workflowDir = path.join(root, ".gitea", "workflows"); const violations = []; const routeViolations = []; +const secretExprPattern = /\$\{\{\s*secrets\./; for (const fileName of fs.readdirSync(workflowDir).sort()) { if (!fileName.endsWith(".yml") && !fileName.endsWith(".yaml")) { @@ -49,7 +53,12 @@ for (const fileName of fs.readdirSync(workflowDir).sort()) { block = null; } - const blockMatch = line.match(/^(\s*)(env|with):\s*$/); + if ((trimmed.startsWith("run:") || trimmed.startsWith("with:")) && secretExprPattern.test(line)) { + const inlineSection = trimmed.startsWith("with:") ? "with" : "run"; + violations.push(`${filePath}:${index + 1}:${inlineSection}`); + } + + const blockMatch = line.match(/^(\s*)(env|with|run):\s*(?:[>|].*)?$/); if (blockMatch) { block = { indent: blockMatch[1].length, @@ -58,14 +67,14 @@ for (const fileName of fs.readdirSync(workflowDir).sort()) { return; } - if (block && line.includes("${{ secrets.")) { + if (block && block.section !== "env" && secretExprPattern.test(line)) { violations.push(`${filePath}:${index + 1}:${block.section}`); } }); } if (violations.length > 0) { - console.error("Gitea workflow exposes secrets through step env/with:"); + console.error("Gitea workflow exposes secrets through run/with text:"); for (const violation of violations) { console.error(` - ${violation}`); } @@ -80,4 +89,4 @@ if (routeViolations.length > 0) { process.exit(1); } -console.log("no Gitea step env/with secrets or legacy Telegram routes"); +console.log("no Gitea run/with secrets or legacy Telegram routes");