Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s
240 lines
6 KiB
Bash
Executable file
240 lines
6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: Scripts/forgejo-dispatch.sh <workflow> [--ref <ref>] [--inputs <json>] [--inputs-file <path>] [--return-run-info]
|
|
|
|
Dispatch a Burrow Forgejo workflow through the Forgejo API.
|
|
|
|
Environment:
|
|
FORGEJO_BASE_URL Default: https://git.burrow.net
|
|
FORGEJO_REPO Default: hackclub/burrow
|
|
FORGEJO_PAT Token string
|
|
FORGEJO_PAT_FILE Path to plaintext token
|
|
FORGEJO_DISPATCH_HOST_FALLBACK auto, always, or never. Default: auto
|
|
EOF
|
|
}
|
|
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
base_url="${FORGEJO_BASE_URL:-https://git.burrow.net}"
|
|
repo_slug="${FORGEJO_REPO:-hackclub/burrow}"
|
|
host_fallback="${FORGEJO_DISPATCH_HOST_FALLBACK:-auto}"
|
|
original_args=("$@")
|
|
workflow=""
|
|
ref="main"
|
|
inputs_json=""
|
|
return_run_info="false"
|
|
|
|
case "$host_fallback" in
|
|
auto|always|never) ;;
|
|
*)
|
|
echo "FORGEJO_DISPATCH_HOST_FALLBACK must be auto, always, or never." >&2
|
|
exit 64
|
|
;;
|
|
esac
|
|
|
|
host_dispatch() {
|
|
local reason="$1"
|
|
local fallback="${repo_root}/Scripts/forgejo-dispatch-via-host.sh"
|
|
|
|
if [[ ! -x "$fallback" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
echo "Forgejo API dispatch ${reason}; retrying through the forge host." >&2
|
|
exec "$fallback" "${original_args[@]}"
|
|
}
|
|
|
|
if [[ "$host_fallback" == "always" ]]; then
|
|
if ! host_dispatch "forced by FORGEJO_DISPATCH_HOST_FALLBACK=always"; then
|
|
echo "Forge host dispatcher is not available." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--ref)
|
|
ref="${2:-}"
|
|
shift 2
|
|
;;
|
|
--inputs)
|
|
inputs_json="${2:-}"
|
|
shift 2
|
|
;;
|
|
--inputs-file)
|
|
inputs_json="$(cat "${2:-}")"
|
|
shift 2
|
|
;;
|
|
--return-run-info)
|
|
return_run_info="true"
|
|
shift
|
|
;;
|
|
*)
|
|
if [[ -z "$workflow" ]]; then
|
|
workflow="$1"
|
|
shift
|
|
else
|
|
echo "Unexpected argument: $1" >&2
|
|
usage >&2
|
|
exit 64
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$workflow" ]]; then
|
|
echo "Missing workflow name or file, for example build-release.yml." >&2
|
|
usage >&2
|
|
exit 64
|
|
fi
|
|
|
|
extract_dispatcher_token() {
|
|
python3 - "$1" <<'PY'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
path = Path(sys.argv[1])
|
|
in_forgejo = False
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
if re.match(r"^[A-Za-z0-9_-]+:", line):
|
|
in_forgejo = line.startswith("forgejo:")
|
|
continue
|
|
if not in_forgejo:
|
|
continue
|
|
match = re.match(r"\s+token:\s*['\"]?([^'\"]+)['\"]?\s*$", line)
|
|
if match:
|
|
print(match.group(1))
|
|
break
|
|
PY
|
|
}
|
|
|
|
decrypt_age_secret() {
|
|
local path="$1"
|
|
|
|
if [[ ! -f "$path" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if command -v nix >/dev/null 2>&1; then
|
|
nix run "${repo_root}#agenix" -- -d "$path" 2>/dev/null && return 0
|
|
fi
|
|
|
|
if command -v agenix >/dev/null 2>&1; then
|
|
agenix -d "$path" 2>/dev/null && return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
pat="${FORGEJO_PAT:-}"
|
|
pat_source="none"
|
|
if [[ -n "$pat" ]]; then
|
|
pat_source="FORGEJO_PAT"
|
|
fi
|
|
|
|
if [[ -z "$pat" && -n "${FORGEJO_PAT_FILE:-}" && -f "$FORGEJO_PAT_FILE" ]]; then
|
|
pat="$(tr -d '\r\n' < "$FORGEJO_PAT_FILE")"
|
|
if [[ -n "$pat" ]]; then
|
|
pat_source="FORGEJO_PAT_FILE"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$pat" ]]; then
|
|
for default_file in \
|
|
"${repo_root}/intake/forgejo_dispatch_pat.txt" \
|
|
"${repo_root}/intake/forgejo_nsc_dispatcher.yaml"
|
|
do
|
|
if [[ -s "$default_file" ]]; then
|
|
if [[ "$default_file" == *.yaml ]]; then
|
|
pat="$(extract_dispatcher_token "$default_file" | tr -d '\r\n')"
|
|
else
|
|
pat="$(tr -d '\r\n' < "$default_file")"
|
|
fi
|
|
fi
|
|
if [[ -n "$pat" ]]; then
|
|
pat_source="$default_file"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ -z "$pat" ]]; then
|
|
decrypted_config="$(mktemp)"
|
|
trap 'rm -f "$decrypted_config"' EXIT
|
|
if decrypt_age_secret "${repo_root}/secrets/infra/forgejo-nsc-dispatcher-config.age" > "$decrypted_config"; then
|
|
pat="$(extract_dispatcher_token "$decrypted_config" | tr -d '\r\n')"
|
|
if [[ -n "$pat" ]]; then
|
|
pat_source="secrets/infra/forgejo-nsc-dispatcher-config.age"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$pat" ]]; then
|
|
if [[ "$host_fallback" == "auto" ]]; then
|
|
if host_dispatch "has no local token"; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
echo "Forgejo token not found. Set FORGEJO_PAT or FORGEJO_PAT_FILE." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$inputs_json" ]]; then
|
|
if ! printf '%s\n' "$inputs_json" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
echo "--inputs must be a JSON object." >&2
|
|
exit 64
|
|
fi
|
|
inputs_json="$(printf '%s\n' "$inputs_json" | jq -c 'with_entries(.value |= tostring)')"
|
|
payload="$(jq -n --arg ref "$ref" --argjson inputs "$inputs_json" --argjson return_run_info "$return_run_info" \
|
|
'{ref:$ref, inputs:$inputs} + (if $return_run_info then {return_run_info:true} else {} end)')"
|
|
else
|
|
payload="$(jq -n --arg ref "$ref" --argjson return_run_info "$return_run_info" \
|
|
'{ref:$ref} + (if $return_run_info then {return_run_info:true} else {} end)')"
|
|
fi
|
|
|
|
workflow_path="$(python3 - "$workflow" <<'PY'
|
|
import sys
|
|
import urllib.parse
|
|
|
|
print(urllib.parse.quote(sys.argv[1], safe=""))
|
|
PY
|
|
)"
|
|
|
|
url="${base_url%/}/api/v1/repos/${repo_slug}/actions/workflows/${workflow_path}/dispatches"
|
|
response_file="$(mktemp)"
|
|
trap 'rm -f "$response_file" "${decrypted_config:-}"' EXIT
|
|
|
|
curl_status=0
|
|
http_code="$(
|
|
curl -sS -o "$response_file" -w '%{http_code}' -X POST \
|
|
-H "Authorization: token ${pat}" \
|
|
-H "Content-Type: application/json" \
|
|
--data-binary "$payload" \
|
|
"$url"
|
|
)" || curl_status="$?"
|
|
|
|
if [[ "$curl_status" != "0" || "$http_code" != 2* ]]; then
|
|
if [[ "$host_fallback" == "auto" && "$pat_source" != "FORGEJO_PAT" && ( "$http_code" == "401" || "$http_code" == "403" ) ]]; then
|
|
if host_dispatch "returned HTTP ${http_code} with ${pat_source}"; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
cat "$response_file" >&2 || true
|
|
if [[ "$curl_status" != "0" ]]; then
|
|
exit "$curl_status"
|
|
fi
|
|
exit 22
|
|
fi
|
|
|
|
cat "$response_file"
|
|
if [[ "$return_run_info" != "true" ]]; then
|
|
echo "Dispatched ${workflow} on ${ref}."
|
|
fi
|