Wire Forge-native release infrastructure
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

This commit is contained in:
Conrad Kramer 2026-06-07 05:51:12 -07:00
parent 97c569fb35
commit 002bd382e9
199 changed files with 14268 additions and 185 deletions

View file

@ -0,0 +1,102 @@
#!/usr/bin/env bash
set -euo pipefail
: "${BURROW_GARAGE_ENDPOINT:?BURROW_GARAGE_ENDPOINT is required}"
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required for Garage backup}"
: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required for Garage backup}"
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
if ! command -v aws >/dev/null 2>&1; then
echo "aws CLI is required for Garage backup" >&2
exit 127
fi
if ! command -v gcloud >/dev/null 2>&1; then
echo "gcloud is required for GCS backup" >&2
exit 127
fi
if ! gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q .; then
Scripts/ci/google-wif-auth.sh
fi
export AWS_DEFAULT_REGION="${BURROW_GARAGE_REGION:-garage}"
export AWS_EC2_METADATA_DISABLED=true
requested_releases=0
requested_packages=0
requested_nix_cache=0
IFS=',' read -r -a requested_items <<< "${BURROW_GARAGE_BACKUP_SET:-all}"
for raw_item in "${requested_items[@]}"; do
item="${raw_item//[[:space:]]/}"
case "$item" in
"" )
;;
all )
requested_releases=1
requested_packages=1
requested_nix_cache=1
;;
release|releases )
requested_releases=1
;;
package|packages )
requested_packages=1
;;
attic|nix|nix-cache|nix_cache )
requested_nix_cache=1
;;
* )
echo "unknown Garage backup set item: $item" >&2
exit 64
;;
esac
done
if [[ "$requested_releases" == "0" && "$requested_packages" == "0" && "$requested_nix_cache" == "0" ]]; then
echo "no Garage backup buckets selected" >&2
exit 1
fi
work_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-garage-backup"
rsync_flags=(--recursive)
if [[ "${BURROW_GARAGE_BACKUP_DELETE:-false}" == "true" ]]; then
rsync_flags+=(--delete-unmatched-destination-objects)
fi
sync_bucket() {
local name="$1"
local garage_bucket="$2"
local gcs_bucket="$3"
local source_dir="${work_root}/${name}"
rm -rf "$source_dir"
mkdir -p "$source_dir"
aws --endpoint-url "$BURROW_GARAGE_ENDPOINT" s3 sync "s3://${garage_bucket}" "$source_dir" --no-progress
gcloud storage rsync "${rsync_flags[@]}" "$source_dir" "gs://${gcs_bucket}"
}
if [[ "$requested_releases" == "1" ]]; then
sync_bucket \
releases \
"${BURROW_RELEASE_GARAGE_BUCKET:-burrow-releases}" \
"${BURROW_RELEASE_GCS_BUCKET:-burrow-net-releases}"
fi
if [[ "$requested_packages" == "1" ]]; then
sync_bucket \
packages \
"${BURROW_PACKAGE_GARAGE_BUCKET:-burrow-packages}" \
"${BURROW_PACKAGE_GCS_BUCKET:-burrow-net-packages}"
fi
if [[ "$requested_nix_cache" == "1" ]]; then
sync_bucket \
nix-cache \
"${BURROW_NIX_CACHE_GARAGE_BUCKET:-attic}" \
"${BURROW_NIX_CACHE_GCS_BUCKET:-burrow-net-nix-cache}"
fi

View file

@ -4,17 +4,8 @@ set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
release_ref="${RELEASE_REF:-manual-${GITHUB_SHA:-unknown}}"
target="x86_64-unknown-linux-gnu"
out_dir="${repo_root}/dist"
staging="${out_dir}/burrow-${release_ref}-${target}"
build_number="${BUILD_NUMBER:-${RELEASE_REF:-manual-${GITHUB_SHA:-unknown}}}"
export BUILD_NUMBER="$build_number"
export BURROW_RELEASE_OUT="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
mkdir -p "${staging}"
cargo build --locked --release -p burrow --bin burrow
install -m 0755 target/release/burrow "${staging}/burrow"
cp README.md "${staging}/README.md"
tarball="${out_dir}/burrow-${release_ref}-${target}.tar.gz"
tar -C "${out_dir}" -czf "${tarball}" "$(basename "${staging}")"
shasum -a 256 "${tarball}" > "${tarball}.sha256"
Scripts/ci/package-release-artifacts.sh "${1:-all}"

View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
require_env() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
echo "missing required environment variable: ${name}" >&2
return 1
fi
}
require_file_or_env() {
local file_name="$1"
local env_name="$2"
if [[ -n "${!file_name:-}" && -f "${!file_name:-}" ]]; then
return 0
fi
if [[ -n "${!env_name:-}" ]]; then
return 0
fi
echo "missing required ${file_name} file or ${env_name} environment value" >&2
return 1
}
case "${1:-all}" in
gcs)
require_env BURROW_GOOGLE_PROJECT_ID
require_env BURROW_GOOGLE_PROJECT_NUMBER
require_env BURROW_GOOGLE_WIF_POOL_ID
require_env BURROW_GOOGLE_WIF_PROVIDER_ID
require_env BURROW_GOOGLE_WIF_SERVICE_ACCOUNT
require_env BURROW_AUTHENTIK_WIF_ISSUER
require_env BURROW_AUTHENTIK_WIF_AUDIENCE
require_env BURROW_AUTHENTIK_WIF_CLIENT_ID
if [[ -z "${BURROW_AUTHENTIK_WIF_TOKEN:-}" && -z "${BURROW_AUTHENTIK_WIF_CLIENT_SECRET:-}" ]]; then
if [[ -z "${BURROW_AUTHENTIK_WIF_TOKEN_FILE:-}" || ! -f "${BURROW_AUTHENTIK_WIF_TOKEN_FILE:-}" ]]; then
echo "missing Authentik WIF token, token file, or client secret" >&2
exit 1
fi
fi
;;
app-store)
require_env ASC_API_KEY_ID
require_env ASC_API_ISSUER_ID
require_file_or_env ASC_API_KEY_PATH ASC_API_KEY_BASE64
;;
apple-signing)
require_file_or_env APPLE_SIGNING_CERTIFICATE_PATH APPLE_SIGNING_CERTIFICATE_BASE64
require_env APPLE_SIGNING_CERTIFICATE_PASSWORD
;;
sparkle)
"$0" gcs
;;
microsoft-store)
require_env MICROSOFT_TENANT_ID
require_env MICROSOFT_CLIENT_ID
require_env MICROSOFT_CLIENT_SECRET
;;
all)
"$0" gcs
"$0" app-store
"$0" apple-signing
;;
*)
echo "unknown release config group: $1" >&2
exit 64
;;
esac

View file

