293 lines
6.9 KiB
Bash
Executable File
293 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# shellcheck source=scripts/github/_gh_env.sh
|
|
source "$SCRIPT_DIR/_gh_env.sh"
|
|
# shellcheck source=scripts/github/_gh_retry.sh
|
|
source "$SCRIPT_DIR/_gh_retry.sh"
|
|
WORKSPACE_ENV_PY="$(cd "$SCRIPT_DIR/.." && pwd)/env/workspace_env.py"
|
|
|
|
ENV_FILE=""
|
|
ORG="${GITHUB_ORG:-${GH_ORG:-lingjingwanxiang}}"
|
|
VISIBILITY=""
|
|
HOST_OVERRIDE=""
|
|
SHOW_KEYS=0
|
|
DRY_RUN=0
|
|
declare -a SECRET_NAMES=()
|
|
declare -a REPOS=()
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
bash scripts/github/sync-org-secrets-from-env.sh --env-file FILE --org ORG --secret NAME [options]
|
|
|
|
Options:
|
|
--env-file FILE Source env file. Standard KEY=value is recommended.
|
|
Legacy lines like export KEY=value or "KEY": "value",
|
|
are still accepted for migration.
|
|
--org ORG GitHub organization (default: lingjingwanxiang)
|
|
--secret NAME Secret name to sync, repeatable
|
|
--repo NAME Selected repository name, repeatable
|
|
--repos CSV Selected repositories, comma-separated
|
|
--visibility MODE all | private | selected
|
|
--host HOST Override GH_HOST for GitHub Enterprise
|
|
--show-keys List parseable keys from env file and exit
|
|
--dry-run Print planned updates without mutating GitHub
|
|
-h, --help Show help
|
|
|
|
Examples:
|
|
bash scripts/github/sync-org-secrets-from-env.sh \
|
|
--env-file /root/codes/.env \
|
|
--org lingjingwanxiang \
|
|
--secret HARBOR_USERNAME \
|
|
--secret HARBOR_PASSWORD \
|
|
--repo ljwx-bookstore \
|
|
--repo ljwx-dify
|
|
|
|
bash scripts/github/sync-org-secrets-from-env.sh \
|
|
--env-file /root/codes/.env \
|
|
--show-keys
|
|
EOF
|
|
}
|
|
|
|
fail() {
|
|
echo "[错误] $1" >&2
|
|
exit 1
|
|
}
|
|
|
|
normalize_repo_name() {
|
|
local raw="$1"
|
|
local name="$raw"
|
|
|
|
if [[ "$raw" == */* ]]; then
|
|
local owner="${raw%%/*}"
|
|
local repo="${raw##*/}"
|
|
if [[ -n "$ORG" && "$owner" != "$ORG" ]]; then
|
|
fail "仓库 ${raw} 的 owner 与 --org ${ORG} 不一致"
|
|
fi
|
|
name="$repo"
|
|
fi
|
|
|
|
name="$(printf '%s' "$name" | xargs)"
|
|
if [[ -z "$name" ]]; then
|
|
fail "仓库名不能为空"
|
|
fi
|
|
|
|
printf '%s\n' "$name"
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--env-file)
|
|
ENV_FILE="${2:?missing value for --env-file}"
|
|
shift 2
|
|
;;
|
|
--org)
|
|
ORG="${2:?missing value for --org}"
|
|
shift 2
|
|
;;
|
|
--secret)
|
|
SECRET_NAMES+=("${2:?missing value for --secret}")
|
|
shift 2
|
|
;;
|
|
--repo)
|
|
REPOS+=("$(normalize_repo_name "${2:?missing value for --repo}")")
|
|
shift 2
|
|
;;
|
|
--repos)
|
|
IFS=',' read -r -a csv_repos <<<"${2:?missing value for --repos}"
|
|
for repo in "${csv_repos[@]}"; do
|
|
REPOS+=("$(normalize_repo_name "$repo")")
|
|
done
|
|
shift 2
|
|
;;
|
|
--visibility)
|
|
VISIBILITY="${2:?missing value for --visibility}"
|
|
shift 2
|
|
;;
|
|
--host)
|
|
HOST_OVERRIDE="${2:?missing value for --host}"
|
|
shift 2
|
|
;;
|
|
--show-keys)
|
|
SHOW_KEYS=1
|
|
shift
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail "未知参数: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$ENV_FILE" ]]; then
|
|
fail "必须提供 --env-file"
|
|
fi
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
fail "env 文件不存在: $ENV_FILE"
|
|
fi
|
|
|
|
case "${VISIBILITY:-}" in
|
|
""|all|private|selected) ;;
|
|
*)
|
|
fail "--visibility 只能是 all / private / selected"
|
|
;;
|
|
esac
|
|
|
|
if (( SHOW_KEYS == 0 )) && (( ${#SECRET_NAMES[@]} == 0 )); then
|
|
fail "至少提供一个 --secret,或使用 --show-keys"
|
|
fi
|
|
|
|
if [[ -z "$VISIBILITY" ]]; then
|
|
if (( ${#REPOS[@]} > 0 )); then
|
|
VISIBILITY="selected"
|
|
else
|
|
VISIBILITY="private"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$VISIBILITY" == "selected" && ${#REPOS[@]} -eq 0 ]]; then
|
|
fail "visibility=selected 时必须提供 --repo 或 --repos"
|
|
fi
|
|
|
|
if [[ "$VISIBILITY" != "selected" && ${#REPOS[@]} -gt 0 ]]; then
|
|
fail "提供了 --repo/--repos 时,--visibility 必须为 selected"
|
|
fi
|
|
}
|
|
|
|
prepare_gh_auth() {
|
|
if [[ -n "${HOST_OVERRIDE:-}" ]]; then
|
|
export GH_HOST="$HOST_OVERRIDE"
|
|
fi
|
|
|
|
if [[ -n "${GH_TOKEN:-${GITHUB_TOKEN:-}}" ]]; then
|
|
gh_env_prepare
|
|
fi
|
|
|
|
if ! gh auth status >/dev/null 2>&1; then
|
|
fail "gh 未登录,且当前环境未提供 GH_TOKEN/GITHUB_TOKEN"
|
|
fi
|
|
}
|
|
|
|
build_parsed_cache() {
|
|
local output_file="$1"
|
|
if [[ ! -x "$WORKSPACE_ENV_PY" ]]; then
|
|
fail "未找到共享 env 解析器: $WORKSPACE_ENV_PY"
|
|
fi
|
|
python3 "$WORKSPACE_ENV_PY" json --env-file "$ENV_FILE" >"$output_file"
|
|
}
|
|
|
|
list_keys() {
|
|
local parsed_cache="$1"
|
|
python3 - "$parsed_cache" <<'PY'
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
for key in sorted(data):
|
|
print(key)
|
|
PY
|
|
}
|
|
|
|
read_secret_value() {
|
|
local parsed_cache="$1"
|
|
local secret_name="$2"
|
|
python3 - "$parsed_cache" "$secret_name" <<'PY'
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
key = sys.argv[2]
|
|
value = data.get(key)
|
|
if value is None:
|
|
raise SystemExit(2)
|
|
sys.stdout.write(str(value))
|
|
PY
|
|
}
|
|
|
|
unique_repos_csv() {
|
|
if (( ${#REPOS[@]} == 0 )); then
|
|
return 0
|
|
fi
|
|
|
|
python3 - "${REPOS[@]}" <<'PY'
|
|
import sys
|
|
|
|
seen: set[str] = set()
|
|
ordered: list[str] = []
|
|
for repo in sys.argv[1:]:
|
|
name = repo.strip()
|
|
if not name or name in seen:
|
|
continue
|
|
seen.add(name)
|
|
ordered.append(name)
|
|
print(",".join(ordered))
|
|
PY
|
|
}
|
|
|
|
sync_secret() {
|
|
local secret_name="$1"
|
|
local secret_value="$2"
|
|
local repos_csv="$3"
|
|
local label="sync secret ${ORG}/${secret_name}"
|
|
|
|
if (( DRY_RUN == 1 )); then
|
|
if [[ "$VISIBILITY" == "selected" ]]; then
|
|
echo "[dry-run] org=${ORG} secret=${secret_name} visibility=selected repos=${repos_csv}"
|
|
else
|
|
echo "[dry-run] org=${ORG} secret=${secret_name} visibility=${VISIBILITY}"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
local -a cmd=(gh secret set "$secret_name" --org "$ORG" --body "$secret_value")
|
|
if [[ "$VISIBILITY" == "selected" ]]; then
|
|
cmd+=(--repos "$repos_csv")
|
|
else
|
|
cmd+=(--visibility "$VISIBILITY")
|
|
fi
|
|
|
|
gh_retry_command "$label" "${cmd[@]}" >/dev/null
|
|
echo "[ok] 已同步 org secret: ${secret_name}"
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
prepare_gh_auth
|
|
|
|
local parsed_cache=""
|
|
parsed_cache="$(mktemp)"
|
|
trap "rm -f -- '$parsed_cache'" EXIT
|
|
build_parsed_cache "$parsed_cache"
|
|
|
|
if (( SHOW_KEYS == 1 )); then
|
|
list_keys "$parsed_cache"
|
|
exit 0
|
|
fi
|
|
|
|
local repos_csv=""
|
|
repos_csv="$(unique_repos_csv)"
|
|
local secret_name=""
|
|
local secret_value=""
|
|
|
|
for secret_name in "${SECRET_NAMES[@]}"; do
|
|
if ! secret_value="$(read_secret_value "$parsed_cache" "$secret_name")"; then
|
|
fail "env 文件中未找到 secret: ${secret_name}"
|
|
fi
|
|
sync_secret "$secret_name" "$secret_value" "$repos_csv"
|
|
done
|
|
}
|
|
|
|
main "$@"
|