@ -143,13 +143,18 @@ if [[ -e "${config_file}" && ! -w "${config_file}" ]]; then
fi
mkdir -p "$(dirname -- "${config_file}")"
cat > "${config_file}" <<'EOF'
experimental-features = nix-command flakes
sandbox = true
fallback = true
substituters = https://cache.nixos.org
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
EOF
{
echo "experimental-features = nix-command flakes"
echo "sandbox = true"
echo "fallback = true"
if [[ -n "${BURROW_NIX_CACHE_PUBLIC_KEY:-}" ]]; then
echo "substituters = ${BURROW_NIX_CACHE_URL:-https://nix.burrow.net/${BURROW_NIX_CACHE_NAME:-burrow}} https://cache.nixos.org"
echo "trusted-public-keys = ${BURROW_NIX_CACHE_PUBLIC_KEY} cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
else
echo "substituters = https://cache.nixos.org"
echo "trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
fi
} > "${config_file}"
command -v nix >/dev/null 2>&1 || {
echo "nix is still unavailable after bootstrap" >&2

73
Scripts/ci/ensure-nsc.sh Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v nsc >/dev/null 2>&1; then
nsc version || true
exit 0
fi
if ! command -v curl >/dev/null 2>&1; then
echo "::error ::curl is required to install nsc" >&2
exit 1
fi
if ! command -v tar >/dev/null 2>&1; then
echo "::error ::tar is required to install nsc" >&2
exit 1
fi
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
*)
echo "::error ::Unsupported arch for nsc: ${arch}" >&2
exit 1
;;
esac
if [[ "$os" != "linux" ]]; then
echo "::warning ::nsc bootstrap is only supported on Linux; relying on the runner or Nix shell for ${os}."
exit 1
fi
version="${NSC_VERSION:-0.0.506}"
expected_sha256=""
case "${version}/${arch}" in
0.0.506/amd64) expected_sha256="4a093e0269878a9514e7c00d99b88a924f307ad15fb9d1595ee5f6582f99a0f0" ;;
0.0.506/arm64) expected_sha256="6acf074b67fcbaaf59a4437f6bd40da7e38438c427849c1b5fcf4621ce75aee2" ;;
esac
asset="nsc_${version}_linux_${arch}.tar.gz"
url="https://get.namespace.so/packages/nsc/v${version}/${asset}"
tmp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-nsc.$$"
install_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/nsc-bin"
mkdir -p "$tmp_dir" "$install_dir"
trap 'rm -rf "$tmp_dir"' EXIT
curl -fsSL -o "${tmp_dir}/${asset}" "$url"
if [[ -n "$expected_sha256" ]] && command -v sha256sum >/dev/null 2>&1; then
actual_sha256="$(sha256sum "${tmp_dir}/${asset}" | awk '{print $1}')"
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
echo "::error ::nsc ${asset} sha256 mismatch: expected ${expected_sha256}, got ${actual_sha256}" >&2
exit 1
fi
fi
tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
for bin in nsc bazel-credential-nsc docker-credential-nsc; do
if [[ -x "${tmp_dir}/${bin}" ]]; then
install -m 0755 "${tmp_dir}/${bin}" "${install_dir}/${bin}"
fi
done
if [[ ! -x "${install_dir}/nsc" || ! -x "${install_dir}/bazel-credential-nsc" ]]; then
echo "::error ::Failed to install nsc and bazel-credential-nsc from ${asset}" >&2
exit 1
fi
if [[ -n "${GITHUB_PATH:-}" ]]; then
echo "${install_dir}" >> "${GITHUB_PATH}"
fi
export PATH="${install_dir}:$PATH"
nsc version || true

60
Scripts/ci/ensure-spacectl.sh Executable file
View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
if command -v spacectl >/dev/null 2>&1; then
spacectl version || true
exit 0
fi
if ! command -v curl >/dev/null 2>&1; then
echo "::error ::curl is required to install spacectl" >&2
exit 1
fi
if ! command -v tar >/dev/null 2>&1; then
echo "::error ::tar is required to install spacectl" >&2
exit 1
fi
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m)"
case "$arch" in
x86_64|amd64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
*)
echo "::error ::Unsupported arch for spacectl: ${arch}" >&2
exit 1
;;
esac
if [[ "$os" != "linux" && "$os" != "darwin" ]]; then
echo "::error ::Unsupported OS for spacectl: ${os}" >&2
exit 1
fi
version="${SPACECTL_VERSION:-v0.5.0}"
version_num="${version#v}"
asset="spacectl_${version_num}_${os}_${arch}.tar.gz"
url="https://github.com/namespacelabs/spacectl/releases/download/${version}/${asset}"
tmp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-spacectl.$$"
install_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/spacectl-bin"
mkdir -p "$tmp_dir" "$install_dir"
trap 'rm -rf "$tmp_dir"' EXIT
curl -fsSL -o "${tmp_dir}/${asset}" "$url"
tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
bin="${tmp_dir}/spacectl"
if [[ ! -x "$bin" ]]; then
bin="$(find "$tmp_dir" -maxdepth 3 -type f -name spacectl -perm -111 | head -n1 || true)"
fi
if [[ -z "${bin}" || ! -x "${bin}" ]]; then
echo "::error ::Failed to locate spacectl binary in ${asset}" >&2
exit 1
fi
install -m 0755 "$bin" "${install_dir}/spacectl"
if [[ -n "${GITHUB_PATH:-}" ]]; then
echo "${install_dir}" >> "${GITHUB_PATH}"
fi
export PATH="${install_dir}:$PATH"
spacectl version || true

126
Scripts/ci/google-wif-auth.sh Executable file
View file

@ -0,0 +1,126 @@
#!/usr/bin/env bash
set -euo pipefail
project_id="${GOOGLE_CLOUD_PROJECT:-${BURROW_GOOGLE_PROJECT_ID:-project-88c23ce9-918a-470a-b33}}"
project_number="${BURROW_GOOGLE_PROJECT_NUMBER:-416198671487}"
pool_id="${BURROW_GOOGLE_WIF_POOL_ID:-burrow-authentik}"
provider_id="${BURROW_GOOGLE_WIF_PROVIDER_ID:-forgejo-runners}"
service_account="${BURROW_GOOGLE_WIF_SERVICE_ACCOUNT:-burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com}"
authentik_issuer="${BURROW_AUTHENTIK_WIF_ISSUER:-https://auth.burrow.net/application/o/google-cloud/}"
authentik_audience="${BURROW_AUTHENTIK_WIF_AUDIENCE:-google-wif.burrow.net}"
authentik_client_id="${BURROW_AUTHENTIK_WIF_CLIENT_ID:-${AUTHENTIK_GOOGLE_WIF_CLIENT_ID:-google-wif.burrow.net}}"
authentik_client_secret="${BURROW_AUTHENTIK_WIF_CLIENT_SECRET:-${AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET:-}}"
authentik_token_file="${BURROW_AUTHENTIK_WIF_TOKEN_FILE:-}"
authentik_token="${BURROW_AUTHENTIK_WIF_TOKEN:-}"
work_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-google-wif"
emit_env=1
usage() {
cat <<'EOF'
Usage: Scripts/ci/google-wif-auth.sh [--no-emit-env]
Authenticates gcloud to the Burrow Google project through Authentik-issued OIDC
and Google Workload Identity Federation. No Google service-account JSON key is
used or written.
Inputs:
GOOGLE_CLOUD_PROJECT / BURROW_GOOGLE_PROJECT_ID
BURROW_GOOGLE_PROJECT_NUMBER
BURROW_GOOGLE_WIF_POOL_ID
BURROW_GOOGLE_WIF_PROVIDER_ID
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT
BURROW_AUTHENTIK_WIF_ISSUER
BURROW_AUTHENTIK_WIF_AUDIENCE
BURROW_AUTHENTIK_WIF_TOKEN or BURROW_AUTHENTIK_WIF_TOKEN_FILE
If no token is supplied, the script requests one from Authentik using
BURROW_AUTHENTIK_WIF_CLIENT_ID and BURROW_AUTHENTIK_WIF_CLIENT_SECRET.
EOF
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--no-emit-env)
emit_env=0
;;
-h|--help)
usage
exit 0
;;
*)
echo "unknown argument: $1" >&2
exit 64
;;
esac
shift
done
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "$1 is required for Google WIF authentication" >&2
exit 127
fi
}
require_command gcloud
require_command curl
require_command jq
mkdir -p "$work_dir"
chmod 0700 "$work_dir"
token_path="$work_dir/authentik-oidc-token.jwt"
credential_config="$work_dir/google-wif-credentials.json"
if [[ -n "$authentik_token_file" ]]; then
if [[ ! -s "$authentik_token_file" ]]; then
echo "Authentik WIF token file is empty or missing: $authentik_token_file" >&2
exit 1
fi
cp "$authentik_token_file" "$token_path"
elif [[ -n "$authentik_token" ]]; then
printf '%s' "$authentik_token" > "$token_path"
else
if [[ -z "$authentik_client_secret" ]]; then
echo "missing Authentik WIF token. Set BURROW_AUTHENTIK_WIF_TOKEN, BURROW_AUTHENTIK_WIF_TOKEN_FILE, or BURROW_AUTHENTIK_WIF_CLIENT_SECRET." >&2
exit 1
fi
token_endpoint="${authentik_issuer%/}/token/"
curl -fsS \
-u "${authentik_client_id}:${authentik_client_secret}" \
-d grant_type=client_credentials \
-d scope="openid profile email groups" \
-d audience="$authentik_audience" \
"$token_endpoint" \
| jq -r '.id_token // empty' > "$token_path"
fi
chmod 0600 "$token_path"
if [[ ! -s "$token_path" ]]; then
echo "Authentik did not return an OIDC token for Google WIF" >&2
exit 1
fi
provider_resource="projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/providers/${provider_id}"
gcloud iam workload-identity-pools create-cred-config "$provider_resource" \
--service-account "$service_account" \
--subject-token-type="urn:ietf:params:oauth:token-type:jwt" \
--credential-source-file "$token_path" \
--output-file "$credential_config"
chmod 0600 "$credential_config"
gcloud auth login --cred-file="$credential_config" --brief
gcloud config set project "$project_id" >/dev/null
{
echo "GOOGLE_APPLICATION_CREDENTIALS=$credential_config"
echo "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=$credential_config"
echo "CLOUDSDK_CORE_PROJECT=$project_id"
echo "GOOGLE_CLOUD_PROJECT=$project_id"
} > "$work_dir/google-wif.env"
if [[ "$emit_env" == "1" && -n "${GITHUB_ENV:-}" ]]; then
cat "$work_dir/google-wif.env" >> "$GITHUB_ENV"
fi
echo "Authenticated gcloud through Authentik WIF as ${service_account} in ${project_id}."

View file

@ -0,0 +1,124 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"
signing_ready=0
keychain_path=""
write_env() {
local key="$1"
local value="$2"
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "${key}=${value}" >> "$GITHUB_ENV"
fi
}
install_profile_file() {
local source="$1"
[[ -f "$source" ]] || return 0
local uuid
uuid="$(security cms -D -i "$source" 2>/dev/null | plutil -extract UUID raw -o - - 2>/dev/null || true)"
if [[ -z "$uuid" ]]; then
uuid="$(basename "$source")"
fi
local profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$profiles_dir"
cp "$source" "${profiles_dir}/${uuid}.provisionprofile"
echo "Installed provisioning profile ${uuid}."
}
install_profile_base64() {
local name="$1"
local value="${!name:-}"
[[ -n "$value" ]] || return 0
local out="${RUNNER_TEMP:-/tmp}/${name}.provisionprofile"
printf '%s' "$value" | base64 --decode > "$out"
install_profile_file "$out"
}
install_profile_env_path() {
local name="$1"
local path="${!name:-}"
[[ -n "$path" ]] || return 0
if [[ ! -f "$path" ]]; then
echo "::error ::${name} points to a missing provisioning profile: ${path}" >&2
exit 1
fi
install_profile_file "$path"
}
for profile in \
Apple/Profiles/*.provisionprofile \
Apple/Profiles/*.mobileprovision \
.forgejo/actions/export/*.provisionprofile \
.forgejo/actions/export/*.mobileprovision; do
[[ -e "$profile" ]] || continue
install_profile_file "$profile"
done
install_profile_env_path APPLE_PROVISIONING_PROFILE_PATH
install_profile_env_path APPLE_NETWORK_EXTENSION_PROVISIONING_PROFILE_PATH
install_profile_env_path APPLE_MACOS_PROVISIONING_PROFILE_PATH
install_profile_base64 APPLE_PROVISIONING_PROFILE_BASE64
install_profile_base64 APPLE_NETWORK_EXTENSION_PROVISIONING_PROFILE_BASE64
install_profile_base64 APPLE_MACOS_PROVISIONING_PROFILE_BASE64
cert_base64="${APPLE_SIGNING_CERTIFICATE_BASE64:-${APPLE_DISTRIBUTION_CERTIFICATE_BASE64:-}}"
cert_path_env="${APPLE_SIGNING_CERTIFICATE_PATH:-${APPLE_DISTRIBUTION_CERTIFICATE_PATH:-}}"
cert_password="${APPLE_SIGNING_CERTIFICATE_PASSWORD:-${APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD:-}}"
keychain_password="${APPLE_KEYCHAIN_PASSWORD:-${cert_password}}"
if [[ -n "$cert_base64" || -n "$cert_path_env" ]]; then
if [[ -z "$cert_password" ]]; then
echo "::error ::APPLE_SIGNING_CERTIFICATE_PASSWORD is required when Apple signing certificate material is set." >&2
exit 1
fi
cert_path="${RUNNER_TEMP:-/tmp}/burrow-apple-signing.p12"
keychain_path="$HOME/Library/Keychains/BurrowRelease.keychain-db"
if [[ -n "$cert_path_env" ]]; then
if [[ ! -f "$cert_path_env" ]]; then
echo "::error ::APPLE_SIGNING_CERTIFICATE_PATH points to a missing file: ${cert_path_env}" >&2
exit 1
fi
cp "$cert_path_env" "$cert_path"
else
printf '%s' "$cert_base64" | base64 --decode > "$cert_path"
fi
security create-keychain -p "$keychain_password" "$keychain_path" || true
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$cert_path" \
-k "$keychain_path" \
-P "$cert_password" \
-T /usr/bin/codesign \
-T /usr/bin/security \
-T /usr/bin/productbuild \
-T /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" >/dev/null
existing="$(security list-keychains -d user | sed 's/[ "]//g' || true)"
if ! printf '%s\n' "$existing" | grep -Fxq "$keychain_path"; then
security list-keychains -d user -s "$keychain_path" $existing
fi
signing_ready=1
write_env BURROW_APPLE_KEYCHAIN_PATH "$keychain_path"
else
identities="$(security find-identity -v -p codesigning 2>/dev/null || true)"
if printf '%s\n' "$identities" | grep -Eq '"(Apple Distribution|Developer ID Application):'; then
signing_ready=1
fi
fi
write_env BURROW_APPLE_SIGNING_READY "$signing_ready"
if [[ "$signing_ready" == "1" ]]; then
echo "Apple signing assets are available."
else
echo "::notice ::Apple signing certificate is not configured; release lane will build unsigned validation artifacts only."
fi

225
Scripts/ci/nscloud-cache.sh Executable file
View file

@ -0,0 +1,225 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -eq 0 ]]; then
echo "No cache modes specified; skipping."
exit 0
fi
want_bazel_cache=0
for mode in "$@"; do
if [[ "$mode" == "bazel" ]]; then
want_bazel_cache=1
break
fi
done
portable_mkdir() {
hash -r >/dev/null 2>&1 || true
command -p mkdir "$@"
}
append_bazel_storage_cache() {
local bazelrc_path="$1"
local cache_root="${BURROW_BAZEL_STORAGE_CACHE:-}"
if [[ -z "$cache_root" && -n "${NSC_CACHE_PATH:-}" ]]; then
cache_root="${NSC_CACHE_PATH}/bazel"
fi
if [[ -z "$cache_root" ]]; then
return 0
fi
portable_mkdir -p "${cache_root}/disk" "${cache_root}/repository"
{
echo "build --disk_cache=${cache_root}/disk"
echo "build --repository_cache=${cache_root}/repository"
} >> "$bazelrc_path"
echo "Namespace Bazel storage cache configured: ${cache_root}"
}
configure_bazel_remote_cache() {
if [[ "$want_bazel_cache" != "1" ]]; then
return 0
fi
export BURROW_BAZEL_NSCCACHE_ACTIVE=0
if ! command -v nsc >/dev/null 2>&1; then
if bash Scripts/ci/ensure-nsc.sh; then
nsc_bin_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/nsc-bin"
if [[ -x "${nsc_bin_dir}/nsc" ]]; then
export PATH="${nsc_bin_dir}:$PATH"
fi
else
echo "::warning ::nsc not found; using local Bazel disk cache only."
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "BURROW_BAZEL_NSCCACHE_ACTIVE=0" >> "$GITHUB_ENV"
fi
return 0
fi
fi
local bazelrc_path
bazelrc_path="${NSC_BAZELRC_PATH:-${TMPDIR:-/tmp}/burrow-nsc-cache.bazelrc}"
portable_mkdir -p "$(dirname "$bazelrc_path")"
if nsc auth check-login >/dev/null 2>&1 && nsc cache bazel setup --bazelrc "$bazelrc_path" >/dev/null 2>&1; then
append_bazel_storage_cache "$bazelrc_path"
export BURROW_BAZEL_NSCCACHE_BAZELRC="$bazelrc_path"
export BURROW_BAZEL_NSCCACHE_ACTIVE=1
echo "Namespace Bazel cache configured: $bazelrc_path"
if [[ -n "${GITHUB_ENV:-}" ]]; then
{
echo "BURROW_BAZEL_NSCCACHE_ACTIVE=1"
echo "BURROW_BAZEL_NSCCACHE_BAZELRC=$bazelrc_path"
echo "NSC_BAZELRC_PATH=$bazelrc_path"
} >> "$GITHUB_ENV"
fi
else
append_bazel_storage_cache "$bazelrc_path"
echo "::warning ::Namespace Bazel remote cache unavailable; using local Bazel repository/disk cache."
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "BURROW_BAZEL_NSCCACHE_ACTIVE=0" >> "$GITHUB_ENV"
echo "BURROW_BAZEL_NSCCACHE_BAZELRC=$bazelrc_path" >> "$GITHUB_ENV"
echo "NSC_BAZELRC_PATH=$bazelrc_path" >> "$GITHUB_ENV"
fi
fi
}
cache_root="${NSC_CACHE_PATH:-}"
if [[ -z "$cache_root" ]]; then
ensure_dir() {
local dir="$1"
if portable_mkdir -p "$dir" 2>/dev/null; then
return 0
fi
if [[ "$(id -u)" != "0" ]] && command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
sudo mkdir -p "$dir" >/dev/null 2>&1 || return 1
return 0
fi
return 1
}
if [[ "$(uname -s 2>/dev/null || true)" == "Linux" ]]; then
tmp_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-nscloud-cache"
for candidate in "/cache/nscloud" "$tmp_root" "$PWD/.nscloud-cache"; do
if ensure_dir "$candidate"; then
cache_root="$candidate"
break
fi
done
elif ensure_dir "$PWD/.nscloud-cache"; then
cache_root="$PWD/.nscloud-cache"
fi
fi
if [[ -z "$cache_root" ]]; then
echo "NSCloud cache volume not configured (NSC_CACHE_PATH missing); skipping."
configure_bazel_remote_cache
exit 0
fi
export NSC_CACHE_PATH="$cache_root"
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "NSC_CACHE_PATH=$cache_root" >> "$GITHUB_ENV"
fi
spacectl_bin_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/spacectl-bin"
if ! command -v spacectl >/dev/null 2>&1; then
if ! bash Scripts/ci/ensure-spacectl.sh; then
echo "::warning ::Unable to install spacectl; skipping filesystem cache mount."
configure_bazel_remote_cache
exit 0
fi
if [[ -x "${spacectl_bin_dir}/spacectl" ]]; then
export PATH="${spacectl_bin_dir}:$PATH"
fi
fi
mode_args=()
for mode in "$@"; do
[[ -z "$mode" ]] && continue
if [[ "$mode" == "bazel" ]]; then
continue
fi
if [[ "$mode" == "nix" && "${BURROW_NSC_DIRECT_NIX_VOLUME:-0}" == "1" ]]; then
echo "Namespace Nix cache volume is mounted directly at /nix; skipping spacectl nix mount."
continue
fi
if [[ "$mode" == "nix" && "$(uname -s)" == "Darwin" ]]; then
echo "Skipping late Nix cache mount on macOS; using existing Nix profile and Bazel cache."
continue
fi
mode_args+=("--mode=${mode}")
done
if [[ "${#mode_args[@]}" -eq 0 ]]; then
echo "No filesystem cache modes require mounting; skipping spacectl cache mount."
configure_bazel_remote_cache
exit 0
fi
portable_mkdir -p "$cache_root" 2>/dev/null || true
args=(-o json cache mount "--dry_run=false" "--cache_root=${cache_root}" "${mode_args[@]}")
echo "Mounting NSCloud cache: spacectl ${args[*]//${cache_root}/<cache_root>}"
spacectl_bin="$(command -v spacectl)"
run_mount=("$spacectl_bin" "${args[@]}")
if [[ "$(id -u)" != "0" ]]; then
if ! command -v sudo >/dev/null 2>&1 || ! sudo -n true >/dev/null 2>&1; then
echo "::warning ::spacectl cache mount requires passwordless sudo; skipping filesystem cache."
configure_bazel_remote_cache
exit 0
fi
run_mount=(sudo -n env "PATH=$PATH" "$spacectl_bin" "${args[@]}")
fi
tmp_err="$(mktemp "${TMPDIR:-/tmp}/spacectl.XXXXXX")"
cleanup() {
rm -f "$tmp_err"
}
trap cleanup EXIT
json=""
if ! json="$("${run_mount[@]}" 2>"$tmp_err")"; then
echo "::warning ::spacectl cache mount failed; skipping filesystem cache."
if [[ -n "$json" ]]; then
printf '%s\n' "$json" || true
fi
if [[ -s "$tmp_err" ]]; then
sed -n '1,200p' "$tmp_err" || true
fi
configure_bazel_remote_cache
exit 0
fi
if command -v python3 >/dev/null 2>&1; then
NSC_MOUNT_JSON="$json" python3 - <<'PY'
import json
import os
import sys
raw = os.environ.get("NSC_MOUNT_JSON", "").strip()
if not raw:
sys.exit(0)
try:
payload = json.loads(raw)
except Exception as exc:
print(f"::warning ::Unable to parse spacectl JSON output; skipping cache env export: {exc}")
sys.exit(0)
if payload.get("error"):
print(f"::warning ::spacectl cache mount skipped: {payload.get('message') or 'error=true'}")
sys.exit(0)
add_envs = (payload.get("output") or {}).get("add_envs") or {}
gh_env = os.environ.get("GITHUB_ENV")
if gh_env and isinstance(add_envs, dict):
with open(gh_env, "a", encoding="utf-8") as f:
for key, value in add_envs.items():
if key and value is not None:
f.write(f"{key}={value}\n")
PY
fi
configure_bazel_remote_cache

View file

@ -0,0 +1,287 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"
platform="${1:-all}"
build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}"
out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
apple_root="${out_root}/apple"
archives_root="${apple_root}/archives"
derived_data="${BURROW_DERIVED_DATA_PATH:-${repo_root}/.derived-data/apple}"
source_packages="${BURROW_SWIFTPM_CACHE_PATH:-${XDG_CACHE_HOME:-${HOME}/.cache}/burrow/swiftpm}"
project="Apple/Burrow.xcodeproj"
scheme="${BURROW_APPLE_SCHEME:-App}"
bundle_id="${BURROW_APPLE_BUNDLE_ID:-com.hackclub.burrow}"
network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}"
signing_ready="${BURROW_APPLE_SIGNING_READY:-0}"
require_signing="${BURROW_APPLE_REQUIRE_SIGNING:-false}"
sparkle_feed_url="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}"
sparkle_public_ed_key="${BURROW_SPARKLE_PUBLIC_ED_KEY:-Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=}"
sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}"
mkdir -p "$apple_root" "$archives_root" "$derived_data" "$source_packages"
truthy() {
case "${1:-}" in
1|true|TRUE|True|yes|YES|Yes|y|Y|on|ON|On) return 0 ;;
*) return 1 ;;
esac
}
sha256_file() {
local file="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" > "${file}.sha256"
else
sha256sum "$file" > "${file}.sha256"
fi
}
write_version() {
mkdir -p Apple/Configuration
printf 'CURRENT_PROJECT_VERSION = %s\n' "$build_number" > Apple/Configuration/Version.xcconfig
}
xcode_common_args=(
-project "$project"
-scheme "$scheme"
-configuration Release
-derivedDataPath "$derived_data"
-clonedSourcePackagesDirPath "$source_packages"
CURRENT_PROJECT_VERSION="$build_number"
APP_BUNDLE_IDENTIFIER="$bundle_id"
NETWORK_EXTENSION_BUNDLE_IDENTIFIER="$network_extension_bundle_id"
BURROW_SPARKLE_FEED_URL="$sparkle_feed_url"
BURROW_SPARKLE_PUBLIC_ED_KEY="$sparkle_public_ed_key"
)
xcode_auth_args=()
if [[ -n "${ASC_API_KEY_ID:-}" && -n "${ASC_API_ISSUER_ID:-}" && -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then
xcode_auth_args+=(
-authenticationKeyID "$ASC_API_KEY_ID"
-authenticationKeyIssuerID "$ASC_API_ISSUER_ID"
-authenticationKeyPath "$ASC_API_KEY_PATH"
)
fi
if [[ -n "${BURROW_APPLE_KEYCHAIN_PATH:-}" ]]; then
xcode_common_args+=(OTHER_CODE_SIGN_FLAGS="--keychain ${BURROW_APPLE_KEYCHAIN_PATH}")
fi
require_xcodebuild() {
if ! command -v xcodebuild >/dev/null 2>&1; then
echo "xcodebuild is required for Apple release artifacts" >&2
exit 1
fi
xcodebuild -version
xcrun --find swiftc >/dev/null
}
unsigned_note() {
local platform_name="$1"
local artifact_name="$2"
{
echo "Burrow ${platform_name} signing assets were not available."
echo
echo "The lane built an unsigned validation artifact instead of an uploadable release artifact."
echo "Seal the Apple release credentials with agenix and let CI sync provisioning profiles from App Store Connect to produce ${artifact_name}."
} > "${apple_root}/README-${platform_name}-unsigned.txt"
}
ensure_signing_or_note() {
local platform_name="$1"
local artifact_name="$2"
if [[ "$signing_ready" == "1" ]]; then
return 0
fi
if truthy "$require_signing"; then
echo "Apple signing is required for ${platform_name}, but signing assets are not configured." >&2
exit 1
fi
unsigned_note "$platform_name" "$artifact_name"
return 1
}
write_export_options() {
local method="$1"
local path="$2"
cat > "$path" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>${method}</string>
<key>destination</key>
<string>export</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>${DEVELOPMENT_TEAM:-P6PV2R9443}</string>
</dict>
</plist>
EOF
}
build_ios_unsigned() {
local product_dir zip_path
xcodebuild build \
"${xcode_common_args[@]}" \
-destination "generic/platform=iOS Simulator" \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO
product_dir="${derived_data}/Build/Products/Release-iphonesimulator"
zip_path="${apple_root}/Burrow-iOS-simulator-${build_number}.unsigned.zip"
if [[ -d "${product_dir}/Burrow.app" ]]; then
(cd "$product_dir" && zip -qr "$zip_path" Burrow.app)
sha256_file "$zip_path"
fi
}
build_ios_release() {
local archive_path export_dir export_options ipa
if ! ensure_signing_or_note "iOS" "Burrow-iOS-${build_number}.ipa"; then
build_ios_unsigned
return 0
fi
archive_path="${archives_root}/Burrow-iOS-${build_number}.xcarchive"
export_dir="${apple_root}/ios-export"
export_options="${apple_root}/ExportOptions-iOS.plist"
write_export_options "app-store-connect" "$export_options"
xcodebuild archive \
"${xcode_common_args[@]}" \
-destination "generic/platform=iOS" \
-archivePath "$archive_path" \
ARCHS=arm64 \
-allowProvisioningUpdates \
"${xcode_auth_args[@]}"
xcodebuild -exportArchive \
-archivePath "$archive_path" \
-exportPath "$export_dir" \
-exportOptionsPlist "$export_options" \
-allowProvisioningUpdates \
"${xcode_auth_args[@]}"
ipa="$(find "$export_dir" -maxdepth 1 -type f -name '*.ipa' | sort | head -n 1 || true)"
if [[ -z "$ipa" ]]; then
echo "iOS export completed but did not produce an IPA" >&2
exit 1
fi
cp "$ipa" "${apple_root}/Burrow-iOS-${build_number}.ipa"
sha256_file "${apple_root}/Burrow-iOS-${build_number}.ipa"
}
build_macos_unsigned() {
local product_dir zip_path
xcodebuild build \
"${xcode_common_args[@]}" \
-destination "platform=macOS" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO
product_dir="${derived_data}/Build/Products/Release"
zip_path="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip"
if [[ -d "${product_dir}/Burrow.app" ]]; then
ditto -c -k --keepParent "${product_dir}/Burrow.app" "$zip_path"
sha256_file "$zip_path"
fi
}
build_macos_release() {
local archive_path export_dir export_options app dmg channel sparkle_dir length pubdate url
if ! ensure_signing_or_note "macOS" "Burrow-macOS-${build_number}.dmg"; then
build_macos_unsigned
return 0
fi
archive_path="${archives_root}/Burrow-macOS-${build_number}.xcarchive"
export_dir="${apple_root}/macos-export"
export_options="${apple_root}/ExportOptions-macOS.plist"
write_export_options "${BURROW_MACOS_EXPORT_METHOD:-developer-id}" "$export_options"
xcodebuild archive \
"${xcode_common_args[@]}" \
-destination "generic/platform=macOS" \
-archivePath "$archive_path" \
-allowProvisioningUpdates \
"${xcode_auth_args[@]}"
xcodebuild -exportArchive \
-archivePath "$archive_path" \
-exportPath "$export_dir" \
-exportOptionsPlist "$export_options" \
-allowProvisioningUpdates \
"${xcode_auth_args[@]}"
app="$(find "$export_dir" -maxdepth 2 -type d -name 'Burrow.app' | sort | head -n 1 || true)"
if [[ -z "$app" ]]; then
echo "macOS export completed but did not produce Burrow.app" >&2
exit 1
fi
dmg="${apple_root}/Burrow-macOS-${build_number}.dmg"
rm -f "$dmg"
hdiutil create -volname "Burrow ${build_number}" -srcfolder "$app" -ov -format UDZO "$dmg"
sha256_file "$dmg"
channel="${SPARKLE_CHANNEL:-build-${build_number}}"
sparkle_dir="${out_root}/sparkle/${channel}"
mkdir -p "$sparkle_dir"
cp "$dmg" "$sparkle_dir/"
length="$(wc -c < "$dmg" | tr -d ' ')"
pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/$(basename "$dmg")"
cat > "${sparkle_dir}/appcast.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Burrow ${channel}</title>
<item>
<title>Burrow ${build_number}</title>
<pubDate>${pubdate}</pubDate>
<sparkle:version>${build_number}</sparkle:version>
<sparkle:shortVersionString>0.1</sparkle:shortVersionString>
<enclosure url="${url}" sparkle:version="${build_number}" length="${length}" type="application/octet-stream" />
</item>
</channel>
</rss>
EOF
if truthy "$sparkle_sign_with_kms"; then
Scripts/sparkle/sign-appcast-kms.py \
--appcast "${sparkle_dir}/appcast.xml" \
--artifact-dir "$sparkle_dir"
fi
}
require_xcodebuild
write_version
case "$platform" in
all)
build_ios_release
build_macos_release
;;
ios)
build_ios_release
;;
macos)
build_macos_release
;;
*)
echo "unknown Apple release platform: $platform" >&2
exit 64
;;
esac
find "$out_root" -type f -print | sort

View file

@ -0,0 +1,332 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
platform="${1:-all}"
build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}"
out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
mkdir -p "${out_root}"
sha256_file() {
local file="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" > "${file}.sha256"
else
sha256sum "$file" > "${file}.sha256"
fi
}
burrow_crate_version() {
awk -F '"' '/^version = / { print $2; exit }' burrow/Cargo.toml
}
package_release_suffix() {
printf '%s' "$build_number" | tr -c 'A-Za-z0-9.+~' '.'
}
arch_package_suffix() {
printf '%s' "$build_number" | tr -c 'A-Za-z0-9.+_' '.'
}
deb_arch_for_target() {
case "$1" in
x86_64-unknown-linux-gnu) printf 'amd64' ;;
aarch64-unknown-linux-gnu) printf 'arm64' ;;
riscv64gc-unknown-linux-gnu) printf 'riscv64' ;;
*) return 1 ;;
esac
}
rpm_arch_for_target() {
case "$1" in
x86_64-unknown-linux-gnu) printf 'x86_64' ;;
aarch64-unknown-linux-gnu) printf 'aarch64' ;;
riscv64gc-unknown-linux-gnu) printf 'riscv64' ;;
*) return 1 ;;
esac
}
pacman_arch_for_target() {
case "$1" in
x86_64-unknown-linux-gnu) printf 'x86_64' ;;
aarch64-unknown-linux-gnu) printf 'aarch64' ;;
riscv64gc-unknown-linux-gnu) printf 'riscv64' ;;
*) return 1 ;;
esac
}
copy_license_readme() {
local dst="$1"
cp README.md "$dst/README.md"
if [[ -f LICENSE ]]; then
cp LICENSE "$dst/LICENSE"
elif [[ -f LICENSE.md ]]; then
cp LICENSE.md "$dst/LICENSE.md"
fi
}
package_deb() {
local target binary deb_arch pkg_version suffix staging deb_file installed_size
target="$1"
binary="$2"
if ! deb_arch="$(deb_arch_for_target "$target")"; then
echo "warning: no Debian architecture mapping for ${target}; skipping .deb package" >&2
return 0
fi
if ! command -v dpkg-deb >/dev/null 2>&1; then
echo "warning: dpkg-deb is not available; skipping .deb package" >&2
return 0
fi
suffix="$(package_release_suffix)"
pkg_version="${BURROW_PACKAGE_VERSION:-$(burrow_crate_version)+${suffix}}"
staging="${out_root}/linux/pkg-deb/burrow_${pkg_version}_${deb_arch}"
rm -rf "$staging"
mkdir -p \
"$staging/DEBIAN" \
"$staging/usr/bin" \
"$staging/usr/lib/systemd/system" \
"$staging/usr/share/doc/burrow" \
"$staging/usr/share/licenses/burrow"
install -m 0755 "$binary" "$staging/usr/bin/burrow"
install -m 0644 systemd/burrow.service "$staging/usr/lib/systemd/system/burrow.service"
install -m 0644 systemd/burrow.socket "$staging/usr/lib/systemd/system/burrow.socket"
install -m 0644 README.md "$staging/usr/share/doc/burrow/README.md"
install -m 0644 LICENSE.md "$staging/usr/share/licenses/burrow/LICENSE.md"
installed_size="$(du -sk "$staging/usr" | awk '{ print $1 }')"
cat > "$staging/DEBIAN/control" <<EOF
Package: burrow
Version: ${pkg_version}
Section: net
Priority: optional
Architecture: ${deb_arch}
Maintainer: Burrow Release Automation <packages@burrow.net>
Installed-Size: ${installed_size}
Depends: systemd
Description: Burrow VPN daemon and CLI
Burrow installs the daemon entrypoint and systemd socket used by the desktop UI.
EOF
deb_file="${out_root}/linux/burrow_${pkg_version}_${deb_arch}.deb"
dpkg-deb --build --root-owner-group "$staging" "$deb_file"
sha256_file "$deb_file"
}
package_rpm() {
local target binary rpm_arch
target="$1"
binary="$2"
if ! rpm_arch="$(rpm_arch_for_target "$target")"; then
echo "warning: no RPM architecture mapping for ${target}; skipping .rpm package" >&2
return 0
fi
if ! command -v cargo-generate-rpm >/dev/null 2>&1; then
echo "warning: cargo-generate-rpm is not available; skipping .rpm package" >&2
return 0
fi
mkdir -p target/release
cp "$binary" target/release/burrow
if cargo generate-rpm -p burrow; then
find target/generate-rpm -type f -name '*.rpm' -exec cp {} "${out_root}/linux/" \; 2>/dev/null || true
find "${out_root}/linux" -maxdepth 1 -type f -name '*.rpm' -print | while read -r rpm; do
sha256_file "$rpm"
done
else
echo "warning: cargo-generate-rpm failed for ${rpm_arch}; skipping .rpm package" >&2
fi
}
package_pacman() {
local target binary pacman_arch pkg_version pkgrel work source_sum service_sum socket_sum readme_sum license_sum
target="$1"
binary="$2"
if ! pacman_arch="$(pacman_arch_for_target "$target")"; then
echo "warning: no pacman architecture mapping for ${target}; skipping Arch package" >&2
return 0
fi
if ! command -v makepkg >/dev/null 2>&1; then
echo "warning: makepkg is not available; skipping Arch package" >&2
return 0
fi
pkg_version="${BURROW_ARCH_PKGVER:-$(burrow_crate_version)_$(arch_package_suffix)}"
pkgrel="${BURROW_ARCH_PKGREL:-1}"
work="${out_root}/linux/pkg-arch/${pacman_arch}"
rm -rf "$work"
mkdir -p "$work"
install -m 0755 "$binary" "$work/burrow"
install -m 0644 systemd/burrow.service "$work/burrow.service"
install -m 0644 systemd/burrow.socket "$work/burrow.socket"
install -m 0644 README.md "$work/README.md"
install -m 0644 LICENSE.md "$work/LICENSE.md"
source_sum="$(sha256sum "$work/burrow" | awk '{ print $1 }')"
service_sum="$(sha256sum "$work/burrow.service" | awk '{ print $1 }')"
socket_sum="$(sha256sum "$work/burrow.socket" | awk '{ print $1 }')"
readme_sum="$(sha256sum "$work/README.md" | awk '{ print $1 }')"
license_sum="$(sha256sum "$work/LICENSE.md" | awk '{ print $1 }')"
cat > "$work/PKGBUILD" <<EOF
# Maintainer: Burrow Release Automation <packages@burrow.net>
pkgname=burrow
pkgver=${pkg_version}
pkgrel=${pkgrel}
pkgdesc="Burrow VPN daemon and CLI"
arch=("${pacman_arch}")
url="https://git.burrow.net/hackclub/burrow"
license=("GPL-3.0-or-later")
depends=("systemd")
source=("burrow" "burrow.service" "burrow.socket" "README.md" "LICENSE.md")
sha256sums=("${source_sum}" "${service_sum}" "${socket_sum}" "${readme_sum}" "${license_sum}")
package() {
install -Dm755 "\${srcdir}/burrow" "\${pkgdir}/usr/bin/burrow"
install -Dm644 "\${srcdir}/burrow.service" "\${pkgdir}/usr/lib/systemd/system/burrow.service"
install -Dm644 "\${srcdir}/burrow.socket" "\${pkgdir}/usr/lib/systemd/system/burrow.socket"
install -Dm644 "\${srcdir}/README.md" "\${pkgdir}/usr/share/doc/burrow/README.md"
install -Dm644 "\${srcdir}/LICENSE.md" "\${pkgdir}/usr/share/licenses/burrow/LICENSE.md"
}
EOF
(
cd "$work"
makepkg --force --nodeps --noconfirm
)
find "$work" -maxdepth 1 -type f -name '*.pkg.tar.*' -exec cp {} "${out_root}/linux/" \;
find "${out_root}/linux" -maxdepth 1 -type f -name '*.pkg.tar.*' -print | while read -r package; do
sha256_file "$package"
done
}
package_linux() {
local target host archive staging
host="$(rustc -vV | awk '/^host:/ { print $2 }')"
target="${BURROW_LINUX_TARGET:-$host}"
if [[ "$target" != *-linux-* && -z "${BURROW_LINUX_TARGET:-}" ]]; then
mkdir -p "${out_root}/linux"
echo "warning: host target ${target} is not Linux; skipping Linux artifact on this host" >&2
printf 'Linux build was skipped on host target %s. Run this lane on a Linux Namespace runner or set BURROW_LINUX_TARGET explicitly.\n' "$target" > "${out_root}/linux/README.txt"
return 0
fi
staging="${out_root}/linux/burrow-${build_number}-${target}"
mkdir -p "$staging"
cargo build --locked --release -p burrow --bin burrow --target "$target"
install -m 0755 "target/${target}/release/burrow" "$staging/burrow"
copy_license_readme "$staging"
archive="${out_root}/linux/burrow-${build_number}-${target}.tar.gz"
tar -C "${out_root}/linux" -czf "$archive" "$(basename "$staging")"
sha256_file "$archive"
package_deb "$target" "target/${target}/release/burrow"
package_rpm "$target" "target/${target}/release/burrow"
package_pacman "$target" "target/${target}/release/burrow"
}
package_windows_stub_unix() {
local target exe archive staging
target="${BURROW_WINDOWS_TARGET:-x86_64-pc-windows-gnu}"
staging="${out_root}/windows/burrow-${build_number}-${target}"
mkdir -p "$staging"
target_libdir="$(rustc --print target-libdir --target "$target" 2>/dev/null || true)"
if [[ -z "$target_libdir" || ! -d "$target_libdir" ]]; then
echo "warning: Rust target ${target} is not installed on this host" >&2
printf 'Windows stub build for %s was skipped on this host. Use the Namespace Windows lane for the native artifact.\n' "$target" > "${out_root}/windows/README.txt"
return 0
fi
if cargo build --locked --release -p burrow-windows-stub --target "$target"; then
exe="target/${target}/release/burrow.exe"
else
echo "warning: unable to build the Windows release stub on this host" >&2
printf 'Windows stub build for %s was skipped on this host. Use the Namespace Windows lane for the native artifact.\n' "$target" > "${out_root}/windows/README.txt"
return 0
fi
install -m 0755 "$exe" "$staging/burrow.exe"
copy_license_readme "$staging"
printf 'Burrow Windows stub build %s\n' "$build_number" > "$staging/README-Windows.txt"
archive="${out_root}/windows/burrow-${build_number}-${target}.zip"
(cd "${out_root}/windows" && zip -qr "$archive" "$(basename "$staging")")
sha256_file "$archive"
}
package_sparkle_unsigned() {
local macos_dir appcast channel dmg
channel="${SPARKLE_CHANNEL:-build-${build_number}}"
macos_dir="${out_root}/macos"
appcast="${out_root}/sparkle/${channel}/appcast.xml"
mkdir -p "$(dirname "$appcast")"
dmg="$(find "$macos_dir" -maxdepth 1 -type f -name '*.dmg' -print 2>/dev/null | sort | tail -n 1 || true)"
if [[ -z "$dmg" ]]; then
echo "No macOS DMG exists yet; Sparkle appcast generation is waiting on the macOS signed artifact." > "${out_root}/sparkle/${channel}/README.txt"
return 0
fi
local name length pubdate url
name="$(basename "$dmg")"
length="$(wc -c < "$dmg" | tr -d ' ')"
pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/${name}"
cat > "$appcast" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Burrow ${channel}</title>
<item>
<title>Burrow ${build_number}</title>
<pubDate>${pubdate}</pubDate>
<sparkle:version>${build_number}</sparkle:version>
<sparkle:shortVersionString>0.1</sparkle:shortVersionString>
<enclosure url="${url}" sparkle:version="${build_number}" length="${length}" type="application/octet-stream" />
</item>
</channel>
</rss>
EOF
cp "$dmg" "${out_root}/sparkle/${channel}/"
}
case "$platform" in
all)
package_linux
package_windows_stub_unix
if [[ "$(uname -s)" == "Darwin" ]] && command -v xcodebuild >/dev/null 2>&1; then
Scripts/ci/package-apple-artifacts.sh all
else
mkdir -p "${out_root}/apple"
printf 'Apple artifacts require a macOS runner with Xcode. The release workflow builds them in the dedicated Apple lane.\n' > "${out_root}/apple/README.txt"
fi
package_sparkle_unsigned
;;
linux)
package_linux
;;
windows-stub|windows)
package_windows_stub_unix
;;
sparkle)
package_sparkle_unsigned
;;
apple)
Scripts/ci/package-apple-artifacts.sh all
;;
ios|macos)
Scripts/ci/package-apple-artifacts.sh "$platform"
;;
*)
echo "unknown release platform: $platform" >&2
exit 64
;;
esac
find "$out_root" -type f -print | sort

View file

@ -7,7 +7,8 @@ set -euo pipefail
: "${TOKEN:?TOKEN is required}"
release_api="${API_URL}/repos/${REPOSITORY}/releases"
tag_api="${release_api}/tags/${RELEASE_TAG}"
encoded_release_tag="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "${RELEASE_TAG}")"
tag_api="${release_api}/tags/${encoded_release_tag}"
release_json="$(mktemp)"
create_json="$(mktemp)"
trap 'rm -f "${release_json}" "${create_json}"' EXIT
@ -50,6 +51,9 @@ if [[ -z "${release_id}" || "${release_id}" == "null" ]]; then
fi
for file in dist/*; do
if [[ ! -f "${file}" ]]; then
continue
fi
name="$(basename "${file}")"
asset_id="$(jq -r --arg name "${name}" '.assets[]? | select(.name == $name) | .id' "${release_json}" | head -n1)"
if [[ -n "${asset_id}" ]]; then

49
Scripts/ci/publish-nix-cache.sh Executable file
View file

@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
cache_server_name="${BURROW_NIX_CACHE_SERVER_NAME:-burrow}"
cache_server_url="${BURROW_NIX_CACHE_SERVER_URL:-https://nix.burrow.net}"
cache_name="${BURROW_NIX_CACHE_NAME:-burrow}"
cache_ref="${cache_server_name}:${cache_name}"
token="${BURROW_NIX_CACHE_PUSH_TOKEN:-}"
if [[ -z "$token" ]]; then
if [[ "${BURROW_NIX_CACHE_REQUIRED:-false}" == "true" ]]; then
echo "BURROW_NIX_CACHE_PUSH_TOKEN is required to publish Nix cache paths" >&2
exit 1
fi
echo "::notice ::BURROW_NIX_CACHE_PUSH_TOKEN is not set; skipping Nix cache publish."
exit 0
fi
if ! command -v attic >/dev/null 2>&1; then
echo "attic is required to publish Nix cache paths" >&2
exit 127
fi
if ! command -v nix >/dev/null 2>&1; then
echo "nix is required to build paths for cache publishing" >&2
exit 127
fi
attic login "$cache_server_name" "$cache_server_url" "$token" --set-default
push_inputs=("$@")
if [[ "${#push_inputs[@]}" -eq 0 ]]; then
read -r -a attrs <<< "${BURROW_NIX_CACHE_ATTRS:-.#burrow .#forgejo-nsc-dispatcher .#forgejo-nsc-autoscaler}"
if [[ "${#attrs[@]}" -eq 0 ]]; then
echo "no Nix attrs were supplied for cache publishing" >&2
exit 1
fi
mapfile -t push_inputs < <(nix build --no-link --print-out-paths "${attrs[@]}")
fi
if [[ "${#push_inputs[@]}" -eq 0 ]]; then
echo "no Nix output paths were produced for cache publishing" >&2
exit 1
fi
attic push "$cache_ref" "${push_inputs[@]}"

56
Scripts/ci/resolve-nix-bin.sh Executable file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
find_nix_bin() {
local candidate
for candidate in \
"${NIX_BIN:-}" \
"$(command -v nix 2>/dev/null || true)" \
"${HOME:-}/.nix-profile/bin/nix" \
"${HOME:-}/.local/state/nix/profile/bin/nix" \
"/nix/var/nix/profiles/per-user/${USER:-runner}/profile/bin/nix" \
/nix/var/nix/profiles/default/bin/nix \
/run/current-system/sw/bin/nix \
/etc/profiles/per-user/runner/bin/nix \
/Users/runner/.nix-profile/bin/nix \
/Users/runner/.local/state/nix/profile/bin/nix \
/home/runner/.nix-profile/bin/nix \
/home/runner/.local/state/nix/profile/bin/nix \
/nix/profile/bin/nix; do
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
candidate="$(
find \
"${HOME:-/nonexistent}" \
/Users/runner \
/home/runner \
/nix/var/nix/profiles \
/etc/profiles/per-user \
-path '*/bin/nix' -type f 2>/dev/null | head -n 1 || true
)"
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
return 1
}
if nix_bin="$(find_nix_bin)"; then
printf '%s\n' "$nix_bin"
exit 0
fi
if bash Scripts/ci/ensure-nix.sh >/dev/null 2>&1; then
if nix_bin="$(find_nix_bin)"; then
printf '%s\n' "$nix_bin"
exit 0
fi
fi
echo "::error ::nix is required but is not on PATH and no standard install path exists" >&2
exit 1

View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"
platform="${1:-all}"
out_dir="${BURROW_APPLE_PROFILES_OUT_DIR:-.forgejo/actions/export}"
app_id="${BURROW_APPLE_BUNDLE_ID:-com.hackclub.burrow}"
network_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${app_id}.network}"
replace_certificate_mismatch="${BURROW_APPLE_REPLACE_CERTIFICATE_MISMATCH:-false}"
if [[ -n "${BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID:-}" || -n "${BURROW_DEVELOPER_ID_CERTIFICATE_ID:-}" ]]; then
replace_certificate_mismatch="${BURROW_APPLE_REPLACE_CERTIFICATE_MISMATCH:-true}"
fi
key_id="${ASC_API_KEY_ID:-${APPSTORE_KEY_ID:-}}"
issuer_id="${ASC_API_ISSUER_ID:-${APPSTORE_KEY_ISSUER_ID:-}}"
key_path="${ASC_API_KEY_PATH:-${APPSTORE_KEY_PATH:-}}"
key_base64="${ASC_API_KEY_BASE64:-}"
if [[ -z "$key_id" || -z "$issuer_id" ]]; then
echo "::notice ::App Store Connect credentials are not available; provisioning profile sync skipped."
exit 0
fi
temp_key=""
cleanup() {
[[ -z "$temp_key" ]] || rm -f "$temp_key"
}
trap cleanup EXIT
if [[ -z "$key_path" ]]; then
if [[ -z "$key_base64" ]]; then
echo "::notice ::ASC_API_KEY_PATH/ASC_API_KEY_BASE64 is not available; provisioning profile sync skipped."
exit 0
fi
temp_key="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-asc-key.XXXXXX.p8")"
printf '%s' "$key_base64" | base64 --decode > "$temp_key"
chmod 600 "$temp_key"
key_path="$temp_key"
fi
if [[ ! -f "$key_path" ]]; then
echo "::error ::App Store Connect key path does not exist: ${key_path}" >&2
exit 1
fi
mkdir -p "$out_dir"
profile_args=(
--platform "$platform"
--key-id "$key_id"
--issuer-id "$issuer_id"
--key-file "$key_path"
--out-dir "$out_dir"
--app-id "$app_id"
--network-id "$network_id"
--create-missing-bundle-ids "${BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS:-false}"
--enable-capabilities "${BURROW_APPLE_ENABLE_CAPABILITIES:-false}"
--replace-certificate-mismatch "$replace_certificate_mismatch"
--bundle-platform "${BURROW_APPLE_BUNDLE_PLATFORM:-UNIVERSAL}"
--associated-domains "${BURROW_APPLE_ASSOCIATED_DOMAINS:-applinks:burrow.rs?mode=developer,webcredentials:burrow.rs?mode=developer}"
)
if [[ -n "${BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID:-}" ]]; then
profile_args+=(--ios-certificate-id "$BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID")
fi
if [[ -n "${BURROW_DEVELOPER_ID_CERTIFICATE_ID:-}" ]]; then
profile_args+=(--macos-certificate-id "$BURROW_DEVELOPER_ID_CERTIFICATE_ID")
fi
node Scripts/apple/sync-provisioning-profiles.mjs "${profile_args[@]}"

View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
: "${BURROW_GARAGE_ENDPOINT:?BURROW_GARAGE_ENDPOINT is required}"
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required for Garage upload}"
: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required for Garage upload}"
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
source_dir="${BURROW_PACKAGE_REPOSITORY_DIR:-${repo_root}/publish/repositories}"
if [[ ! -d "$source_dir" ]]; then
echo "package repository directory not found: $source_dir" >&2
exit 1
fi
if ! command -v aws >/dev/null 2>&1; then
echo "aws CLI is required for Garage package repository upload" >&2
exit 127
fi
bucket="${BURROW_PACKAGE_GARAGE_BUCKET:-burrow-packages}"
prefix="${BURROW_PACKAGE_GARAGE_PREFIX:-${BURROW_PACKAGE_GCS_PREFIX:-}}"
region="${BURROW_GARAGE_REGION:-garage}"
destination="s3://${bucket}"
if [[ -n "$prefix" ]]; then
destination="${destination}/${prefix}"
fi
export AWS_DEFAULT_REGION="$region"
export AWS_EC2_METADATA_DISABLED=true
aws --endpoint-url "$BURROW_GARAGE_ENDPOINT" s3 sync "$source_dir" "$destination" --no-progress

View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
prefix="${BURROW_PACKAGE_GCS_PREFIX:-}"
source_dir="${BURROW_PACKAGE_REPOSITORY_DIR:-${repo_root}/publish/repositories}"
if [[ ! -d "$source_dir" ]]; then
echo "package repository directory not found: $source_dir" >&2
exit 1
fi
bucket="${BURROW_PACKAGE_GCS_BUCKET:-burrow-net-packages}"
if ! command -v gcloud >/dev/null 2>&1; then
echo "gcloud is required for GCS package repository upload" >&2
exit 127
fi
gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q . || {
echo "gcloud has no active account; run Scripts/ci/google-wif-auth.sh first" >&2
exit 1
}
destination="gs://${bucket}"
if [[ -n "$prefix" ]]; then
destination="${destination}/${prefix}"
fi
gcloud storage rsync --recursive "$source_dir" "$destination"

View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
uploaded=0
garage_configured=0
if [[ -n "${BURROW_GARAGE_ENDPOINT:-}" && -n "${AWS_ACCESS_KEY_ID:-}" && -n "${AWS_SECRET_ACCESS_KEY:-}" ]]; then
garage_configured=1
fi
if [[ "$garage_configured" == "1" ]]; then
Scripts/ci/upload-package-garage.sh
uploaded=1
elif [[ "${BURROW_PACKAGE_REQUIRE_GARAGE:-false}" == "true" ]]; then
echo "Garage package repository upload is required, but BURROW_GARAGE_ENDPOINT or AWS credentials are missing" >&2
exit 1
else
echo "::notice ::Garage package repository upload not configured; skipping primary S3-compatible target."
fi
if [[ "${BURROW_PACKAGE_GCS_BACKUP:-true}" != "false" ]]; then
if ! command -v gcloud >/dev/null 2>&1; then
echo "gcloud is required for GCS package repository backup" >&2
exit 127
fi
if ! gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q .; then
Scripts/ci/google-wif-auth.sh
fi
Scripts/ci/upload-package-repositories.sh
uploaded=1
fi
if [[ "$uploaded" != "1" ]]; then
echo "no package repository storage targets were enabled" >&2
exit 1
fi

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
: "${BURROW_GARAGE_ENDPOINT:?BURROW_GARAGE_ENDPOINT is required}"
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required for Garage upload}"
: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required for Garage upload}"
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
source_dir="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${BUILD_NUMBER}}"
if [[ ! -d "$source_dir" ]]; then
echo "release artifact directory not found: $source_dir" >&2
exit 1
fi
if ! command -v aws >/dev/null 2>&1; then
echo "aws CLI is required for Garage release upload" >&2
exit 127
fi
bucket="${BURROW_RELEASE_GARAGE_BUCKET:-burrow-releases}"
region="${BURROW_GARAGE_REGION:-garage}"
destination="s3://${bucket}/builds/${BUILD_NUMBER}"
export AWS_DEFAULT_REGION="$region"
export AWS_EC2_METADATA_DISABLED=true
aws --endpoint-url "$BURROW_GARAGE_ENDPOINT" s3 sync "$source_dir" "$destination" --no-progress

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
source_dir="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${BUILD_NUMBER}}"
if [[ ! -d "$source_dir" ]]; then
echo "release artifact directory not found: $source_dir" >&2
exit 1
fi
bucket="${BURROW_RELEASE_GCS_BUCKET:-burrow-net-releases}"
if ! command -v gcloud >/dev/null 2>&1; then
echo "gcloud is required for GCS release upload" >&2
exit 127
fi
gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q . || {
echo "gcloud has no active account; run Scripts/ci/google-wif-auth.sh first" >&2
exit 1
}
gcloud storage rsync --recursive "$source_dir" "gs://${bucket}/builds/${BUILD_NUMBER}"

View file

@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${repo_root}"
uploaded=0
garage_configured=0
if [[ -n "${BURROW_GARAGE_ENDPOINT:-}" && -n "${AWS_ACCESS_KEY_ID:-}" && -n "${AWS_SECRET_ACCESS_KEY:-}" ]]; then
garage_configured=1
fi
if [[ "$garage_configured" == "1" ]]; then
Scripts/ci/upload-release-garage.sh
uploaded=1
elif [[ "${BURROW_RELEASE_REQUIRE_GARAGE:-false}" == "true" ]]; then
echo "Garage release upload is required, but BURROW_GARAGE_ENDPOINT or AWS credentials are missing" >&2
exit 1
else
echo "::notice ::Garage release upload not configured; skipping primary S3-compatible target."
fi
if [[ "${BURROW_RELEASE_GCS_BACKUP:-true}" != "false" ]]; then
if ! command -v gcloud >/dev/null 2>&1; then
echo "gcloud is required for GCS release backup" >&2
exit 127
fi
if ! gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q .; then
Scripts/ci/google-wif-auth.sh
fi
Scripts/ci/upload-release-gcs.sh
uploaded=1
fi
if [[ "$uploaded" != "1" ]]; then
echo "no release storage targets were enabled" >&2
exit 1
fi