Compare commits

..

1 commit

Author SHA1 Message Date
Conrad Kramer
3cc3358a4f Update release pipelines to upload release artifacts 2024-03-16 11:34:46 -07:00
729 changed files with 2619 additions and 64615 deletions

View file

@ -1,10 +0,0 @@
.build
.derived-data
.git
.nscloud-cache
Apple/build
ci-artifacts
dist
publish
release
target

View file

@ -1 +0,0 @@
9.1.0

View file

@ -1,3 +1,6 @@
[target.'cfg(unix)']
runner = "sudo -E"
[alias] # command aliases [alias] # command aliases
rr = "run --release" rr = "run --release"
bb = "build --release" bb = "build --release"

View file

@ -1,327 +0,0 @@
name: Decrypt Release Secrets
description: Decrypts age-sealed Burrow release secrets and exports CI paths/env.
inputs:
root:
description: Repository root
required: false
default: .
runs:
using: composite
steps:
- shell: bash
run: |
set -euo pipefail
ROOT_INPUT='${{ inputs.root }}'
ROOT="$(cd "$ROOT_INPUT" && pwd)"
resolve_nix_bin() {
local candidate
for candidate in \
"${NIX_BIN:-}" \
"$(command -v nix 2>/dev/null || true)" \
/nix/var/nix/profiles/default/bin/nix \
"${HOME:-}/.nix-profile/bin/nix" \
"${HOME:-}/.local/state/nix/profile/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 \
; do
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
resolve_decrypt_tool() {
local candidate
for candidate in \
"${AGE_BIN:-}" \
"$(command -v age 2>/dev/null || true)" \
/opt/homebrew/bin/age \
/usr/local/bin/age \
/usr/bin/age \
/run/current-system/sw/bin/age \
/etc/profiles/per-user/runner/bin/age \
"${HOME:-}/.nix-profile/bin/age" \
"${HOME:-}/.local/state/nix/profile/bin/age" \
/Users/runner/.nix-profile/bin/age \
/Users/runner/.local/state/nix/profile/bin/age \
/home/runner/.nix-profile/bin/age \
/home/runner/.local/state/nix/profile/bin/age \
; do
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf 'age:%s\n' "$candidate"
return 0
fi
done
for candidate in \
"${AGENIX_BIN:-}" \
"$(command -v agenix 2>/dev/null || true)" \
; do
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf 'agenix:%s\n' "$candidate"
return 0
fi
done
local nix_bin
nix_bin="$(resolve_nix_bin || true)"
if [[ -n "$nix_bin" ]]; then
local built
built="$("$nix_bin" --extra-experimental-features 'nix-command flakes' build --no-link "$ROOT#age" --print-out-paths 2>/dev/null | tail -n 1 || true)"
if [[ -n "$built" && -x "$built/bin/age" ]]; then
printf 'age:%s\n' "$built/bin/age"
return 0
fi
built="$("$nix_bin" --extra-experimental-features 'nix-command flakes' build --no-link "$ROOT#agenix" --print-out-paths 2>/dev/null | tail -n 1 || true)"
if [[ -n "$built" && -x "$built/bin/agenix" ]]; then
printf 'agenix:%s\n' "$built/bin/agenix"
return 0
fi
fi
echo "::error ::age or agenix is required to decrypt release secrets but neither is available." >&2
exit 1
}
decrypt_tool="$(resolve_decrypt_tool)"
decrypt_kind="${decrypt_tool%%:*}"
decrypt_bin="${decrypt_tool#*:}"
KEY="${AGE_IDENTITY_PATH:-}"
if [[ -z "$KEY" || ! -s "$KEY" ]]; then
echo "::error ::Missing age identity. Run Scripts/resolve-age-identity.sh before this action." >&2
exit 1
fi
decrypt_age_file() {
local file_path="$1"
if [[ "$decrypt_kind" == "agenix" ]]; then
local agenix_path="$file_path"
if [[ "$file_path" == "$ROOT/"* ]]; then
agenix_path="${file_path#"$ROOT"/}"
fi
(cd "$ROOT" && "$decrypt_bin" --identity "$KEY" --decrypt "$agenix_path")
else
"$decrypt_bin" --decrypt -i "$KEY" "$file_path"
fi
}
write_env() {
local name="$1"
local value="$2"
echo "${name}=${value}" >> "$GITHUB_ENV"
}
mask_multiline() {
local value="$1"
echo "::add-mask::${value}"
while IFS= read -r line; do
[[ -n "$line" ]] || continue
echo "::add-mask::${line}"
done <<<"$value"
}
decrypt_env_optional() {
local name="$1"
local file="$2"
if [[ ! -f "$file" ]]; then
echo "::notice ::Skipping optional release secret ${file}"
return 0
fi
local value
value="$(decrypt_age_file "$file")"
mask_multiline "$value"
{
echo "${name}<<EOF"
printf '%s\n' "$value"
echo "EOF"
} >> "$GITHUB_ENV"
}
decrypt_file_optional() {
local name="$1"
local file="$2"
local suffix="$3"
if [[ ! -f "$file" ]]; then
echo "::notice ::Skipping optional release secret ${file}"
return 0
fi
local temp_dir="${RUNNER_TEMP:-/tmp}"
local temp_file
temp_file="$(mktemp "${temp_dir}/${name}.XXXXXX")"
if [[ -n "$suffix" ]]; then
local with_suffix="${temp_file}.${suffix}"
mv "$temp_file" "$with_suffix"
temp_file="$with_suffix"
fi
decrypt_age_file "$file" > "$temp_file"
chmod 600 "$temp_file"
write_env "$name" "$temp_file"
}
decrypt_dotenv_optional() {
local file="$1"
if [[ ! -f "$file" ]]; then
echo "::notice ::Skipping optional release secret ${file}"
return 0
fi
local temp_file
temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-release-dotenv.XXXXXX")"
decrypt_age_file "$file" > "$temp_file"
# shellcheck disable=SC1090
set -a
source "$temp_file"
set +a
rm -f "$temp_file"
if [[ -z "${MINIO_ROOT_USER:-}" || -z "${MINIO_ROOT_PASSWORD:-}" ]]; then
echo "::error ::${file} must define MINIO_ROOT_USER and MINIO_ROOT_PASSWORD." >&2
exit 1
fi
echo "::add-mask::${MINIO_ROOT_USER}"
echo "::add-mask::${MINIO_ROOT_PASSWORD}"
{
echo "AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER}"
echo "AWS_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}"
echo "AWS_DEFAULT_REGION=${BLOB_REGION:-hel1}"
echo "AWS_REGION=${BLOB_REGION:-hel1}"
echo "AWS_S3_ENDPOINT_URL=https://${BLOB_ENDPOINT:-hel1.your-objectstorage.com}"
} >> "$GITHUB_ENV"
}
decode_base64_stream() {
if command -v base64 >/dev/null 2>&1; then
base64 -d 2>/dev/null || base64 -D
elif command -v openssl >/dev/null 2>&1; then
openssl base64 -d -A
else
echo "::error ::base64 or openssl is required to decode release secrets." >&2
return 127
fi
}
decode_base64_to_file() {
local output="$1"
decode_base64_stream > "$output"
}
decrypt_appstore_connect() {
local file="$ROOT/secrets/apple/appstore-connect.env.age"
if [[ ! -f "$file" ]]; then
echo "::notice ::Skipping optional release secret ${file}"
return 0
fi
local temp_file key_path key_value
temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-asc-env.XXXXXX")"
decrypt_age_file "$file" > "$temp_file"
# shellcheck disable=SC1090
set -a
source "$temp_file"
set +a
rm -f "$temp_file"
if [[ -z "${ASC_API_KEY_ID:-}" || -z "${ASC_API_ISSUER_ID:-}" ]]; then
echo "::error ::${file} must define ASC_API_KEY_ID and ASC_API_ISSUER_ID." >&2
exit 1
fi
key_path="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-asc-key.XXXXXX.p8")"
if [[ -n "${ASC_API_KEY_BASE64:-}" ]]; then
printf '%s' "$ASC_API_KEY_BASE64" | decode_base64_to_file "$key_path"
elif [[ -n "${ASC_API_KEY_P8_BASE64:-}" ]]; then
printf '%s' "$ASC_API_KEY_P8_BASE64" | decode_base64_to_file "$key_path"
elif [[ -n "${APPSTORE_KEY:-}" ]]; then
printf '%s\n' "$APPSTORE_KEY" > "$key_path"
elif [[ -n "${ASC_API_KEY:-}" ]]; then
printf '%s\n' "$ASC_API_KEY" > "$key_path"
else
echo "::error ::${file} must define ASC_API_KEY_BASE64 or ASC_API_KEY." >&2
exit 1
fi
chmod 600 "$key_path"
key_value="$(cat "$key_path")"
mask_multiline "$key_value"
echo "::add-mask::${ASC_API_KEY_ID}"
echo "::add-mask::${ASC_API_ISSUER_ID}"
write_env ASC_API_KEY_ID "$ASC_API_KEY_ID"
write_env ASC_API_ISSUER_ID "$ASC_API_ISSUER_ID"
write_env ASC_API_KEY_PATH "$key_path"
}
decrypt_distribution_signing() {
local file="$ROOT/secrets/apple/distribution-signing.env.age"
if [[ ! -f "$file" ]]; then
echo "::notice ::Skipping optional release secret ${file}"
return 0
fi
local temp_file cert_path cert_password cert_base64
temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-signing-env.XXXXXX")"
decrypt_age_file "$file" > "$temp_file"
# shellcheck disable=SC1090
set -a
source "$temp_file"
set +a
rm -f "$temp_file"
cert_base64="${APPLE_SIGNING_CERTIFICATE_BASE64:-${APPLE_DISTRIBUTION_CERTIFICATE_BASE64:-}}"
cert_password="${APPLE_SIGNING_CERTIFICATE_PASSWORD:-${APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD:-}}"
if [[ -z "$cert_password" && -n "${APPLE_SIGNING_CERTIFICATE_PASSWORD_BASE64:-}" ]]; then
cert_password="$(printf '%s' "$APPLE_SIGNING_CERTIFICATE_PASSWORD_BASE64" | decode_base64_stream)"
fi
if [[ -z "$cert_base64" || -z "$cert_password" ]]; then
echo "::error ::${file} must define APPLE_SIGNING_CERTIFICATE_BASE64 and APPLE_SIGNING_CERTIFICATE_PASSWORD." >&2
exit 1
fi
cert_path="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-apple-signing.XXXXXX.p12")"
printf '%s' "$cert_base64" | decode_base64_to_file "$cert_path"
chmod 600 "$cert_path"
echo "::add-mask::${cert_password}"
write_env APPLE_SIGNING_CERTIFICATE_PATH "$cert_path"
write_env APPLE_SIGNING_CERTIFICATE_PASSWORD "$cert_password"
write_env APPLE_KEYCHAIN_PASSWORD "$cert_password"
}
decrypt_sparkle() {
local file="$ROOT/secrets/apple/sparkle.env.age"
if [[ ! -f "$file" ]]; then
echo "::notice ::Skipping optional release secret ${file}"
return 0
fi
local temp_file key_path key_base64 key_value
temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-sparkle-env.XXXXXX")"
decrypt_age_file "$file" > "$temp_file"
# shellcheck disable=SC1090
set -a
source "$temp_file"
set +a
rm -f "$temp_file"
key_base64="${SPARKLE_EDDSA_KEY_BASE64:-${SPARKLE_PRIVATE_KEY_BASE64:-}}"
key_path="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-sparkle-eddsa.XXXXXX.key")"
if [[ -n "$key_base64" ]]; then
printf '%s' "$key_base64" | decode_base64_to_file "$key_path"
elif [[ -n "${SPARKLE_EDDSA_KEY:-}" ]]; then
printf '%s\n' "$SPARKLE_EDDSA_KEY" > "$key_path"
elif [[ -n "${SPARKLE_PRIVATE_KEY:-}" ]]; then
printf '%s\n' "$SPARKLE_PRIVATE_KEY" > "$key_path"
else
echo "::error ::${file} must define SPARKLE_EDDSA_KEY_BASE64 or SPARKLE_EDDSA_KEY." >&2
exit 1
fi
chmod 600 "$key_path"
key_value="$(cat "$key_path")"
mask_multiline "$key_value"
write_env SPARKLE_EDDSA_KEY_PATH "$key_path"
}
decrypt_appstore_connect
decrypt_distribution_signing
decrypt_sparkle
decrypt_dotenv_optional "$ROOT/secrets/hetzner/object-storage.age"

View file

@ -1,98 +0,0 @@
name: "Apple: Developer ID KMS CSR"
run-name: "Apple: Developer ID KMS CSR (${{ github.event.inputs.kms_key || 'apple-developer-id-application' }})"
on:
workflow_dispatch:
inputs:
kms_key:
description: Google KMS key name in the Burrow identity key ring
required: false
default: apple-developer-id-application
kms_version:
description: Google KMS key version
required: false
default: "1"
common_name:
description: CSR common name
required: false
default: Burrow Developer ID Application
email_address:
description: CSR email address
required: false
default: release@burrow.net
permissions:
contents: read
concurrency:
group: apple-developer-id-kms-csr-${{ github.ref }}
cancel-in-progress: false
env:
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_KMS_LOCATION: global
BURROW_KMS_KEYRING: burrow-identity
jobs:
csr:
name: Generate CSR
runs-on: [self-hosted, linux, x86_64, burrow-forge]
timeout-minutes: 20
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Authenticate Google WIF
shell: bash
run: nix develop .#ci -c Scripts/ci/google-wif-auth.sh
- name: Generate KMS-backed CSR
shell: bash
env:
BURROW_APPLE_DEVELOPER_ID_KMS_KEY: ${{ github.event.inputs.kms_key || 'apple-developer-id-application' }}
BURROW_KMS_VERSION: ${{ github.event.inputs.kms_version || '1' }}
BURROW_APPLE_DEVELOPER_ID_CSR_COMMON_NAME: ${{ github.event.inputs.common_name || 'Burrow Developer ID Application' }}
BURROW_APPLE_DEVELOPER_ID_CSR_EMAIL: ${{ github.event.inputs.email_address || 'release@burrow.net' }}
run: |
set -euo pipefail
mkdir -p dist/apple-developer-id
nix develop .#ci -c Scripts/apple/google-kms-csr.py \
--output dist/apple-developer-id/developer-id-application.certSigningRequest \
--public-key-output dist/apple-developer-id/google-kms-public-key.pem
if command -v openssl >/dev/null 2>&1; then
openssl req -in dist/apple-developer-id/developer-id-application.certSigningRequest -noout -verify
fi
cat > dist/apple-developer-id/README.txt <<'EOF'
This CSR is backed by a non-exportable Google Cloud KMS key in the Burrow
identity key ring. Apple currently documents Developer ID certificate creation
through the Developer website or Xcode, not App Store Connect API creation.
Upload developer-id-application.certSigningRequest in:
Certificates, Identifiers & Profiles -> Certificates -> + -> Developer ID ->
Developer ID Application.
Keep the returned .cer file with release artifacts. The private key remains in
Google Cloud KMS and is never exported as a p12.
EOF
- name: Upload CSR Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-developer-id-kms-csr
path: dist/apple-developer-id

View file

@ -1,530 +0,0 @@
name: "Apple: Distribute Testers"
run-name: "Apple: Distribute Testers (${{ github.ref_name }})"
on:
workflow_dispatch:
inputs:
build_number_override:
description: Optional explicit build number
required: false
default: ""
upload_app_store:
description: Upload iOS artifact to App Store Connect and TestFlight
required: false
default: "true"
upload_sparkle:
description: Publish Sparkle appcast and macOS artifact
required: false
default: "true"
sparkle_channel:
description: Sparkle channel, defaults to build-<number>
required: false
default: ""
sparkle_point_main:
description: Update Sparkle main channel pointers
required: false
default: "true"
testflight_distribute_external:
description: Distribute to external TestFlight groups
required: false
default: "false"
testflight_groups:
description: Optional comma-separated TestFlight groups
required: false
default: ""
ios_uses_non_exempt_encryption:
description: Set true only when the iOS build uses non-exempt encryption
required: false
default: "false"
permissions:
actions: write
contents: read
concurrency:
group: burrow-apple-distribute-testers-${{ github.ref }}
cancel-in-progress: true
env:
BURROW_RELEASE_GCS_BUCKET: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }}
BURROW_RELEASE_GCS_BACKUP: ${{ vars.BURROW_RELEASE_GCS_BACKUP || 'true' }}
BURROW_RELEASE_GARAGE_BUCKET: ${{ vars.BURROW_RELEASE_GARAGE_BUCKET || 'burrow-releases' }}
BURROW_RELEASE_REQUIRE_GARAGE: ${{ vars.BURROW_RELEASE_REQUIRE_GARAGE || 'true' }}
BURROW_GARAGE_ENDPOINT: ${{ vars.BURROW_GARAGE_ENDPOINT || 'https://objects.burrow.net' }}
BURROW_GARAGE_REGION: ${{ vars.BURROW_GARAGE_REGION || 'garage' }}
AWS_ACCESS_KEY_ID: ${{ secrets.BURROW_GARAGE_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BURROW_GARAGE_SECRET_ACCESS_KEY }}
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }}
BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }}
BURROW_DEVELOPER_ID_CERTIFICATE_ID: ${{ vars.BURROW_DEVELOPER_ID_CERTIFICATE_ID || '9JKN6HXBHC' }}
BURROW_APPLE_SIGNING_MODE: google-kms-staged
BURROW_NOTARIZE_MACOS: ${{ vars.BURROW_NOTARIZE_MACOS || 'true' }}
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'true' }}
BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }}
BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }}
BURROW_SPARKLE_FEED_URL: ${{ vars.BURROW_SPARKLE_FEED_URL || 'https://releases.burrow.net/sparkle/appcast.xml' }}
BURROW_SPARKLE_PUBLIC_ED_KEY: ${{ vars.BURROW_SPARKLE_PUBLIC_ED_KEY || 'uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=' }}
BURROW_KMS_LOCATION: ${{ vars.BURROW_KMS_LOCATION || 'global' }}
BURROW_KMS_KEYRING: ${{ vars.BURROW_KMS_KEYRING || 'burrow-identity' }}
TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS: ${{ vars.TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS || '7200' }}
BURROW_VERSION_REQUIRE_REMOTE: "1"
jobs:
prepare:
name: Prepare Apple Distribution
runs-on: namespace-profile-linux-medium-apple-v2
outputs:
build_number: ${{ steps.plan.outputs.build_number }}
sparkle_channel: ${{ steps.plan.outputs.sparkle_channel }}
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Fetch Build Tags
shell: bash
run: git fetch --force --prune origin "+refs/tags/builds/*:refs/tags/builds/*"
- name: Compute Apple Distribution Plan
id: plan
shell: bash
env:
BUILD_NUMBER_OVERRIDE: ${{ github.event.inputs.build_number_override || '' }}
SPARKLE_CHANNEL_INPUT: ${{ github.event.inputs.sparkle_channel || '' }}
run: |
set -euo pipefail
if [[ -n "$BUILD_NUMBER_OVERRIDE" ]]; then
build_number="$BUILD_NUMBER_OVERRIDE"
else
build_number="$(Scripts/version.sh pre-increment)"
fi
channel="$SPARKLE_CHANNEL_INPUT"
if [[ -z "$channel" ]]; then
channel="build-${build_number}"
fi
echo "build_number=${build_number}" >> "$GITHUB_OUTPUT"
echo "sparkle_channel=${channel}" >> "$GITHUB_OUTPUT"
printf 'build_number=%s\nsparkle_channel=%s\n' "$build_number" "$channel"
build-ios:
name: Build Apple iOS
needs: prepare
runs-on: namespace-profile-macos-large-apple-v2
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
submodules: recursive
- name: Select Apple SDK
shell: bash
run: |
set -euo pipefail
xcrun --find swiftc
xcrun --sdk iphoneos --show-sdk-path
xcrun --sdk iphonesimulator --show-sdk-path
- name: Prepare Rust Apple Targets
shell: bash
run: |
set -euo pipefail
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
export PATH="$HOME/.cargo/bin:$PATH"
fi
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
- name: Bootstrap Runner Environment
shell: bash
run: |
set -euo pipefail
runner_home="${RUNNER_HOME:-}"
if [[ -z "$runner_home" ]]; then
runner_home="$(python3 -c 'import os,pwd; print(pwd.getpwuid(os.getuid()).pw_dir)' 2>/dev/null || true)"
if [[ -z "$runner_home" ]]; then
runner_home="$PWD/.home"
fi
fi
mkdir -p "$runner_home" "$PWD/.tmp"
{
echo "HOME=$runner_home"
echo "XDG_CACHE_HOME=$runner_home/.cache"
echo "XDG_CONFIG_HOME=$runner_home/.config"
echo "XDG_DATA_HOME=$runner_home/.local/share"
echo "TMPDIR=$PWD/.tmp"
echo "TMP=$PWD/.tmp"
} >> "$GITHUB_ENV"
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Namespace Cache
shell: bash
run: |
set -euo pipefail
bash Scripts/ci/nscloud-cache.sh bazel
- name: Build Staged iOS Artifact
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
bazel_args=()
if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" && -f "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then
bazel_args+=("--bazelrc=${BURROW_BAZEL_NSCCACHE_BAZELRC}")
fi
NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)"
ci_path="$("$NIX_BIN" develop .#ci --command bash -lc 'printf "%s" "$PATH"')"
ci_protoc="$("$NIX_BIN" develop .#ci --command bash -lc 'command -v protoc')"
"$NIX_BIN" develop .#ci --command bazel "${bazel_args[@]}" build \
--verbose_failures \
--show_timestamps \
--experimental_ui_max_stdouterr_bytes=16777216 \
--action_env=PATH="$ci_path" \
--action_env=PROTOC="$ci_protoc" \
--action_env=BUILD_WORKSPACE_DIRECTORY="$PWD" \
--action_env=BUILD_NUMBER="$BUILD_NUMBER" \
--action_env=BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER" \
--action_env=BURROW_APPLE_SIGNING_MODE="$BURROW_APPLE_SIGNING_MODE" \
--action_env=BURROW_APPLE_REQUIRE_SIGNING=true \
--action_env=BURROW_APPLE_BUNDLE_ID="$BURROW_APPLE_BUNDLE_ID" \
--action_env=BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID="$BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID" \
--action_env=HOME="$HOME" \
--action_env=XDG_CACHE_HOME="$XDG_CACHE_HOME" \
//bazel/apple:release_ios_stamp
- name: Upload Staged iOS Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-apple-ios-${{ needs.prepare.outputs.build_number }}
path: |
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-iOS-${{ needs.prepare.outputs.build_number }}.unsigned.ipa
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-iOS-${{ needs.prepare.outputs.build_number }}.unsigned.ipa.sha256
if-no-files-found: error
build-macos:
name: Build Apple macOS
needs: prepare
runs-on: namespace-profile-macos-large-apple-v2
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
submodules: recursive
- name: Select Apple SDK
shell: bash
run: |
set -euo pipefail
xcrun --find swiftc
xcrun --sdk macosx --show-sdk-path
- name: Prepare Rust Apple Targets
shell: bash
run: |
set -euo pipefail
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
export PATH="$HOME/.cargo/bin:$PATH"
fi
rustup target add aarch64-apple-darwin x86_64-apple-darwin
- name: Bootstrap Runner Environment
shell: bash
run: |
set -euo pipefail
runner_home="${RUNNER_HOME:-}"
if [[ -z "$runner_home" ]]; then
runner_home="$(python3 -c 'import os,pwd; print(pwd.getpwuid(os.getuid()).pw_dir)' 2>/dev/null || true)"
if [[ -z "$runner_home" ]]; then
runner_home="$PWD/.home"
fi
fi
mkdir -p "$runner_home" "$PWD/.tmp"
{
echo "HOME=$runner_home"
echo "XDG_CACHE_HOME=$runner_home/.cache"
echo "XDG_CONFIG_HOME=$runner_home/.config"
echo "XDG_DATA_HOME=$runner_home/.local/share"
echo "TMPDIR=$PWD/.tmp"
echo "TMP=$PWD/.tmp"
} >> "$GITHUB_ENV"
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Namespace Cache
shell: bash
run: |
set -euo pipefail
bash Scripts/ci/nscloud-cache.sh bazel
- name: Build Staged macOS Artifact
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
bazel_args=()
if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" && -f "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then
bazel_args+=("--bazelrc=${BURROW_BAZEL_NSCCACHE_BAZELRC}")
fi
NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)"
ci_path="$("$NIX_BIN" develop .#ci --command bash -lc 'printf "%s" "$PATH"')"
ci_protoc="$("$NIX_BIN" develop .#ci --command bash -lc 'command -v protoc')"
"$NIX_BIN" develop .#ci --command bazel "${bazel_args[@]}" build \
--verbose_failures \
--show_timestamps \
--experimental_ui_max_stdouterr_bytes=16777216 \
--action_env=PATH="$ci_path" \
--action_env=PROTOC="$ci_protoc" \
--action_env=BUILD_WORKSPACE_DIRECTORY="$PWD" \
--action_env=BUILD_NUMBER="$BUILD_NUMBER" \
--action_env=BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER" \
--action_env=BURROW_APPLE_SIGNING_MODE="$BURROW_APPLE_SIGNING_MODE" \
--action_env=BURROW_APPLE_REQUIRE_SIGNING=true \
--action_env=BURROW_APPLE_BUNDLE_ID="$BURROW_APPLE_BUNDLE_ID" \
--action_env=BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID="$BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID" \
--action_env=BURROW_SPARKLE_FEED_URL="$BURROW_SPARKLE_FEED_URL" \
--action_env=BURROW_SPARKLE_PUBLIC_ED_KEY="$BURROW_SPARKLE_PUBLIC_ED_KEY" \
--action_env=HOME="$HOME" \
--action_env=XDG_CACHE_HOME="$XDG_CACHE_HOME" \
//bazel/apple:release_macos_stamp
- name: Upload Staged macOS Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-apple-macos-${{ needs.prepare.outputs.build_number }}
path: |
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-macOS-${{ needs.prepare.outputs.build_number }}.unsigned.zip
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-macOS-${{ needs.prepare.outputs.build_number }}.unsigned.zip.sha256
if-no-files-found: error
sign-apple:
name: Sign Apple Artifacts
needs:
- prepare
- build-ios
- build-macos
runs-on: namespace-profile-linux-medium-apple-v2
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Validate App Store Credentials
shell: bash
run: Scripts/ci/check-release-config.sh app-store
- name: Authenticate Google WIF
shell: bash
run: nix develop .#ci -c Scripts/ci/google-wif-auth.sh
- name: Download Staged Apple Artifacts
uses: https://code.forgejo.org/actions/download-artifact@v3
with:
path: dist/downloaded
- name: Prepare Staged Apple Directory
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
mkdir -p "dist/builds/${BUILD_NUMBER}/apple"
find dist/downloaded -type f -name 'Burrow-*.unsigned.*' -exec cp -v {} "dist/builds/${BUILD_NUMBER}/apple/" \;
find "dist/builds/${BUILD_NUMBER}/apple" -type f -print | sort
- name: Sync Apple Provisioning Profiles
shell: bash
run: nix develop .#ci -c Scripts/ci/sync-apple-provisioning-profiles.sh all
- name: Sign Apple Artifacts With KMS
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
SPARKLE_CHANNEL: ${{ needs.prepare.outputs.sparkle_channel }}
run: nix develop .#ci -c Scripts/apple/sign-release-artifacts-kms.sh all
- name: Upload Signed Apple Artifacts To Release Storage
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: nix develop .#ci -c Scripts/ci/upload-release-storage.sh
- name: Upload Signed Apple Artifacts
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-apple-signed-${{ needs.prepare.outputs.build_number }}
path: |
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-iOS-${{ needs.prepare.outputs.build_number }}.ipa
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-iOS-${{ needs.prepare.outputs.build_number }}.ipa.sha256
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-macOS-${{ needs.prepare.outputs.build_number }}.zip
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/Burrow-macOS-${{ needs.prepare.outputs.build_number }}.zip.sha256
dist/builds/${{ needs.prepare.outputs.build_number }}/sparkle/**
if-no-files-found: error
upload-testflight:
name: Upload TestFlight
needs:
- prepare
- sign-apple
if: ${{ github.event.inputs.upload_app_store == 'true' }}
runs-on: namespace-profile-macos-large-testflight
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Download Signed Apple Artifacts
uses: https://code.forgejo.org/actions/download-artifact@v3
with:
name: burrow-apple-signed-${{ needs.prepare.outputs.build_number }}
path: publish
- name: Upload iOS IPA To App Store Connect
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
Scripts/ci/check-release-config.sh app-store
key_dir="${RUNNER_TEMP:-/tmp}/burrow-asc"
altool_key_dir="${HOME}/.appstoreconnect/private_keys"
mkdir -p "$key_dir" "$altool_key_dir"
key_path="$key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then
cp "$ASC_API_KEY_PATH" "$key_path"
else
printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_path"
fi
cp "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
chmod 600 "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
ipa="publish/apple/Burrow-iOS-${BUILD_NUMBER}.ipa"
if [[ ! -s "$ipa" ]]; then
echo "::error ::No signed iOS IPA found at $ipa" >&2
find publish -type f -print | sort
exit 1
fi
upload_log="${RUNNER_TEMP:-/tmp}/burrow-altool-ios-${BUILD_NUMBER}.log"
set +e
xcrun altool --upload-app \
--type ios \
--file "$ipa" \
--apiKey "$ASC_API_KEY_ID" \
--apiIssuer "$ASC_API_ISSUER_ID" 2>&1 | tee "$upload_log"
altool_status=${PIPESTATUS[0]}
set -e
if (( altool_status != 0 )) || grep -Eq 'UPLOAD FAILED|Validation failed|ERROR:' "$upload_log"; then
echo "::error ::altool reported upload validation failure for $ipa" >&2
exit 1
fi
- name: Distribute iOS Build To TestFlight
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }}
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }}
run: Scripts/ci/distribute-testflight.sh
publish-sparkle:
name: Publish Sparkle
needs:
- prepare
- sign-apple
if: ${{ github.event.inputs.upload_sparkle == 'true' }}
runs-on: namespace-profile-linux-medium-apple-v2
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Authenticate Google WIF
shell: bash
run: nix develop .#ci -c Scripts/ci/google-wif-auth.sh
- name: Download Signed Apple Artifacts
uses: https://code.forgejo.org/actions/download-artifact@v3
with:
name: burrow-apple-signed-${{ needs.prepare.outputs.build_number }}
path: publish
- name: Publish Sparkle Channel
shell: bash
env:
CHANNEL: ${{ needs.prepare.outputs.sparkle_channel }}
SPARKLE_POINT_MAIN: ${{ github.event.inputs.sparkle_point_main || 'true' }}
run: |
set -euo pipefail
if [[ ! -f "publish/sparkle/${CHANNEL}/appcast.xml" ]]; then
echo "::error ::No Sparkle appcast staged for channel ${CHANNEL}" >&2
find publish -type f -print | sort
exit 1
fi
nix develop .#ci -c gcloud storage rsync --recursive "publish/sparkle/${CHANNEL}" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/${CHANNEL}"
if [[ "$SPARKLE_POINT_MAIN" == "true" ]]; then
printf '%s\n' "$CHANNEL" > main-channel.txt
nix develop .#ci -c gcloud storage cp main-channel.txt "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/main-channel.txt"
nix develop .#ci -c gcloud storage cp "publish/sparkle/${CHANNEL}/appcast.xml" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/appcast.xml"
nix develop .#ci -c gcloud storage rsync --recursive "publish/sparkle/${CHANNEL}" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/default"
fi

View file

@ -1,99 +0,0 @@
name: "Apple: iOS Distribution KMS CSR"
run-name: "Apple: iOS Distribution KMS CSR (${{ github.event.inputs.kms_key || 'apple-ios-distribution' }})"
on:
workflow_dispatch:
inputs:
kms_key:
description: Google KMS key name in the Burrow identity key ring
required: false
default: apple-ios-distribution
kms_version:
description: Google KMS key version
required: false
default: "1"
common_name:
description: CSR common name
required: false
default: Burrow iOS Distribution
email_address:
description: CSR email address
required: false
default: release@burrow.net
permissions:
contents: read
concurrency:
group: apple-ios-distribution-kms-csr-${{ github.ref }}
cancel-in-progress: false
env:
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_KMS_LOCATION: global
BURROW_KMS_KEYRING: burrow-identity
jobs:
csr:
name: Generate CSR
runs-on: [self-hosted, linux, x86_64, burrow-forge]
timeout-minutes: 20
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Authenticate Google WIF
shell: bash
run: nix develop .#ci -c Scripts/ci/google-wif-auth.sh
- name: Generate KMS-backed CSR
shell: bash
run: |
set -euo pipefail
mkdir -p dist/apple-ios-distribution
nix develop .#ci -c Scripts/apple/google-kms-csr.py \
--kms-key "${{ github.event.inputs.kms_key || 'apple-ios-distribution' }}" \
--kms-version "${{ github.event.inputs.kms_version || '1' }}" \
--common-name "${{ github.event.inputs.common_name || 'Burrow iOS Distribution' }}" \
--email-address "${{ github.event.inputs.email_address || 'release@burrow.net' }}" \
--output dist/apple-ios-distribution/ios-distribution.certSigningRequest \
--public-key-output dist/apple-ios-distribution/google-kms-public-key.pem
if command -v openssl >/dev/null 2>&1; then
openssl req -in dist/apple-ios-distribution/ios-distribution.certSigningRequest -noout -verify
fi
cat > dist/apple-ios-distribution/README.txt <<'EOF'
This CSR is backed by a non-exportable Google Cloud KMS key in the Burrow
identity key ring.
Create an iOS Distribution certificate explicitly through App Store Connect:
Scripts/apple/create-asc-certificate.mjs \
--certificate-type IOS_DISTRIBUTION \
--csr-file ios-distribution.certSigningRequest \
--out-dir Apple/Certificates
Keep the returned .cer file with release artifacts. The private key remains in
Google Cloud KMS and is never exported as a p12.
EOF
- name: Upload CSR Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-ios-distribution-kms-csr
path: dist/apple-ios-distribution

View file

@ -1,67 +0,0 @@
name: "Backup: Garage Storage"
run-name: "Backup Garage Storage (${{ github.event.inputs.set || 'all' }})"
on:
schedule:
- cron: "17 9 * * *"
workflow_dispatch:
inputs:
set:
description: "Comma-separated backup set: all, releases, packages, nix-cache"
required: false
default: all
delete_unmatched:
description: Delete destination objects that are missing from Garage
required: false
default: "false"
permissions:
contents: read
concurrency:
group: backup-garage-storage-${{ github.event.inputs.set || 'all' }}
cancel-in-progress: false
jobs:
backup:
name: Mirror Garage To GCS
runs-on: namespace-profile-linux-medium
timeout-minutes: 90
env:
BURROW_GARAGE_ENDPOINT: ${{ vars.BURROW_GARAGE_ENDPOINT || 'https://objects.burrow.net' }}
BURROW_GARAGE_REGION: ${{ vars.BURROW_GARAGE_REGION || 'garage' }}
BURROW_GARAGE_BACKUP_SET: ${{ github.event.inputs.set || 'all' }}
BURROW_GARAGE_BACKUP_DELETE: ${{ github.event.inputs.delete_unmatched || 'false' }}
BURROW_RELEASE_GARAGE_BUCKET: ${{ vars.BURROW_RELEASE_GARAGE_BUCKET || 'burrow-releases' }}
BURROW_PACKAGE_GARAGE_BUCKET: ${{ vars.BURROW_PACKAGE_GARAGE_BUCKET || 'burrow-packages' }}
BURROW_NIX_CACHE_GARAGE_BUCKET: ${{ vars.BURROW_NIX_CACHE_GARAGE_BUCKET || 'attic' }}
BURROW_RELEASE_GCS_BUCKET: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }}
BURROW_PACKAGE_GCS_BUCKET: ${{ vars.BURROW_PACKAGE_GCS_BUCKET || 'burrow-net-packages' }}
BURROW_NIX_CACHE_GCS_BUCKET: ${{ vars.BURROW_NIX_CACHE_GCS_BUCKET || 'burrow-net-nix-cache' }}
AWS_ACCESS_KEY_ID: ${{ secrets.BURROW_GARAGE_BACKUP_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BURROW_GARAGE_BACKUP_SECRET_ACCESS_KEY }}
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Mirror Garage Buckets To GCS
shell: bash
run: nix develop .#ci -c Scripts/ci/backup-garage-to-gcs.sh

View file

@ -1,74 +0,0 @@
name: "Build: Android"
run-name: "Build: Android (${{ github.ref_name }})"
on:
push:
branches:
- main
paths:
- ".forgejo/workflows/build-android.yml"
- "Android/**"
- "BUILD.bazel"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "bazel/android/**"
- "Cargo.lock"
- "Cargo.toml"
- "crates/burrow-core/**"
- "crates/burrow-mobile-ffi/**"
- "rust-toolchain.toml"
- "Scripts/ci/nscloud-cache.sh"
pull_request:
paths:
- ".forgejo/workflows/build-android.yml"
- "Android/**"
- "BUILD.bazel"
- "MODULE.bazel"
- "MODULE.bazel.lock"
- "bazel/android/**"
- "Cargo.lock"
- "Cargo.toml"
- "crates/burrow-core/**"
- "crates/burrow-mobile-ffi/**"
- "rust-toolchain.toml"
- "Scripts/ci/nscloud-cache.sh"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: burrow-build-android-${{ github.ref }}
cancel-in-progress: true
env:
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
jobs:
stub:
name: Android Rust Core Stub
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Configure Cache
shell: bash
run: |
set -euo pipefail
bash Scripts/ci/ensure-nix.sh
nix develop .#ci -c Scripts/ci/nscloud-cache.sh bazel
- name: Build Android Stub Check
shell: bash
run: |
set -euo pipefail
if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then
bazel --bazelrc="$BURROW_BAZEL_NSCCACHE_BAZELRC" build //bazel/android:check_stub_stamp
else
bazel build //bazel/android:check_stub_stamp
fi

View file

@ -1,480 +0,0 @@
name: "Build: Release"
run-name: "Build: Release (${{ github.ref_name }})"
on:
workflow_dispatch:
inputs:
build_number_override:
description: Optional explicit build number
required: false
default: ""
upload_app_store:
description: Trigger downstream App Store Connect upload workflow
required: false
default: "true"
upload_sparkle:
description: Trigger downstream Sparkle publish workflow
required: false
default: "true"
upload_microsoft_store:
description: Trigger downstream Microsoft Store beta upload workflow
required: false
default: "false"
sparkle_point_main:
description: Update Sparkle main channel pointers
required: false
default: "true"
testflight_distribute_external:
description: Distribute to external TestFlight groups
required: false
default: "false"
testflight_groups:
description: Optional comma-separated TestFlight groups for external distribution
required: false
default: ""
ios_uses_non_exempt_encryption:
description: Set true only when the iOS build uses non-exempt encryption
required: false
default: "false"
push:
tags:
- "release/*"
permissions:
actions: write
contents: write
concurrency:
group: burrow-build-release-${{ github.ref }}
cancel-in-progress: true
env:
BURROW_RELEASE_GCS_BUCKET: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }}
BURROW_RELEASE_GCS_BACKUP: ${{ vars.BURROW_RELEASE_GCS_BACKUP || 'true' }}
BURROW_RELEASE_GARAGE_BUCKET: ${{ vars.BURROW_RELEASE_GARAGE_BUCKET || 'burrow-releases' }}
BURROW_RELEASE_REQUIRE_GARAGE: ${{ vars.BURROW_RELEASE_REQUIRE_GARAGE || 'true' }}
BURROW_GARAGE_ENDPOINT: ${{ vars.BURROW_GARAGE_ENDPOINT || 'https://objects.burrow.net' }}
BURROW_GARAGE_REGION: ${{ vars.BURROW_GARAGE_REGION || 'garage' }}
AWS_ACCESS_KEY_ID: ${{ secrets.BURROW_GARAGE_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BURROW_GARAGE_SECRET_ACCESS_KEY }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }}
BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }}
BURROW_DEVELOPER_ID_CERTIFICATE_ID: ${{ vars.BURROW_DEVELOPER_ID_CERTIFICATE_ID || '9JKN6HXBHC' }}
BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS: ${{ vars.BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS || 'false' }}
BURROW_APPLE_ENABLE_CAPABILITIES: ${{ vars.BURROW_APPLE_ENABLE_CAPABILITIES || 'false' }}
BURROW_APPLE_SIGNING_MODE: ${{ vars.BURROW_APPLE_SIGNING_MODE || 'google-kms-staged' }}
BURROW_NOTARIZE_MACOS: ${{ vars.BURROW_NOTARIZE_MACOS || 'true' }}
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'true' }}
BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }}
BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }}
BURROW_SPARKLE_FEED_URL: ${{ vars.BURROW_SPARKLE_FEED_URL || 'https://releases.burrow.net/sparkle/appcast.xml' }}
BURROW_SPARKLE_PUBLIC_ED_KEY: ${{ vars.BURROW_SPARKLE_PUBLIC_ED_KEY || 'uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=' }}
BURROW_VERSION_REQUIRE_REMOTE: "1"
jobs:
prepare:
name: Prepare
runs-on: namespace-profile-linux-medium
outputs:
do_release: ${{ steps.plan.outputs.do_release }}
build_number: ${{ steps.plan.outputs.build_number }}
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Fetch Build Tags
shell: bash
run: git fetch --force --prune origin "+refs/tags/builds/*:refs/tags/builds/*"
- name: Compute Release Plan
id: plan
shell: bash
env:
BURROW_BUILD_NUMBER_OVERRIDE: ${{ github.event.inputs.build_number_override || '' }}
run: |
set -euo pipefail
status="$(Scripts/version.sh status)"
if [[ "$status" == "clean" ]]; then
echo "do_release=false" >> "$GITHUB_OUTPUT"
echo "build_number=" >> "$GITHUB_OUTPUT"
echo "No unreleased changes."
exit 0
fi
build_number="$(Scripts/version.sh pre-increment)"
echo "do_release=true" >> "$GITHUB_OUTPUT"
echo "build_number=${build_number}" >> "$GITHUB_OUTPUT"
build-linux:
name: Build (Linux)
needs: prepare
if: ${{ needs.prepare.outputs.do_release == 'true' }}
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Build Linux Release
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
bash Scripts/ci/ensure-nix.sh
nix develop .#ci -c Scripts/ci/nscloud-cache.sh nix
nix develop .#ci -c Scripts/ci/package-release-artifacts.sh linux
- name: Upload Linux Artifacts
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-linux-${{ needs.prepare.outputs.build_number }}
path: dist/builds/${{ needs.prepare.outputs.build_number }}/linux/*
if-no-files-found: error
- name: Upload Linux Artifacts To Release Storage
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
bash Scripts/ci/ensure-nix.sh
nix develop .#ci -c Scripts/ci/upload-release-storage.sh
build-windows:
name: Build (Windows Stub)
needs: prepare
if: ${{ needs.prepare.outputs.do_release == 'true' }}
runs-on: namespace-profile-windows-large
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Build Rust Stub
shell: pwsh
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
Invoke-WebRequest -Uri "https://win.rustup.rs" -OutFile "rustup-init.exe"
.\rustup-init.exe -y --profile minimal --default-toolchain stable
$env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path"
}
cargo build --locked --release -p burrow-windows-stub
New-Item -ItemType Directory -Force -Path "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc" | Out-Null
Copy-Item "target/release/burrow.exe" "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc/burrow.exe"
Copy-Item "README.md" "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc/README.md"
Compress-Archive -Path "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc" -DestinationPath "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip" -Force
Get-FileHash "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip" -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip" } | Set-Content "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip.sha256"
- name: Upload Windows Artifacts
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-windows-${{ needs.prepare.outputs.build_number }}
path: dist/builds/${{ needs.prepare.outputs.build_number }}/windows/*
if-no-files-found: error
build-apple:
name: Build (Apple ${{ matrix.platform_name }})
needs: prepare
if: ${{ needs.prepare.outputs.do_release == 'true' }}
runs-on: namespace-profile-macos-large
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- platform: ios
platform_name: iOS
bazel_target: //bazel/apple:release_ios_stamp
- platform: macos
platform_name: macOS
bazel_target: //bazel/apple:release_macos_stamp
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
submodules: recursive
- name: Select Apple SDK
shell: bash
run: |
set -euo pipefail
if ! command -v xcrun >/dev/null 2>&1; then
echo "::error ::xcrun is required for Apple release builds" >&2
exit 1
fi
xcrun --find swiftc
xcrun --sdk macosx --show-sdk-path
xcrun --sdk iphoneos --show-sdk-path
xcrun --sdk iphonesimulator --show-sdk-path
- name: Prepare Rust Apple Targets
shell: bash
env:
PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
export PATH="$HOME/.cargo/bin:$PATH"
fi
case "$PLATFORM" in
ios)
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
;;
macos)
rustup target add aarch64-apple-darwin x86_64-apple-darwin
;;
*)
echo "::error ::unsupported Apple platform ${PLATFORM}" >&2
exit 1
;;
esac
- name: Bootstrap Runner Environment
shell: bash
run: |
set -euo pipefail
runner_home="${RUNNER_HOME:-}"
if [[ -z "$runner_home" ]]; then
runner_home="$(python3 -c 'import os,pwd; print(pwd.getpwuid(os.getuid()).pw_dir)' 2>/dev/null || true)"
if [[ -z "$runner_home" ]]; then
runner_home="$PWD/.home"
fi
fi
mkdir -p "$runner_home" "$PWD/.tmp"
{
echo "HOME=$runner_home"
echo "XDG_CACHE_HOME=$runner_home/.cache"
echo "XDG_CONFIG_HOME=$runner_home/.config"
echo "XDG_DATA_HOME=$runner_home/.local/share"
echo "TMPDIR=$PWD/.tmp"
echo "TMP=$PWD/.tmp"
} >> "$GITHUB_ENV"
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Namespace Cache
shell: bash
run: |
set -euo pipefail
NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)"
"$NIX_BIN" develop .#ci --command bash Scripts/ci/nscloud-cache.sh bazel
- name: Configure Age
id: age_apple
continue-on-error: true
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
if: ${{ steps.age_apple.outcome == 'success' }}
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Sync Apple Provisioning Profiles
shell: bash
env:
PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)"
"$NIX_BIN" develop .#ci --command Scripts/ci/sync-apple-provisioning-profiles.sh "$PLATFORM"
- name: Install Apple Signing Assets
shell: bash
run: Scripts/ci/install-apple-signing-assets.sh
- name: Build Apple Release
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
PLATFORM: ${{ matrix.platform }}
UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'true' }}
UPLOAD_SPARKLE: ${{ github.event.inputs.upload_sparkle || 'true' }}
SPARKLE_CHANNEL: build-${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
require_signing=false
if [[ "$UPLOAD_APP_STORE" == "true" && "$PLATFORM" == "ios" ]]; then
require_signing=true
fi
if [[ "$UPLOAD_SPARKLE" == "true" && "$PLATFORM" == "macos" ]]; then
require_signing=true
fi
bazel_args=()
if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" && -f "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then
bazel_args+=("--bazelrc=${BURROW_BAZEL_NSCCACHE_BAZELRC}")
fi
NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)"
ci_path="$("$NIX_BIN" develop .#ci --command bash -lc 'printf "%s" "$PATH"')"
ci_protoc="$("$NIX_BIN" develop .#ci --command bash -lc 'command -v protoc')"
"$NIX_BIN" develop .#ci --command bazel "${bazel_args[@]}" build \
--verbose_failures \
--show_timestamps \
--experimental_ui_max_stdouterr_bytes=16777216 \
--action_env=PATH="$ci_path" \
--action_env=PROTOC="$ci_protoc" \
--action_env=BUILD_WORKSPACE_DIRECTORY="$PWD" \
--action_env=BUILD_NUMBER="$BUILD_NUMBER" \
--action_env=BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER" \
--action_env=BURROW_APPLE_SIGNING_READY="${BURROW_APPLE_SIGNING_READY:-0}" \
--action_env=BURROW_APPLE_SIGNING_MODE="${BURROW_APPLE_SIGNING_MODE:-keychain}" \
--action_env=BURROW_APPLE_REQUIRE_SIGNING="$require_signing" \
--action_env=BURROW_APPLE_BUNDLE_ID="${BURROW_APPLE_BUNDLE_ID:-}" \
--action_env=BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-}" \
--action_env=BURROW_APPLE_KEYCHAIN_PATH="${BURROW_APPLE_KEYCHAIN_PATH:-}" \
--action_env=BURROW_NOTARIZE_MACOS="${BURROW_NOTARIZE_MACOS:-false}" \
--action_env=BURROW_SPARKLE_SIGN_WITH_KMS="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}" \
--action_env=BURROW_SPARKLE_KMS_KEY="${BURROW_SPARKLE_KMS_KEY:-sparkle-ed25519}" \
--action_env=BURROW_SPARKLE_KMS_VERSION="${BURROW_SPARKLE_KMS_VERSION:-1}" \
--action_env=BURROW_SPARKLE_FEED_URL="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}" \
--action_env=BURROW_SPARKLE_PUBLIC_ED_KEY="${BURROW_SPARKLE_PUBLIC_ED_KEY:-}" \
--action_env=GOOGLE_CLOUD_PROJECT="${GOOGLE_CLOUD_PROJECT:-}" \
--action_env=ASC_API_KEY_ID="${ASC_API_KEY_ID:-}" \
--action_env=ASC_API_ISSUER_ID="${ASC_API_ISSUER_ID:-}" \
--action_env=ASC_API_KEY_PATH="${ASC_API_KEY_PATH:-}" \
--action_env=SPARKLE_CHANNEL="$SPARKLE_CHANNEL" \
--action_env=HOME="$HOME" \
--action_env=XDG_CACHE_HOME="$XDG_CACHE_HOME" \
"${{ matrix.bazel_target }}"
- name: Upload Apple Artifacts
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-apple-${{ matrix.platform }}-${{ needs.prepare.outputs.build_number }}
path: |
dist/builds/${{ needs.prepare.outputs.build_number }}/apple/*
dist/builds/${{ needs.prepare.outputs.build_number }}/sparkle/**
if-no-files-found: error
- name: Upload Apple Artifacts To Release Storage
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
bash Scripts/ci/ensure-nix.sh
nix develop .#ci -c Scripts/ci/upload-release-storage.sh
publish:
name: Publish (Forgejo)
needs:
- prepare
- build-linux
- build-windows
- build-apple
if: ${{ always() && needs.prepare.outputs.do_release == 'true' }}
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Download Artifacts
uses: https://code.forgejo.org/actions/download-artifact@v3
with:
path: dist/downloaded
- name: Prepare Release Assets
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
mkdir -p dist
mkdir -p "dist/builds/${BUILD_NUMBER}/windows"
find dist/downloaded -path '*/burrow-windows-*/*' -type f -exec cp {} "dist/builds/${BUILD_NUMBER}/windows/" \;
find dist/downloaded -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.sha256' -o -name '*.ipa' -o -name '*.pkg' -o -name '*.dmg' -o -name 'README*.txt' \) -exec cp {} dist/ \;
rm -rf dist/downloaded
find dist -maxdepth 1 -type f -print | sort
- name: Upload Windows Artifacts To Release Storage
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
if ! find "dist/builds/${BUILD_NUMBER}/windows" -type f -print -quit | grep -q .; then
echo "::warning ::No Windows artifacts staged for release storage upload."
exit 0
fi
bash Scripts/ci/ensure-nix.sh
nix develop .#ci -c Scripts/ci/upload-release-storage.sh
- name: Tag Build
shell: bash
env:
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
run: |
set -euo pipefail
tag="builds/${BUILD_NUMBER}"
head_commit="$(git rev-parse HEAD)"
remote_commit="$(git ls-remote --tags --refs origin "refs/tags/${tag}" | awk 'NR==1 { print $1 }')"
if [[ -n "$remote_commit" && "$remote_commit" != "$head_commit" ]]; then
echo "::error ::Remote tag ${tag} already points at ${remote_commit}, not ${head_commit}" >&2
exit 1
fi
git tag -f "$tag" "$head_commit"
git push --quiet --no-verify origin "refs/tags/${tag}:refs/tags/${tag}"
- name: Publish Forgejo Release
shell: bash
env:
RELEASE_TAG: builds/${{ needs.prepare.outputs.build_number }}
API_URL: ${{ github.api_url }}
REPOSITORY: ${{ github.repository }}
TOKEN: ${{ github.token }}
run: Scripts/ci/publish-forgejo-release.sh
- name: Dispatch Store Uploads
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }}
UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'true' }}
UPLOAD_SPARKLE: ${{ github.event.inputs.upload_sparkle || 'true' }}
UPLOAD_MICROSOFT_STORE: ${{ github.event.inputs.upload_microsoft_store || 'false' }}
SPARKLE_POINT_MAIN: ${{ github.event.inputs.sparkle_point_main || 'true' }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }}
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }}
run: |
set -euo pipefail
inputs="$(python3 -c 'import json, os; print(json.dumps({"build_number":os.environ["BUILD_NUMBER"],"upload_app_store":os.environ["UPLOAD_APP_STORE"],"upload_sparkle":os.environ["UPLOAD_SPARKLE"],"upload_microsoft_store":os.environ["UPLOAD_MICROSOFT_STORE"],"sparkle_point_main":os.environ["SPARKLE_POINT_MAIN"],"testflight_distribute_external":os.environ["TESTFLIGHT_DISTRIBUTE_EXTERNAL"],"testflight_groups":os.environ["TESTFLIGHT_GROUPS"],"ios_uses_non_exempt_encryption":os.environ["IOS_USES_NON_EXEMPT_ENCRYPTION"]}))')"
curl -fsS \
-X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"ref\":\"main\",\"inputs\":${inputs}}" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/workflows/publish-store-uploads.yml/dispatches"

View file

@ -1,42 +0,0 @@
name: Build Rust
on:
push:
branches:
- main
pull_request:
branches:
- "**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
rust:
name: Cargo Test
runs-on: [self-hosted, linux, x86_64, burrow-forge]
steps:
- name: Checkout
shell: bash
run: |
set -euo pipefail
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
if [ ! -d .git ]; then
git init .
fi
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${repo_url}"
else
git remote add origin "${repo_url}"
fi
git fetch --force --tags origin "${GITHUB_SHA}"
git checkout --force --detach FETCH_HEAD
git clean -ffdqx
- name: Test
shell: bash
run: |
set -euo pipefail
nix develop .#ci -c cargo test --workspace --all-features

View file

@ -1,42 +0,0 @@
name: Build Site
on:
push:
branches:
- main
pull_request:
branches:
- "**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
site:
name: Next.js Build
runs-on: [self-hosted, linux, x86_64, burrow-forge]
steps:
- name: Checkout
shell: bash
run: |
set -euo pipefail
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
if [ ! -d .git ]; then
git init .
fi
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${repo_url}"
else
git remote add origin "${repo_url}"
fi
git fetch --force --tags origin "${GITHUB_SHA}"
git checkout --force --detach FETCH_HEAD
git clean -ffdqx
- name: Build
shell: bash
run: |
set -euo pipefail
nix develop .#ci -c bash -lc 'cd site && npm ci --no-audit --no-fund && npm run build'

View file

@ -1,71 +0,0 @@
name: "Apple: Distribute TestFlight"
run-name: "Apple: Distribute TestFlight (Build ${{ inputs.build_number }})"
on:
workflow_dispatch:
inputs:
build_number:
description: Already-uploaded App Store Connect build number
required: true
testflight_distribute_external:
description: Distribute to external TestFlight groups
required: false
default: "false"
testflight_groups:
description: Optional comma-separated TestFlight groups
required: false
default: ""
ios_uses_non_exempt_encryption:
description: Set true only when the iOS build uses non-exempt encryption
required: false
default: "false"
permissions:
contents: read
concurrency:
group: burrow-distribute-testflight-${{ inputs.build_number }}
cancel-in-progress: true
env:
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS: ${{ vars.TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS || '7200' }}
jobs:
distribute-ios-testflight:
name: Distribute (TestFlight iOS Testers)
runs-on: namespace-profile-linux-testflight
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Validate App Store Credentials
shell: bash
run: Scripts/ci/check-release-config.sh app-store
- name: Distribute iOS Build To TestFlight
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ inputs.testflight_groups || '' }}
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ inputs.ios_uses_non_exempt_encryption || 'false' }}
run: Scripts/ci/distribute-testflight.sh

View file

@ -1,237 +0,0 @@
name: "Infra: OpenTofu"
run-name: "Infra: OpenTofu (${{ github.event.inputs.stack || 'grafana' }} ${{ github.event.inputs.mode || 'validate' }})"
on:
push:
branches:
- main
paths:
- ".forgejo/workflows/infra-opentofu.yml"
- "Scripts/grafana-tofu.sh"
- "Scripts/identity-tofu.sh"
- "Scripts/openbao-tofu.sh"
- "Scripts/releases-tofu.sh"
- "infra/grafana/**"
- "infra/identity/**"
- "infra/openbao/**"
- "infra/releases/**"
- "services/grafana/**"
workflow_dispatch:
inputs:
stack:
description: OpenTofu stack to run
required: true
default: grafana
type: choice
options:
- grafana
- identity
- openbao
- releases
mode:
description: OpenTofu mode
required: true
default: plan
type: choice
options:
- fmt
- validate
- plan
- apply
- import
apply_confirmation:
description: Type apply-burrow-<stack>-tofu to run apply
required: false
default: ""
import_address:
description: OpenTofu resource address for import mode
required: false
default: ""
import_id:
description: Provider import id for import mode
required: false
default: ""
permissions:
contents: read
concurrency:
group: infra-opentofu-${{ github.ref }}-${{ github.event.inputs.stack || 'grafana' }}
cancel-in-progress: false
jobs:
opentofu:
name: OpenTofu (${{ github.event.inputs.stack || 'grafana' }})
runs-on: [self-hosted, linux, x86_64, burrow-forge]
timeout-minutes: 30
env:
MODE: ${{ github.event.inputs.mode || 'validate' }}
STACK: ${{ github.event.inputs.stack || 'grafana' }}
APPLY_CONFIRMATION: ${{ github.event.inputs.apply_confirmation || '' }}
IMPORT_ADDRESS: ${{ github.event.inputs.import_address || '' }}
IMPORT_ID: ${{ github.event.inputs.import_id || '' }}
TF_INPUT: "false"
TF_IN_AUTOMATION: "true"
TF_VAR_grafana_url: ${{ vars.GRAFANA_URL || 'https://graphs.burrow.net' }}
TF_VAR_google_project_id: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
TF_VAR_google_project_number: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
TF_VAR_google_runner_service_account_email: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
TF_VAR_google_wif_runner_client_id: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
TF_VAR_google_release_bucket_name: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }}
TF_VAR_google_package_bucket_name: ${{ vars.BURROW_PACKAGE_GCS_BUCKET || 'burrow-net-packages' }}
TF_VAR_google_nix_cache_bucket_name: ${{ vars.BURROW_NIX_CACHE_GCS_BUCKET || 'burrow-net-nix-cache' }}
TF_VAR_google_nix_cache_public_read: ${{ vars.BURROW_NIX_CACHE_GCS_PUBLIC_READ || 'false' }}
TF_VAR_google_storage_location: ${{ vars.BURROW_GCS_LOCATION || 'US' }}
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
OPENTOFU_STATE_BUCKET: ${{ vars.OPENTOFU_STATE_BUCKET || '' }}
OPENTOFU_STATE_REGION: ${{ vars.OPENTOFU_STATE_REGION || vars.AWS_REGION || 'us-west-2' }}
OPENTOFU_STATE_DYNAMODB_TABLE: ${{ vars.OPENTOFU_STATE_DYNAMODB_TABLE || '' }}
steps:
- name: Checkout
shell: bash
run: |
set -euo pipefail
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
if [ ! -d .git ]; then
git init .
fi
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${repo_url}"
else
git remote add origin "${repo_url}"
fi
git fetch --force --tags origin "${GITHUB_SHA}"
git checkout --force --detach FETCH_HEAD
git clean -ffdqx
- name: Validate Request
shell: bash
run: |
set -euo pipefail
case "$STACK" in
grafana|identity|openbao|releases) ;;
*)
echo "::error ::Unsupported OpenTofu stack: $STACK" >&2
exit 2
;;
esac
case "$MODE" in
fmt|validate|plan|apply|import) ;;
*)
echo "::error ::Unsupported OpenTofu mode: $MODE" >&2
exit 2
;;
esac
if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" && "$MODE" != "validate" ]]; then
echo "::error ::Non-manual runs may only validate." >&2
exit 2
fi
if [[ "$MODE" == "apply" && "$APPLY_CONFIRMATION" != "apply-burrow-${STACK}-tofu" ]]; then
echo "::error ::apply_confirmation must be exactly apply-burrow-${STACK}-tofu." >&2
exit 1
fi
if [[ "$MODE" == "import" && ( -z "$IMPORT_ADDRESS" || -z "$IMPORT_ID" ) ]]; then
echo "::error ::import mode requires import_address and import_id." >&2
exit 2
fi
- name: Configure Age
if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.mode == 'plan' || github.event.inputs.mode == 'apply' || github.event.inputs.mode == 'import') }}
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Configure Grafana Provider Auth
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.stack == 'grafana' && (github.event.inputs.mode == 'plan' || github.event.inputs.mode == 'apply' || github.event.inputs.mode == 'import') }}
shell: bash
run: |
set -euo pipefail
: "${AGE_IDENTITY_PATH:?AGE_IDENTITY_PATH is required}"
agenix="$(nix build --no-link .#agenix --print-out-paths | tail -n1)/bin/agenix"
password="$("$agenix" --identity "$AGE_IDENTITY_PATH" --decrypt secrets/infra/grafana-admin-password.age | tr -d '\r')"
echo "::add-mask::${password}"
{
echo "TF_VAR_grafana_auth<<EOF"
printf 'admin:%s\n' "$password"
echo "EOF"
} >> "$GITHUB_ENV"
- name: Configure OpenTofu Backend
shell: bash
run: |
set -euo pipefail
stack_dir="infra/${STACK}"
rm -f "${stack_dir}/backend.hcl"
if [[ -n "$OPENTOFU_STATE_BUCKET" ]]; then
{
printf 'bucket = "%s"\n' "$OPENTOFU_STATE_BUCKET"
printf 'key = "%s"\n' "${STACK}/${STACK}.tfstate"
printf 'region = "%s"\n' "$OPENTOFU_STATE_REGION"
printf 'encrypt = true\n'
if [[ -n "$OPENTOFU_STATE_DYNAMODB_TABLE" ]]; then
printf 'dynamodb_table = "%s"\n' "$OPENTOFU_STATE_DYNAMODB_TABLE"
fi
} > "${stack_dir}/backend.hcl"
elif [[ "$MODE" == "apply" || "$MODE" == "import" ]]; then
echo "::error ::OPENTOFU_STATE_BUCKET is required for apply/import." >&2
exit 1
fi
- name: Authenticate Google WIF
if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.stack == 'identity' || github.event.inputs.stack == 'releases') && (github.event.inputs.mode == 'plan' || github.event.inputs.mode == 'apply' || github.event.inputs.mode == 'import') }}
shell: bash
run: |
set -euo pipefail
nix develop .#ci -c Scripts/ci/google-wif-auth.sh
- name: Run OpenTofu
shell: bash
run: |
set -euo pipefail
stack_script="Scripts/${STACK}-tofu.sh"
case "$MODE" in
fmt)
nix develop .#ci -c tofu -chdir="infra/${STACK}" fmt -check
;;
validate)
"$stack_script" init -backend=false
"$stack_script" validate
;;
plan)
if [[ -f "infra/${STACK}/backend.hcl" ]]; then
"$stack_script" init -backend-config=backend.hcl
else
"$stack_script" init -backend=false
fi
"$stack_script" plan
;;
apply)
"$stack_script" init -backend-config=backend.hcl
"$stack_script" apply -auto-approve
;;
import)
"$stack_script" init -backend-config=backend.hcl
"$stack_script" import "$IMPORT_ADDRESS" "$IMPORT_ID"
;;
esac

View file

@ -1,38 +0,0 @@
name: Lint Governance
on:
push:
branches:
- main
pull_request:
branches:
- "**"
workflow_dispatch:
jobs:
governance:
name: BEP Metadata
runs-on: [self-hosted, linux, x86_64, burrow-forge]
steps:
- name: Checkout
shell: bash
run: |
set -euo pipefail
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
if [ ! -d .git ]; then
git init .
fi
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${repo_url}"
else
git remote add origin "${repo_url}"
fi
git fetch --force --tags origin "${GITHUB_SHA}"
git checkout --force --detach FETCH_HEAD
git clean -ffdqx
- name: Validate BEP metadata
shell: bash
run: |
set -euo pipefail
python3 Scripts/check-bep-metadata.py

View file

@ -1,63 +0,0 @@
name: "Cache: Publish Nix"
run-name: "Publish Nix Cache (${{ github.ref_name }})"
on:
push:
branches:
- main
paths:
- ".forgejo/workflows/publish-nix-cache.yml"
- "Cargo.lock"
- "Cargo.toml"
- "flake.lock"
- "flake.nix"
- "crates/**"
- "services/forgejo-nsc/**"
- "nixos/**"
- "Scripts/ci/ensure-nix.sh"
- "Scripts/ci/publish-nix-cache.sh"
workflow_dispatch:
inputs:
attrs:
description: Space-separated flake attrs to build and push
required: false
default: ".#burrow .#forgejo-nsc-dispatcher .#forgejo-nsc-autoscaler"
required:
description: Fail when the cache push token is missing
required: false
default: "false"
permissions:
contents: read
concurrency:
group: publish-nix-cache-${{ github.ref }}
cancel-in-progress: true
jobs:
publish:
name: Publish Nix Cache
runs-on: namespace-profile-linux-medium
timeout-minutes: 45
env:
BURROW_NIX_CACHE_SERVER_NAME: ${{ vars.BURROW_NIX_CACHE_SERVER_NAME || 'burrow' }}
BURROW_NIX_CACHE_SERVER_URL: ${{ vars.BURROW_NIX_CACHE_SERVER_URL || 'https://nix.burrow.net' }}
BURROW_NIX_CACHE_NAME: ${{ vars.BURROW_NIX_CACHE_NAME || 'burrow' }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
BURROW_NIX_CACHE_PUSH_TOKEN: ${{ secrets.BURROW_NIX_CACHE_PUSH_TOKEN }}
BURROW_NIX_CACHE_ATTRS: ${{ github.event.inputs.attrs || '.#burrow .#forgejo-nsc-dispatcher .#forgejo-nsc-autoscaler' }}
BURROW_NIX_CACHE_REQUIRED: ${{ github.event.inputs.required || 'false' }}
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Publish Cache Paths
shell: bash
run: nix develop .#ci -c Scripts/ci/publish-nix-cache.sh

View file

@ -1,148 +0,0 @@
name: "Publish: Package Repositories"
run-name: "Publish Package Repositories (${{ github.event.inputs.channel || 'stable' }})"
on:
workflow_dispatch:
inputs:
build_number:
description: Build number or release label for package artifact paths
required: false
default: ""
channel:
description: Native package repository channel
required: true
default: stable
type: choice
options:
- stable
- beta
- nightly
publish_gcs:
description: Publish repository tree to configured package storage targets
required: false
default: "true"
permissions:
contents: read
concurrency:
group: publish-package-repositories-${{ github.ref }}-${{ github.event.inputs.channel || 'stable' }}
cancel-in-progress: false
jobs:
publish:
name: Build Native Repositories
runs-on: [self-hosted, linux, x86_64, burrow-forge]
timeout-minutes: 45
env:
BUILD_NUMBER: ${{ github.event.inputs.build_number || github.sha }}
BURROW_PACKAGE_CHANNEL: ${{ github.event.inputs.channel || 'stable' }}
BURROW_KMS_LOCATION: ${{ vars.BURROW_KMS_LOCATION || 'global' }}
BURROW_KMS_KEYRING: ${{ vars.BURROW_KMS_KEYRING || 'burrow-identity' }}
BURROW_KMS_VERSION: ${{ vars.BURROW_KMS_VERSION || '1' }}
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_PACKAGE_GCS_BUCKET: ${{ vars.BURROW_PACKAGE_GCS_BUCKET || 'burrow-net-packages' }}
BURROW_PACKAGE_GCS_PREFIX: ${{ vars.BURROW_PACKAGE_GCS_PREFIX || '' }}
BURROW_PACKAGE_GCS_BACKUP: ${{ vars.BURROW_PACKAGE_GCS_BACKUP || 'true' }}
BURROW_PACKAGE_GARAGE_BUCKET: ${{ vars.BURROW_PACKAGE_GARAGE_BUCKET || 'burrow-packages' }}
BURROW_PACKAGE_GARAGE_PREFIX: ${{ vars.BURROW_PACKAGE_GARAGE_PREFIX || vars.BURROW_PACKAGE_GCS_PREFIX || '' }}
BURROW_PACKAGE_REQUIRE_GARAGE: ${{ vars.BURROW_PACKAGE_REQUIRE_GARAGE || 'true' }}
BURROW_GARAGE_ENDPOINT: ${{ vars.BURROW_GARAGE_ENDPOINT || 'https://objects.burrow.net' }}
BURROW_GARAGE_REGION: ${{ vars.BURROW_GARAGE_REGION || 'garage' }}
AWS_ACCESS_KEY_ID: ${{ secrets.BURROW_GARAGE_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.BURROW_GARAGE_SECRET_ACCESS_KEY }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
steps:
- name: Checkout
shell: bash
run: |
set -euo pipefail
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
if [ ! -d .git ]; then
git init .
fi
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${repo_url}"
else
git remote add origin "${repo_url}"
fi
git fetch --force --tags origin "${GITHUB_SHA}"
git checkout --force --detach FETCH_HEAD
git clean -ffdqx
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Build Linux Packages
shell: bash
run: |
set -euo pipefail
export BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER"
nix develop .#ci -c Scripts/ci/package-release-artifacts.sh linux
- name: Authenticate Google WIF
shell: bash
run: |
set -euo pipefail
nix develop .#ci -c Scripts/ci/google-wif-auth.sh
- name: Export KMS Public Keys
shell: bash
run: |
set -euo pipefail
mkdir -p "$PWD/.kms"
export BURROW_APT_REPO_KMS_KEY="${BURROW_APT_REPO_KMS_KEY:-package-apt-repository}"
export BURROW_RPM_REPO_KMS_KEY="${BURROW_RPM_REPO_KMS_KEY:-package-rpm-repository}"
export BURROW_PACMAN_REPO_KMS_KEY="${BURROW_PACMAN_REPO_KMS_KEY:-package-pacman-repository}"
for item in \
"APT:${BURROW_APT_REPO_KMS_KEY}:apt" \
"RPM:${BURROW_RPM_REPO_KMS_KEY}:rpm" \
"PACMAN:${BURROW_PACMAN_REPO_KMS_KEY}:pacman"
do
IFS=: read -r env_prefix key_name file_prefix <<< "$item"
pem="$PWD/.kms/${file_prefix}.pem"
nix develop .#ci -c gcloud kms keys versions get-public-key "$BURROW_KMS_VERSION" \
--location "$BURROW_KMS_LOCATION" \
--keyring "$BURROW_KMS_KEYRING" \
--key "$key_name" \
--project "$GOOGLE_CLOUD_PROJECT" \
--output-file "$pem"
echo "BURROW_${env_prefix}_REPO_KMS_PUBLIC_KEY_PEM=$pem" >> "$GITHUB_ENV"
echo "BURROW_${env_prefix}_REPO_KMS_KEY=$key_name" >> "$GITHUB_ENV"
done
- name: Build Repository Metadata
shell: bash
run: |
set -euo pipefail
export BURROW_PACKAGE_INPUT_DIR="$PWD/dist/builds/$BUILD_NUMBER/linux"
export BURROW_PACKAGE_REPOSITORY_DIR="$PWD/dist/builds/$BUILD_NUMBER/repositories"
nix develop .#ci -c Scripts/package/build-repositories.sh all
- name: Upload Repository Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-package-repositories-${{ github.event.inputs.channel || 'stable' }}-${{ github.event.inputs.build_number || github.sha }}
path: dist/builds/${{ github.event.inputs.build_number || github.sha }}/repositories/**
if-no-files-found: error
- name: Publish Package Repository To Storage
if: ${{ github.event.inputs.publish_gcs == 'true' }}
shell: bash
run: |
set -euo pipefail
export BURROW_PACKAGE_REPOSITORY_DIR="$PWD/dist/builds/$BUILD_NUMBER/repositories"
nix develop .#ci -c Scripts/ci/upload-package-storage.sh

View file

@ -1,385 +0,0 @@
name: "Publish: Release Uploads"
run-name: "Publish: Release Uploads (Build ${{ inputs.build_number }})"
on:
workflow_dispatch:
inputs:
build_number:
description: Build number to publish
required: true
upload_app_store:
description: Upload iOS/macOS/visionOS artifacts to App Store Connect
required: false
default: "true"
distribute_testflight:
description: Distribute an already-uploaded iOS build to TestFlight
required: false
default: "false"
upload_sparkle:
description: Publish Sparkle artifacts
required: false
default: "true"
upload_microsoft_store:
description: Upload Windows package to Microsoft Partner Center beta lane
required: false
default: "false"
sparkle_channel:
description: Sparkle channel, defaults to build-<number>
required: false
default: ""
sparkle_point_main:
description: Point Sparkle main appcast/default channel to selected channel
required: false
default: "true"
testflight_distribute_external:
description: Distribute to external TestFlight groups
required: false
default: "false"
testflight_groups:
description: Optional comma-separated TestFlight groups for external distribution
required: false
default: ""
ios_uses_non_exempt_encryption:
description: Set true only when the iOS build uses non-exempt encryption
required: false
default: "false"
permissions:
contents: read
concurrency:
group: burrow-publish-store-uploads-${{ inputs.build_number }}
cancel-in-progress: true
env:
BURROW_RELEASE_GCS_BUCKET: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }}
BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }}
BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }}
BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }}
BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }}
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }}
BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }}
BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }}
BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }}
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }}
BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }}
BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }}
BURROW_DEVELOPER_ID_CERTIFICATE_ID: ${{ vars.BURROW_DEVELOPER_ID_CERTIFICATE_ID || '9JKN6HXBHC' }}
BURROW_APPLE_SIGNING_MODE: ${{ vars.BURROW_APPLE_SIGNING_MODE || 'google-kms-staged' }}
BURROW_NOTARIZE_MACOS: ${{ vars.BURROW_NOTARIZE_MACOS || 'true' }}
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'true' }}
BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }}
BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }}
BURROW_KMS_LOCATION: ${{ vars.BURROW_KMS_LOCATION || 'global' }}
BURROW_KMS_KEYRING: ${{ vars.BURROW_KMS_KEYRING || 'burrow-identity' }}
TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS: ${{ vars.TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS || '7200' }}
jobs:
sign-apple-kms:
name: Sign (Apple KMS)
if: ${{ inputs.upload_app_store == 'true' || inputs.upload_sparkle == 'true' }}
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Validate App Store Credentials
shell: bash
run: Scripts/ci/check-release-config.sh app-store
- name: Download Staged Apple Artifacts
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
run: |
set -euo pipefail
nix develop .#ci -c Scripts/ci/google-wif-auth.sh
mkdir -p "dist/builds/${BUILD_NUMBER}/apple"
nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/apple" "dist/builds/${BUILD_NUMBER}/apple"
find "dist/builds/${BUILD_NUMBER}/apple" -type f -print | sort
- name: Sync Apple Provisioning Profiles
shell: bash
run: nix develop .#ci -c Scripts/ci/sync-apple-provisioning-profiles.sh all
- name: Sign Apple Artifacts With KMS
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
SPARKLE_CHANNEL_INPUT: ${{ inputs.sparkle_channel }}
run: |
set -euo pipefail
channel="${SPARKLE_CHANNEL_INPUT:-}"
if [[ -z "$channel" ]]; then
channel="build-${BUILD_NUMBER}"
fi
SPARKLE_CHANNEL="$channel" nix develop .#ci -c Scripts/apple/sign-release-artifacts-kms.sh all
- name: Upload Signed Apple Artifacts
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
run: nix develop .#ci -c Scripts/ci/upload-release-storage.sh
upload-app-store:
name: Upload (App Store Connect)
needs: sign-apple-kms
if: ${{ inputs.upload_app_store == 'true' }}
runs-on: namespace-profile-macos-large
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Validate App Store Credentials
shell: bash
run: Scripts/ci/check-release-config.sh app-store
- name: Download Apple Artifacts From GCS
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
run: |
set -euo pipefail
nix develop .#ci -c Scripts/ci/google-wif-auth.sh
mkdir -p publish/apple
nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/apple" publish/apple
find publish/apple -type f -print | sort
- name: Upload Apple Packages
shell: bash
run: |
set -euo pipefail
Scripts/ci/check-release-config.sh app-store
key_dir="${RUNNER_TEMP:-/tmp}/burrow-asc"
altool_key_dir="${HOME}/.appstoreconnect/private_keys"
mkdir -p "$key_dir" "$altool_key_dir"
key_path="$key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then
cp "$ASC_API_KEY_PATH" "$key_path"
else
printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_path"
fi
cp "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
chmod 600 "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8"
shopt -s nullglob
artifacts=()
for artifact in publish/apple/*.ipa publish/apple/*.pkg; do
[[ "$artifact" == *.unsigned.ipa ]] && continue
artifacts+=("$artifact")
done
if (( ${#artifacts[@]} == 0 )); then
echo "::error ::No Apple upload artifacts found for build ${{ inputs.build_number }}."
exit 1
fi
for artifact in "${artifacts[@]}"; do
upload_type="ios"
if [[ "$artifact" == *.pkg ]]; then
upload_type="macos"
fi
upload_log="${RUNNER_TEMP:-/tmp}/burrow-altool-${upload_type}-$(basename "$artifact").log"
set +e
xcrun altool --upload-app \
--type "$upload_type" \
--file "$artifact" \
--apiKey "$ASC_API_KEY_ID" \
--apiIssuer "$ASC_API_ISSUER_ID" 2>&1 | tee "$upload_log"
altool_status=${PIPESTATUS[0]}
set -e
if (( altool_status != 0 )) || grep -Eq 'UPLOAD FAILED|Validation failed|ERROR:' "$upload_log"; then
echo "::error ::altool reported upload validation failure for $artifact" >&2
exit 1
fi
done
release-ios-testflight:
name: Release (TestFlight iOS Testers)
needs: upload-app-store
if: ${{ inputs.upload_app_store == 'true' }}
runs-on: namespace-profile-linux-testflight
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Validate App Store Credentials
shell: bash
run: Scripts/ci/check-release-config.sh app-store
- name: Distribute iOS Build To TestFlight
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ inputs.testflight_groups || '' }}
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ inputs.ios_uses_non_exempt_encryption || 'false' }}
run: Scripts/ci/distribute-testflight.sh
distribute-ios-testflight:
name: Distribute (TestFlight iOS Testers)
if: ${{ inputs.upload_app_store != 'true' && inputs.distribute_testflight == 'true' }}
runs-on: namespace-profile-linux-testflight
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Configure Age
shell: bash
env:
AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }}
run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV"
- name: Decrypt Release Secrets
uses: ./.forgejo/actions/decrypt-release-secrets
- name: Validate App Store Credentials
shell: bash
run: Scripts/ci/check-release-config.sh app-store
- name: Distribute iOS Build To TestFlight
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ inputs.testflight_groups || '' }}
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ inputs.ios_uses_non_exempt_encryption || 'false' }}
run: Scripts/ci/distribute-testflight.sh
upload-sparkle:
name: Upload (Sparkle)
needs: sign-apple-kms
if: ${{ inputs.upload_sparkle == 'true' }}
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Resolve Sparkle Channel
id: channel
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
run: |
set -euo pipefail
channel="${{ inputs.sparkle_channel }}"
if [[ -z "$channel" ]]; then
channel="build-${BUILD_NUMBER}"
fi
echo "channel=$channel" >> "$GITHUB_OUTPUT"
- name: Ensure Nix
shell: bash
run: bash Scripts/ci/ensure-nix.sh
- name: Publish Sparkle Channel
shell: bash
env:
BUILD_NUMBER: ${{ inputs.build_number }}
CHANNEL: ${{ steps.channel.outputs.channel }}
SPARKLE_POINT_MAIN: ${{ inputs.sparkle_point_main }}
run: |
set -euo pipefail
nix develop .#ci -c Scripts/ci/google-wif-auth.sh
mkdir -p "publish/sparkle/${CHANNEL}"
nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/sparkle/${CHANNEL}" "publish/sparkle/${CHANNEL}" || true
if [[ ! -f "publish/sparkle/${CHANNEL}/appcast.xml" ]]; then
echo "::warning ::No Sparkle appcast staged for build ${BUILD_NUMBER}; skipping Sparkle publish."
find "publish/sparkle/${CHANNEL}" -maxdepth 2 -type f -print || true
exit 0
fi
nix develop .#ci -c gcloud storage rsync --recursive "publish/sparkle/${CHANNEL}" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/${CHANNEL}"
if [[ "$SPARKLE_POINT_MAIN" == "true" ]]; then
printf '%s\n' "$CHANNEL" > main-channel.txt
nix develop .#ci -c gcloud storage cp main-channel.txt "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/main-channel.txt"
nix develop .#ci -c gcloud storage cp "publish/sparkle/${CHANNEL}/appcast.xml" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/appcast.xml"
nix develop .#ci -c gcloud storage rsync --recursive "publish/sparkle/${CHANNEL}" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/default"
fi
upload-microsoft-store:
name: Upload (Microsoft Store Beta)
if: ${{ inputs.upload_microsoft_store == 'true' }}
runs-on: namespace-profile-windows-large
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Validate Microsoft Store Credentials
shell: pwsh
env:
MICROSOFT_TENANT_ID: ${{ secrets.MICROSOFT_TENANT_ID }}
MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }}
MICROSOFT_CLIENT_SECRET: ${{ secrets.MICROSOFT_CLIENT_SECRET }}
run: |
foreach ($name in "MICROSOFT_TENANT_ID","MICROSOFT_CLIENT_ID","MICROSOFT_CLIENT_SECRET") {
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) {
throw "missing required environment variable: $name"
}
}
- name: Stop Until Store Package Exists
shell: pwsh
run: |
Write-Host "::warning ::Burrow only builds a Windows Rust stub today; Microsoft Store package upload is wired for credentials but waits on MSIX packaging."

View file

@ -1,92 +0,0 @@
name: "Release: If Needed"
run-name: "Release: If Needed (${{ github.ref_name }})"
on:
push:
branches:
- main
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
inputs:
upload_app_store:
description: Trigger downstream App Store Connect upload workflow
required: false
default: "true"
upload_sparkle:
description: Trigger downstream Sparkle publish workflow
required: false
default: "true"
upload_microsoft_store:
description: Trigger downstream Microsoft Store beta upload workflow
required: false
default: "false"
sparkle_point_main:
description: Update Sparkle main channel pointers
required: false
default: "true"
testflight_distribute_external:
description: Distribute to external TestFlight groups
required: false
default: "false"
testflight_groups:
description: Optional comma-separated TestFlight groups for external distribution
required: false
default: ""
ios_uses_non_exempt_encryption:
description: Set true only when the iOS build uses non-exempt encryption
required: false
default: "false"
permissions:
contents: read
actions: write
concurrency:
group: burrow-release-if-needed-main
cancel-in-progress: true
env:
GITHUB_TOKEN: ${{ github.token }}
BURROW_VERSION_REQUIRE_REMOTE: "1"
jobs:
check:
name: Check (Release Needed)
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Fetch Build Tags
shell: bash
run: git fetch --force --prune origin "+refs/tags/builds/*:refs/tags/builds/*"
- name: Dispatch Release Build
shell: bash
env:
UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'true' }}
UPLOAD_SPARKLE: ${{ github.event.inputs.upload_sparkle || 'true' }}
UPLOAD_MICROSOFT_STORE: ${{ github.event.inputs.upload_microsoft_store || 'false' }}
SPARKLE_POINT_MAIN: ${{ github.event.inputs.sparkle_point_main || 'true' }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }}
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }}
run: |
set -euo pipefail
status="$(Scripts/version.sh status)"
if [[ "$status" == "clean" ]]; then
echo "No unreleased changes."
exit 0
fi
inputs="$(python3 -c 'import json, os; print(json.dumps({"upload_app_store":os.environ["UPLOAD_APP_STORE"],"upload_sparkle":os.environ["UPLOAD_SPARKLE"],"upload_microsoft_store":os.environ["UPLOAD_MICROSOFT_STORE"],"sparkle_point_main":os.environ["SPARKLE_POINT_MAIN"],"testflight_distribute_external":os.environ["TESTFLIGHT_DISTRIBUTE_EXTERNAL"],"testflight_groups":os.environ["TESTFLIGHT_GROUPS"],"ios_uses_non_exempt_encryption":os.environ["IOS_USES_NON_EXEMPT_ENCRYPTION"]}))')"
curl -fsS \
-X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"ref\":\"main\",\"inputs\":${inputs}}" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/workflows/build-release.yml/dispatches"
echo "Dispatched build-release.yml (status=${status})."

View file

@ -1,77 +0,0 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }}
BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }}
jobs:
release:
name: Release Build
runs-on: namespace-profile-linux-medium
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
token: ${{ github.token }}
fetch-depth: 0
- name: Bootstrap Nix
shell: bash
run: |
set -euo pipefail
chmod +x Scripts/ci/ensure-nix.sh
Scripts/ci/ensure-nix.sh
- name: Build release artifacts
shell: bash
env:
RELEASE_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
ref="${RELEASE_REF:-manual-${GITHUB_SHA::7}}"
export RELEASE_REF="${ref}"
chmod +x Scripts/ci/build-release-artifacts.sh
nix develop .#ci -c Scripts/ci/build-release-artifacts.sh
- name: Flatten release assets
shell: bash
env:
RELEASE_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
ref="${RELEASE_REF:-manual-${GITHUB_SHA::7}}"
mkdir -p dist/release-assets
find "dist/builds/${ref}" -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.sha256' -o -name 'README.txt' \) -exec cp {} dist/release-assets/ \;
rm -rf dist/builds
cp dist/release-assets/* dist/
find dist -maxdepth 1 -type f -print | sort
- name: Upload release artifacts
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: burrow-release-${{ github.ref_name }}
path: dist/*
if-no-files-found: error
- name: Publish Forgejo release
if: startsWith(github.ref, 'refs/tags/')
shell: bash
env:
RELEASE_TAG: ${{ github.ref_name }}
API_URL: ${{ github.api_url }}
REPOSITORY: ${{ github.repository }}
TOKEN: ${{ github.token }}
run: |
set -euo pipefail
chmod +x Scripts/ci/publish-forgejo-release.sh
nix develop .#ci -c Scripts/ci/publish-forgejo-release.sh

View file

@ -26,7 +26,7 @@ runs:
run: | run: |
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
xcodebuild clean archive \ xcodebuild archive \
-allowProvisioningUpdates \ -allowProvisioningUpdates \
-allowProvisioningDeviceRegistration \ -allowProvisioningDeviceRegistration \
-skipPackagePluginValidation \ -skipPackagePluginValidation \
@ -35,9 +35,12 @@ runs:
-authenticationKeyID ${{ inputs.app-store-key-id }} \ -authenticationKeyID ${{ inputs.app-store-key-id }} \
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \ -authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \ -authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
-onlyUsePackageVersionsFromResolvedFile \
-scheme '${{ inputs.scheme }}' \ -scheme '${{ inputs.scheme }}' \
-destination '${{ inputs.destination }}' \ -destination '${{ inputs.destination }}' \
-archivePath '${{ inputs.archive-path }}' \ -archivePath '${{ inputs.archive-path }}' \
-resultBundlePath BuildResults.xcresult -resultBundlePath BuildResults.xcresult
./Tools/xcresulttool-github BuildResults.xcresult
rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8

View file

@ -18,7 +18,7 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- name: Xcode Cache - name: Cache Swift Packages
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: | path: |
@ -27,9 +27,7 @@ runs:
Apple/DerivedData Apple/DerivedData
key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }} key: ${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ inputs.scheme }}-${{ hashFiles('**/Package.resolved') }}
${{ runner.os }}-${{ inputs.scheme }}- ${{ runner.os }}-${{ inputs.scheme }}-
${{ runner.os }}-
- name: Build - name: Build
shell: bash shell: bash
working-directory: Apple working-directory: Apple

View file

@ -1,30 +0,0 @@
name: Download Provisioning Profiles
inputs:
app-store-key:
description: App Store key in PEM PKCS#8 format
required: true
app-store-key-id:
description: App Store key ID
required: true
app-store-key-issuer-id:
description: App Store key issuer ID
required: true
runs:
using: composite
steps:
- shell: bash
env:
FASTLANE_OPT_OUT_USAGE: 'YES'
run: |
APP_STORE_KEY=$(echo "${{ inputs.app-store-key }}" | jq -sR .)
cat << EOF > api-key.json
{
"key_id": "${{ inputs.app-store-key-id }}",
"issuer_id": "${{ inputs.app-store-key-issuer-id }}",
"key": $APP_STORE_KEY
}
EOF
fastlane sigh download_all --api_key_path api-key.json
rm -rf api-key.json

View file

@ -1,4 +1,4 @@
name: Export name: Notarize
inputs: inputs:
app-store-key: app-store-key:
description: App Store key in PEM PKCS#8 format description: App Store key in PEM PKCS#8 format
@ -12,8 +12,11 @@ inputs:
archive-path: archive-path:
description: Xcode archive path description: Xcode archive path
required: true required: true
export-options: destination:
description: The export options in JSON format description: The Xcode export destination. This can either be "export" or "upload"
required: true
method:
description: The Xcode export method. This can be one of app-store, validation, ad-hoc, package, enterprise, development, developer-id, or mac-application.
required: true required: true
export-path: export-path:
description: The path to export the archive to description: The path to export the archive to
@ -21,12 +24,14 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- shell: bash - id: notarize
shell: bash
working-directory: Apple working-directory: Apple
run: | run: |
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
echo '${{ inputs.export-options }}' | plutil -convert xml1 -o ExportOptions.plist - echo '{"destination":"${{ inputs.destination }}","method":"${{ inputs.method }}"}' \
| plutil -convert xml1 -o ExportOptions.plist -
xcodebuild \ xcodebuild \
-exportArchive \ -exportArchive \

View file

@ -9,17 +9,49 @@ inputs:
app-store-key-issuer-id: app-store-key-issuer-id:
description: App Store key issuer ID description: App Store key issuer ID
required: true required: true
archive-path:
description: Xcode archive path
required: true
outputs:
notarized-app:
description: The compressed and notarized app
value: ${{ steps.notarize.outputs.notarized-app }}
runs: runs:
using: composite using: composite
steps: steps:
- id: notarize - id: notarize
shell: bash shell: bash
working-directory: Apple
run: | run: |
echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8 echo "${{ inputs.app-store-key }}" > AuthKey_${{ inputs.app-store-key-id }}.p8
ditto -c -k --keepParent Release/Burrow.app Upload.zip echo '{"destination":"upload","method":"developer-id"}' \
xcrun notarytool submit --wait --issuer ${{ inputs.app-store-key-issuer-id }} --key-id ${{ inputs.app-store-key-id }} --key "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" Upload.zip | plutil -convert xml1 -o ExportOptions.plist -
xcrun stapler staple Release/Burrow.app
rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 Release xcodebuild \
-exportArchive \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration \
-authenticationKeyID ${{ inputs.app-store-key-id }} \
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
-archivePath '${{ inputs.archive-path }}' \
-exportOptionsPlist ExportOptions.plist
until xcodebuild \
-exportNotarizedApp \
-allowProvisioningUpdates \
-allowProvisioningDeviceRegistration \
-authenticationKeyID ${{ inputs.app-store-key-id }} \
-authenticationKeyIssuerID ${{ inputs.app-store-key-issuer-id }} \
-authenticationKeyPath "${PWD}/AuthKey_${{ inputs.app-store-key-id }}.p8" \
-archivePath '${{ inputs.archive-path }}' \
-exportPath Release
do
echo "Failed to export app, trying again in 10s..."
sleep 10
done
tar --options xz:compression-level=9 -C Release -cJvf Wallet.txz ./
echo "notarized-app=Wallet.txz" >> $GITHUB_OUTPUT
rm -rf AuthKey_${{ inputs.app-store-key-id }}.p8 Release ExportOptions.plist

View file

@ -18,6 +18,9 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- shell: bash
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- shell: bash - shell: bash
working-directory: Apple working-directory: Apple
run: | run: |
@ -25,10 +28,10 @@ runs:
-scheme '${{ inputs.scheme }}' \ -scheme '${{ inputs.scheme }}' \
-destination '${{ inputs.destination }}' \ -destination '${{ inputs.destination }}' \
${{ inputs.test-plan && '-testPlan ' }}${{ inputs.test-plan }} \ ${{ inputs.test-plan && '-testPlan ' }}${{ inputs.test-plan }} \
-resultBundlePath "${{ inputs.artifact-prefix }}.xcresult" -resultBundlePath "${{ inputs.artifact-prefix }}-${{ steps.vars.outputs.sha_short }}.xcresult"
- uses: kishikawakatsumi/xcresulttool@v1 - uses: kishikawakatsumi/xcresulttool@v1
if: always() if: always()
with: with:
path: Apple/${{ inputs.artifact-prefix }}.xcresult path: Apple/${{ inputs.artifact-prefix }}-${{ steps.vars.outputs.sha_short }}.xcresult
title: ${{ inputs.check-name }} title: ${{ inputs.check-name }}
show-passed-tests: false show-passed-tests: false

View file

@ -1,14 +1,8 @@
name: Build AppImage name: Build AppImage
on: on:
push: push:
branches: branches: [main]
- main
pull_request: pull_request:
branches:
- "*"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
appimage: appimage:
name: Build AppImage name: Build AppImage
@ -23,7 +17,7 @@ jobs:
docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage . docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage .
docker rm temp docker rm temp
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: Upload to GitHub
with: with:
name: AppImage name: AppImage
path: Burrow-x86_64.AppImage path: Burrow-x86_64.AppImage

View file

@ -1,4 +1,4 @@
name: Build Apple Apps name: Apple Build
on: on:
push: push:
branches: branches:
@ -24,7 +24,7 @@ jobs:
rust-targets: rust-targets:
- aarch64-apple-ios - aarch64-apple-ios
- scheme: App - scheme: App
destination: platform=iOS Simulator,OS=18.0,name=iPhone 15 Pro destination: platform=iOS Simulator,OS=17.2,name=iPhone 15 Pro
platform: iOS Simulator platform: iOS Simulator
sdk-name: iphonesimulator sdk-name: iphonesimulator
rust-targets: rust-targets:
@ -38,8 +38,7 @@ jobs:
- x86_64-apple-darwin - x86_64-apple-darwin
- aarch64-apple-darwin - aarch64-apple-darwin
env: env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
PROTOC_PATH: /opt/homebrew/bin/protoc
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -54,11 +53,8 @@ jobs:
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: 1.85.0 toolchain: stable
targets: ${{ join(matrix.rust-targets, ', ') }} targets: ${{ join(matrix.rust-targets, ', ') }}
- name: Install Protobuf
shell: bash
run: brew install protobuf
- name: Build - name: Build
id: build id: build
uses: ./.github/actions/build-for-testing uses: ./.github/actions/build-for-testing
@ -68,7 +64,7 @@ jobs:
app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
- name: Run Unit Tests - name: Xcode Unit Test
if: ${{ matrix.xcode-unit-test != '' }} if: ${{ matrix.xcode-unit-test != '' }}
continue-on-error: true continue-on-error: true
uses: ./.github/actions/test-without-building uses: ./.github/actions/test-without-building
@ -78,7 +74,7 @@ jobs:
test-plan: ${{ matrix.xcode-unit-test }} test-plan: ${{ matrix.xcode-unit-test }}
artifact-prefix: unit-tests-${{ matrix.sdk-name }} artifact-prefix: unit-tests-${{ matrix.sdk-name }}
check-name: Xcode Unit Tests (${{ matrix.platform }}) check-name: Xcode Unit Tests (${{ matrix.platform }})
- name: Run UI Tests - name: Xcode UI Test
if: ${{ matrix.xcode-ui-test != '' }} if: ${{ matrix.xcode-ui-test != '' }}
continue-on-error: true continue-on-error: true
uses: ./.github/actions/test-without-building uses: ./.github/actions/test-without-building

View file

@ -6,9 +6,6 @@ on:
pull_request: pull_request:
branches: branches:
- "*" - "*"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
name: Build Docker Image name: Build Docker Image
@ -36,7 +33,6 @@ jobs:
images: ghcr.io/${{ github.repository }} images: ghcr.io/${{ github.repository }}
tags: | tags: |
type=sha type=sha
type=match,pattern=builds/(.*),group=1
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4

View file

@ -1,11 +1,16 @@
on: workflow_dispatch
name: Build RPM name: Build RPM
on:
push:
branches: [ "main" ]
pull_request:
branches:
- "*"
jobs: jobs:
build: build:
name: Build RPM name: Build RPM
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Install RPM - name: Install RPM
run: cargo install cargo-generate-rpm run: cargo install cargo-generate-rpm
@ -15,3 +20,4 @@ jobs:
strip -s target/release/burrow strip -s target/release/burrow
- name: Build RPM - name: Build RPM
run: cargo generate-rpm -p burrow run: cargo generate-rpm -p burrow

View file

@ -1,4 +1,4 @@
name: Build Rust Crate name: Rust Build
on: on:
push: push:
branches: branches:
@ -6,9 +6,6 @@ on:
pull_request: pull_request:
branches: branches:
- "*" - "*"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
name: Build Crate (${{ matrix.platform }}) name: Build Crate (${{ matrix.platform }})
@ -24,21 +21,15 @@ jobs:
- x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu
targets: targets:
- aarch64-unknown-linux-gnu - aarch64-unknown-linux-gnu
- os: macos-13 - os: macos-12
platform: macOS (Intel) platform: macOS
xcode: /Applications/Xcode_15.2.app
test-targets: test-targets:
- x86_64-apple-darwin - x86_64-apple-darwin
targets: targets:
- x86_64-apple-ios
- os: macos-14
platform: macOS
xcode: /Applications/Xcode_16.0.app
test-targets:
- aarch64-apple-darwin - aarch64-apple-darwin
targets:
- aarch64-apple-ios - aarch64-apple-ios
- aarch64-apple-ios-sim - aarch64-apple-ios-sim
- x86_64-apple-ios
- os: windows-2022 - os: windows-2022
platform: Windows platform: Windows
test-targets: test-targets:
@ -47,11 +38,10 @@ jobs:
- aarch64-pc-windows-msvc - aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
DEVELOPER_DIR: ${{ matrix.xcode }}/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
RUST_BACKTRACE: short RUST_BACKTRACE: short
PROTOC_VERSION: 3.25.1
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -64,25 +54,21 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y ${{ join(matrix.packages, ' ') }} sudo apt-get install -y ${{ join(matrix.packages, ' ') }}
- name: Configure LLVM - name: Install Windows Deps
if: matrix.os == 'windows-2022' if: matrix.os == 'windows-2022'
shell: bash shell: bash
run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH run: echo "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\Llvm\x64\bin" >> $GITHUB_PATH
- name: Install protoc
uses: taiki-e/install-action@v2
with:
tool: protoc@${{ env.PROTOC_VERSION }}
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: 1.85.0 toolchain: stable
components: rustfmt components: rustfmt
targets: ${{ join(matrix.targets, ', ') }} targets: ${{ join(matrix.targets, ', ') }}
- name: Setup Rust Cache - name: Setup Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Build - name: Build
shell: bash shell: bash
run: cargo build --locked --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }} run: cargo build --verbose --workspace --all-features --target ${{ join(matrix.targets, ' --target ') }} --target ${{ join(matrix.test-targets, ' --target ') }}
- name: Test - name: Test
shell: bash shell: bash
run: cargo test --locked --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }} run: cargo test --verbose --workspace --all-features --target ${{ join(matrix.test-targets, ' --target ') }}

View file

@ -8,14 +8,13 @@ jobs:
name: Git Lint name: Git Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v3
uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0 fetch-depth: 0
- name: Install - name: Install Gitlint
shell: bash shell: bash
run: python -m pip install gitlint run: python -m pip install gitlint
- name: Lint - name: Run Gitlint
shell: bash shell: bash
run: gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD" run: gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD"

View file

@ -1,23 +0,0 @@
name: Governance Lint
on:
pull_request:
branches:
- "*"
jobs:
governance:
name: BEP Metadata
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Validate BEP metadata
shell: bash
run: |
set -euo pipefail
python3 Scripts/check-bep-metadata.py

View file

@ -1,5 +1,8 @@
name: Swift Lint name: Swift Lint
on: on:
push:
branches:
- main
pull_request: pull_request:
branches: branches:
- "*" - "*"
@ -11,6 +14,8 @@ jobs:
image: ghcr.io/realm/swiftlint:latest image: ghcr.io/realm/swiftlint:latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Lint - name: Lint
run: swiftlint lint --strict --reporter github-actions-logging run: swiftlint lint --reporter github-actions-logging

View file

@ -1,120 +1,70 @@
name: Release (Apple) name: Build Apple Release
on: on:
release: pull_request:
types: branches:
- created - "*"
jobs: jobs:
build: build:
name: Build ${{ matrix.platform }} Release name: Build ${{ matrix.configuration['platform'] }} Release
runs-on: macos-14 runs-on: macos-14
permissions:
contents: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: configuration:
- platform: iOS - destination: generic/platform=iOS
platform: iOS
artifact-file: Apple/Release/Burrow.ipa
rust-targets: rust-targets:
- aarch64-apple-ios - aarch64-apple-ios
- platform: macOS - destination: generic/platform=macOS
platform: macOS
artifact-file: Burrow.app.txz
rust-targets: rust-targets:
- x86_64-apple-darwin - x86_64-apple-darwin
- aarch64-apple-darwin - aarch64-apple-darwin
env: env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
PROTOC_PATH: /opt/homebrew/bin/protoc
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 ssh-key: ${{ secrets.DEPLOY_KEY }}
submodules: recursive
- name: Import Certificate - name: Import Certificate
uses: ./.github/actions/import-cert uses: ./.github/actions/import-cert
with: with:
certificate: ${{ secrets.DEVELOPER_CERT }} certificate: ${{ secrets.DEVELOPER_CERT }}
password: ${{ secrets.DEVELOPER_CERT_PASSWORD }} password: ${{ secrets.DEVELOPER_CERT_PASSWORD }}
- name: Download Provisioning Profiles
uses: ./.github/actions/download-profiles
with:
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
- name: Install Provisioning Profiles
shell: bash
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles/
cp -f Apple/Profiles/* ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: 1.85.0
targets: ${{ join(matrix.rust-targets, ', ') }} targets: ${{ join(matrix.rust-targets, ', ') }}
- name: Install Protobuf
shell: bash
run: brew install protobuf
- name: Configure Version
id: version
shell: bash
run: echo "BUILD_NUMBER=$(Tools/version.sh)" >> $GITHUB_OUTPUT
- name: Archive - name: Archive
uses: ./.github/actions/archive uses: ./.github/actions/archive
with: with:
scheme: App scheme: App
destination: generic/platform=${{ matrix.platform }} destination: ${{ matrix.configuration['destination'] }}
app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive archive-path: Burrow.xcarchive
- name: Export - name: Upload
uses: ./.github/actions/export uses: ./.github/actions/export
with: with:
method: ${{ matrix.platform == 'macOS' && 'developer-id' || 'ad-hoc' }} method: app-store-connect
destination: export destination: upload
app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive archive-path: Burrow.xcarchive
export-options: |
{"teamID":"P6PV2R9443","destination":"export","method":"developer-id","provisioningProfiles":{"com.hackclub.burrow":"Burrow Developer ID","com.hackclub.burrow.network":"Burrow Network Developer ID"},"signingCertificate":"Developer ID Application","signingStyle":"manual"}
export-path: Release export-path: Release
- name: Notarize - name: Notarize
if: ${{ matrix.platform == 'macOS' }} if: ${{ matrix.configuration['platform'] == 'macOS' }}
uses: ./.github/actions/notarize uses: ./.github/actions/notarize
with:
app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
- name: Compress (iOS)
if: ${{ matrix.platform == 'iOS' }}
shell: bash
run: |
cp Apple/Release/Burrow.ipa Burrow.ipa
aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar
rm -rf Apple/Release
- name: Compress (macOS)
if: ${{ matrix.platform == 'macOS' }}
shell: bash
run: |
aa archive -a lzma -b 8m -d Apple/Release -subdir Burrow.app -o Burrow.app.aar
aa archive -a lzma -b 8m -d Apple -subdir Burrow.xcarchive -o Burrow-${{ matrix.platform }}.xcarchive.aar
rm -rf Apple/Release
- name: Upload to GitHub
uses: SierraSoftworks/gh-releases@v1.0.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
release_tag: ${{ github.ref_name }}
overwrite: 'true'
files: |
${{ matrix.platform == 'macOS' && 'Burrow.aap.aar' || 'Burrow.ipa' }}
Burrow-${{ matrix.platform }}.xcarchive.aar
- name: Upload to App Store Connect
if: ${{ matrix.platform == 'iOS' }}
uses: ./.github/actions/export
with: with:
app-store-key: ${{ secrets.APPSTORE_KEY }} app-store-key: ${{ secrets.APPSTORE_KEY }}
app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }} app-store-key-id: ${{ secrets.APPSTORE_KEY_ID }}
app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }} app-store-key-issuer-id: ${{ secrets.APPSTORE_KEY_ISSUER_ID }}
archive-path: Burrow.xcarchive archive-path: Burrow.xcarchive
export-options: | product-name: Burrow.app
{"method": "app-store", "destination": "upload"}
export-path: Release

View file

@ -1,23 +0,0 @@
name: Create Release If Needed
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * *'
concurrency:
group: ${{ github.workflow }}
jobs:
create:
name: Create Release If Needed
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: |
if [[ $(Tools/version.sh status) == "dirty" ]]; then
gh workflow run release-now.yml
fi

View file

@ -1,29 +0,0 @@
name: Release (Linux)
on:
release:
types:
- created
jobs:
appimage:
name: Build AppImage
runs-on: ubuntu-latest
container: docker
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build AppImage
run: |
docker build -t appimage-builder . -f burrow-gtk/build-aux/Dockerfile
docker create --name temp appimage-builder
docker cp temp:/app/burrow-gtk/build-appimage/Burrow-x86_64.AppImage .
docker rm temp
- name: Attach Artifacts
uses: SierraSoftworks/gh-releases@v1.0.7
with:
token: ${{ secrets.GITHUB_TOKEN }}
release_tag: ${{ github.ref_name }}
overwrite: "true"
files: |
Burrow-x86_64.AppImage

View file

@ -1,17 +0,0 @@
name: Create Release
on: workflow_dispatch
concurrency:
group: ${{ github.workflow }}
jobs:
create:
env:
GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- shell: bash
run: Tools/version.sh increment

24
.gitignore vendored
View file

@ -1,32 +1,8 @@
# Xcode # Xcode
xcuserdata xcuserdata
Apple/build/
.derived-data/
AuthKey_*.p8
burrow-certs-pass.txt
*.p12
# Swift
Apple/Package/.swiftpm/
# Rust # Rust
target/ target/
.env
# Bazel
bazel-*
.DS_STORE .DS_STORE
.idea/ .idea/
tmp/
.cache/
intake/
dist/
publish/
release/
ci-artifacts/
*.db
*.sqlite3
*.sock

View file

@ -30,6 +30,7 @@ opt_in_rules:
- function_default_parameter_at_end - function_default_parameter_at_end
- ibinspectable_in_extension - ibinspectable_in_extension
- identical_operands - identical_operands
- implicitly_unwrapped_optional
- indentation_width - indentation_width
- joined_default_parameter - joined_default_parameter
- last_where - last_where

20
.vscode/settings.json vendored
View file

@ -8,19 +8,11 @@
"editor.acceptSuggestionOnEnter": "on", "editor.acceptSuggestionOnEnter": "on",
"rust-analyzer.restartServerOnConfigChange": true, "rust-analyzer.restartServerOnConfigChange": true,
"rust-analyzer.cargo.features": "all", "rust-analyzer.cargo.features": "all",
"rust-analyzer.rustfmt.extraArgs": ["+nightly"], "rust-analyzer.rustfmt.extraArgs": [
"[rust]": { "+nightly"
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.linkedProjects": [
"./burrow/Cargo.toml"
], ],
"[yaml]": { "[rust]": {
"editor.insertSpaces": true, "editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.tabSize": 2, },
"editor.autoIndent": "advanced", "rust-analyzer.inlayHints.typeHints.enable": false
"diffEditor.ignoreTrimWhitespace": false,
"editor.formatOnSave": false
}
} }

View file

@ -1,14 +0,0 @@
# instructions for agents
1. Spell the project name as `Burrow` in user-facing copy and `burrow` in code, package, and protocol identifiers unless an existing integration requires a different literal.
2. Read [CONSTITUTION.md](CONSTITUTION.md) before changing Apple clients, the daemon, the control plane, forge infrastructure, identity, or security-sensitive code.
3. Anchor non-trivial changes in a Burrow Evolution Proposal (BEP) under [evolution/](evolution/README.md) so future contributors can inherit the rationale, safeguards, and rollout shape.
4. Before touching the Apple app, daemon IPC, or Tailnet flows, review:
- [evolution/proposals/BEP-0002-control-plane-bootstrap-and-local-auth.md](evolution/proposals/BEP-0002-control-plane-bootstrap-and-local-auth.md)
- [evolution/proposals/BEP-0003-connect-ip-and-negotiation-roadmap.md](evolution/proposals/BEP-0003-connect-ip-and-negotiation-roadmap.md)
- [evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md](evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md)
- [evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md](evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md)
5. Apple clients must talk only to the daemon over gRPC. Do not add direct HTTP, control-plane, or helper-process calls from Swift UI code.
6. Treat Tailnet as one protocol family. Tailscale-managed and self-hosted Headscale-style deployments differ by authority, policy, and auth details, not by a separate user-facing protocol surface.
7. Maintain canonical identity and operator metadata in [contributors.nix](contributors.nix). If Burrow forge, Authentik, Headscale, or admin/group mappings need to change, edit that registry first and derive runtime configuration from it.
8. When process or architecture is unclear, stop and draft or update a BEP instead of improvising durable behavior in code.

View file

@ -1,14 +0,0 @@
# Burrow Android
The Android app starts as a Kotlin shell linked to `crates/burrow-mobile-ffi`,
which exposes the cross-platform `burrow-core` crate through JNI.
The current stub proves the package shape and Rust core boundary. Full VPN
support must use Android `VpnService` and keep platform consent, Play Store
policy, and disclosure requirements in the release lane.
The Bazel entrypoint is:
```sh
bazel build //bazel/android:check_stub_stamp
```

View file

@ -1,21 +0,0 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "net.burrow.android"
compileSdk = 36
defaultConfig {
applicationId = "net.burrow.android"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
}
}
kotlin {
jvmToolchain(17)
}

View file

@ -1,18 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/BurrowTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,9 +0,0 @@
package net.burrow.android
object BurrowCore {
init {
System.loadLibrary("burrow_mobile_ffi")
}
external fun version(): String
}

View file

@ -1,32 +0,0 @@
package net.burrow.android
import android.app.Activity
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.TextView
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val versionText = runCatching { BurrowCore.version() }
.getOrElse { "burrow-core unavailable" }
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(40, 40, 40, 40)
}
root.addView(TextView(this).apply {
text = getString(R.string.app_name)
textSize = 24f
})
root.addView(TextView(this).apply {
text = versionText
textSize = 14f
})
setContentView(root)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View file

@ -1,3 +0,0 @@
<resources>
<string name="app_name">Burrow</string>
</resources>

View file

@ -1,8 +0,0 @@
<resources>
<style name="BurrowTheme" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">#ffffff</item>
<item name="android:statusBarColor">#ffffff</item>
</style>
</resources>

View file

@ -1,4 +0,0 @@
plugins {
id("com.android.application") version "8.10.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.21" apply false
}

View file

@ -1,18 +0,0 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "BurrowAndroid"
include(":app")

View file

@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:burrow.rs?mode=developer</string>
<string>webcredentials:burrow.rs?mode=developer</string>
</array>
<key>com.apple.developer.networking.networkextension</key> <key>com.apple.developer.networking.networkextension</key>
<array> <array>
<string>packet-tunnel-provider</string> <string>packet-tunnel-provider</string>

View file

@ -2,11 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:burrow.rs?mode=developer</string>
<string>webcredentials:burrow.rs?mode=developer</string>
</array>
<key>com.apple.developer.networking.networkextension</key> <key>com.apple.developer.networking.networkextension</key>
<array> <array>
<string>packet-tunnel-provider</string> <string>packet-tunnel-provider</string>

View file

@ -10,8 +10,6 @@ INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphone*] = YES
INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault INFOPLIST_KEY_UIStatusBarStyle[sdk=iphone*] = UIStatusBarStyleDefault
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone[sdk=iphone*] = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES[sdk=iphone*] = BurrowPanel02 BurrowPanel03 BurrowPanel04 BurrowPanel05 BurrowPanel06
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS[sdk=iphone*] = YES
TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2 TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2
EXCLUDED_SOURCE_FILE_NAMES = MainMenu.xib EXCLUDED_SOURCE_FILE_NAMES = MainMenu.xib
@ -20,10 +18,6 @@ INFOPLIST_KEY_LSUIElement[sdk=macosx*] = YES
INFOPLIST_KEY_NSMainNibFile[sdk=macosx*] = MainMenu INFOPLIST_KEY_NSMainNibFile[sdk=macosx*] = MainMenu
INFOPLIST_KEY_NSPrincipalClass[sdk=macosx*] = NSApplication INFOPLIST_KEY_NSPrincipalClass[sdk=macosx*] = NSApplication
INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*] = public.app-category.utilities INFOPLIST_KEY_LSApplicationCategoryType[sdk=macosx*] = public.app-category.utilities
BURROW_SPARKLE_FEED_URL = https:/$()/releases.burrow.net/sparkle/appcast.xml
BURROW_SPARKLE_PUBLIC_ED_KEY = uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=
INFOPLIST_KEY_SUFeedURL[sdk=macosx*] = $(BURROW_SPARKLE_FEED_URL)
INFOPLIST_KEY_SUPublicEDKey[sdk=macosx*] = $(BURROW_SPARKLE_PUBLIC_ED_KEY)
CODE_SIGN_ENTITLEMENTS = App/App-iOS.entitlements CODE_SIGN_ENTITLEMENTS = App/App-iOS.entitlements
CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = App/App-macOS.entitlements CODE_SIGN_ENTITLEMENTS[sdk=macosx*] = App/App-macOS.entitlements

View file

@ -1,19 +1,10 @@
#if os(macOS) #if os(macOS)
import AppKit import AppKit
import BurrowUI
import Sparkle
import SwiftUI import SwiftUI
@main
@MainActor @MainActor
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
private var windowController: NSWindowController?
private let updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
private let quitItem: NSMenuItem = { private let quitItem: NSMenuItem = {
let quitItem = NSMenuItem( let quitItem = NSMenuItem(
title: "Quit Burrow", title: "Quit Burrow",
@ -25,27 +16,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return quitItem return quitItem
}() }()
private lazy var openItem: NSMenuItem = {
let item = NSMenuItem(
title: "Open Burrow",
action: #selector(openWindow),
keyEquivalent: "o"
)
item.target = self
item.keyEquivalentModifierMask = .command
return item
}()
private lazy var checkForUpdatesItem: NSMenuItem = {
let item = NSMenuItem(
title: "Check for Updates...",
action: #selector(checkForUpdates(_:)),
keyEquivalent: ""
)
item.target = self
return item
}()
private let toggleItem: NSMenuItem = { private let toggleItem: NSMenuItem = {
let toggleView = NSHostingView(rootView: MenuItemToggleView()) let toggleView = NSHostingView(rootView: MenuItemToggleView())
toggleView.frame.size = CGSize(width: 300, height: 32) toggleView.frame.size = CGSize(width: 300, height: 32)
@ -60,8 +30,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let menu = NSMenu() let menu = NSMenu()
menu.items = [ menu.items = [
toggleItem, toggleItem,
openItem,
checkForUpdatesItem,
.separator(), .separator(),
quitItem quitItem
] ]
@ -72,7 +40,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let statusBar = NSStatusBar.system let statusBar = NSStatusBar.system
let statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) let statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem.button { if let button = statusItem.button {
button.image = NSImage(systemSymbolName: "pipe.and.drop.fill", accessibilityDescription: nil) button.image = NSImage(systemSymbolName: "network.badge.shield.half.filled", accessibilityDescription: nil)
} }
return statusItem return statusItem
}() }()
@ -80,33 +48,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
statusItem.menu = menu statusItem.menu = menu
} }
@objc
private func openWindow() {
if let window = windowController?.window {
window.makeKeyAndOrderFront(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
return
}
let contentView = BurrowView()
let hostingController = NSHostingController(rootView: contentView)
let window = NSWindow(contentViewController: hostingController)
window.title = "Burrow"
window.setContentSize(NSSize(width: 820, height: 720))
window.styleMask.insert([.titled, .closable, .miniaturizable, .resizable])
window.center()
let controller = NSWindowController(window: window)
controller.shouldCascadeWindows = true
controller.showWindow(nil)
windowController = controller
NSApplication.shared.activate(ignoringOtherApps: true)
}
@objc
private func checkForUpdates(_ sender: Any?) {
updaterController.checkForUpdates(sender)
}
} }
#endif #endif

View file

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,63 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0x31", "blue" : "0x50",
"green" : "0x7A", "green" : "0x37",
"red" : "0xB7" "red" : "0xEC"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "flag-standalone-wtransparent.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

View file

@ -1,46 +1,13 @@
#if !os(macOS)
import BurrowUI
import SwiftUI import SwiftUI
#if os(iOS)
import UIKit
#endif
#if !os(macOS)
@MainActor @MainActor
@main @main
struct BurrowApp: App { struct BurrowApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
#if os(iOS)
BurrowView(appIconManager: UIKitAppIconManager())
#else
BurrowView() BurrowView()
#endif
}
}
}
#if os(iOS)
@MainActor
private struct UIKitAppIconManager: AppIconManaging {
var supportsAlternateIcons: Bool {
UIApplication.shared.supportsAlternateIcons
}
var alternateIconName: String? {
UIApplication.shared.alternateIconName
}
func setAlternateIconName(_ iconName: String?) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
UIApplication.shared.setAlternateIconName(iconName) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
}
} }
} }
} }
#endif #endif
#endif

View file

@ -0,0 +1,26 @@
import SwiftUI
struct BurrowView: View {
var body: some View {
NavigationStack {
VStack {
NetworkCarouselView()
Spacer()
TunnelStatusView()
TunnelButton()
.padding(.bottom)
}
.padding()
.navigationTitle("Networks")
}
}
}
#if DEBUG
struct NetworkView_Previews: PreviewProvider {
static var previews: some View {
BurrowView()
.environment(\.tunnel, PreviewTunnel())
}
}
#endif

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23091" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23091"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
</dependencies> </dependencies>
<objects> <objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View file

@ -7,11 +7,11 @@
import SwiftUI import SwiftUI
public struct MenuItemToggleView: View { struct MenuItemToggleView: View {
@Environment(\.tunnel) @Environment(\.tunnel)
var tunnel: Tunnel var tunnel: Tunnel
public var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Burrow") Text("Burrow")
@ -30,13 +30,10 @@ public struct MenuItemToggleView: View {
.padding(10) .padding(10)
.frame(minWidth: 300, minHeight: 32, maxHeight: 32) .frame(minWidth: 300, minHeight: 32, maxHeight: 32)
} }
public init() {
}
} }
extension Tunnel { extension Tunnel {
@MainActor fileprivate var toggleDisabled: Bool { fileprivate var toggleDisabled: Bool {
switch status { switch status {
case .disconnected, .permissionRequired, .connected, .disconnecting: case .disconnected, .permissionRequired, .connected, .disconnecting:
false false
@ -45,7 +42,7 @@ extension Tunnel {
} }
} }
@MainActor var toggleIsOn: Binding<Bool> { var toggleIsOn: Binding<Bool> {
Binding { Binding {
switch status { switch status {
case .connecting, .reasserting, .connected: case .connecting, .reasserting, .connected:

View file

@ -1,6 +1,6 @@
import NetworkExtension import NetworkExtension
extension NEVPNManager: @unchecked @retroactive Sendable { extension NEVPNManager {
func remove() async throws { func remove() async throws {
_ = try await withUnsafeThrowingContinuation { continuation in _ = try await withUnsafeThrowingContinuation { continuation in
removeFromPreferences(completionHandler: completion(continuation)) removeFromPreferences(completionHandler: completion(continuation))
@ -14,7 +14,7 @@ extension NEVPNManager: @unchecked @retroactive Sendable {
} }
} }
extension NETunnelProviderManager: @unchecked @retroactive Sendable { extension NETunnelProviderManager {
class var managers: [NETunnelProviderManager] { class var managers: [NETunnelProviderManager] {
get async throws { get async throws {
try await withUnsafeThrowingContinuation { continuation in try await withUnsafeThrowingContinuation { continuation in
@ -34,7 +34,7 @@ private func completion(_ continuation: UnsafeContinuation<Void, Error>) -> (Err
} }
} }
private func completion<T: Sendable>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void { private func completion<T>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void {
return { value, error in return { value, error in
if let error { if let error {
continuation.resume(throwing: error) continuation.resume(throwing: error)

View file

@ -1,23 +1,22 @@
import BurrowCore import BurrowShared
import NetworkExtension import NetworkExtension
@Observable @Observable
public final class NetworkExtensionTunnel: Tunnel { class NetworkExtensionTunnel: Tunnel {
@MainActor public private(set) var status: TunnelStatus = .unknown @MainActor private(set) var status: TunnelStatus = .unknown
@MainActor private var error: NEVPNError? private var error: NEVPNError?
private let logger = Logger.logger(for: Tunnel.self) private let logger = Logger.logger(for: Tunnel.self)
private let bundleIdentifier: String private let bundleIdentifier: String
private let configurationChanged: Task<Void, Error> private var tasks: [Task<Void, Error>] = []
private let statusChanged: Task<Void, Error>
// Each manager corresponds to one entry in the Settings app. // Each manager corresponds to one entry in the Settings app.
// Our goal is to maintain a single manager, so we create one if none exist and delete any extra. // Our goal is to maintain a single manager, so we create one if none exist and delete any extra.
@MainActor private var managers: [NEVPNManager]? { private var managers: [NEVPNManager]? {
didSet { Task { await updateStatus() } } didSet { Task { await updateStatus() } }
} }
@MainActor private var currentStatus: TunnelStatus { private var currentStatus: TunnelStatus {
guard let managers = managers else { guard let managers = managers else {
guard let error = error else { guard let error = error else {
return .unknown return .unknown
@ -42,40 +41,35 @@ public final class NetworkExtensionTunnel: Tunnel {
return manager.connection.tunnelStatus return manager.connection.tunnelStatus
} }
public init(bundleIdentifier: String) { convenience init() {
self.init(Constants.networkExtensionBundleIdentifier)
}
init(_ bundleIdentifier: String) {
self.bundleIdentifier = bundleIdentifier self.bundleIdentifier = bundleIdentifier
let center = NotificationCenter.default let center = NotificationCenter.default
let tunnel: OSAllocatedUnfairLock<NetworkExtensionTunnel?> = .init(initialState: .none) let configurationChanged = Task { [weak self] in
configurationChanged = Task { for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) {
for try await _ in center.notifications(named: .NEVPNConfigurationChange) { await self?.update()
try Task.checkCancellation()
await tunnel.withLock { $0 }?.update()
} }
} }
statusChanged = Task { let statusChanged = Task { [weak self] in
for try await _ in center.notifications(named: .NEVPNStatusDidChange) { for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) {
try Task.checkCancellation() await self?.updateStatus()
await tunnel.withLock { $0 }?.updateStatus()
} }
} }
tunnel.withLock { $0 = self } tasks = [configurationChanged, statusChanged]
Task { await update() } Task { await update() }
} }
private func update() async { private func update() async {
do { do {
let result = try await NETunnelProviderManager.managers managers = try await NETunnelProviderManager.managers
await MainActor.run {
managers = result
status = currentStatus
}
await self.updateStatus() await self.updateStatus()
} catch let vpnError as NEVPNError { } catch let vpnError as NEVPNError {
await MainActor.run {
error = vpnError error = vpnError
}
} catch { } catch {
logger.error("Failed to update VPN configurations: \(error)") logger.error("Failed to update VPN configurations: \(error)")
} }
@ -88,7 +82,12 @@ public final class NetworkExtensionTunnel: Tunnel {
} }
func configure() async throws { func configure() async throws {
let managers = try await NETunnelProviderManager.managers if managers == nil {
await update()
}
guard let managers = managers else { return }
if managers.count > 1 { if managers.count > 1 {
try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in
for manager in managers.suffix(from: 1) { for manager in managers.suffix(from: 1) {
@ -105,15 +104,15 @@ public final class NetworkExtensionTunnel: Tunnel {
let proto = NETunnelProviderProtocol() let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = bundleIdentifier proto.providerBundleIdentifier = bundleIdentifier
proto.serverAddress = "burrow.rs" proto.serverAddress = "hackclub.com"
manager.protocolConfiguration = proto manager.protocolConfiguration = proto
try await manager.save() try await manager.save()
} }
public func start() { func start() {
guard let manager = managers?.first else { return }
Task { Task {
guard let manager = try await NETunnelProviderManager.managers.first else { return }
do { do {
if !manager.isEnabled { if !manager.isEnabled {
manager.isEnabled = true manager.isEnabled = true
@ -126,14 +125,12 @@ public final class NetworkExtensionTunnel: Tunnel {
} }
} }
public func stop() { func stop() {
Task { guard let manager = managers?.first else { return }
guard let manager = try await NETunnelProviderManager.managers.first else { return }
manager.connection.stopVPNTunnel() manager.connection.stopVPNTunnel()
} }
}
public func enable() { func enable() {
Task { Task {
do { do {
try await configure() try await configure()
@ -144,8 +141,7 @@ public final class NetworkExtensionTunnel: Tunnel {
} }
deinit { deinit {
configurationChanged.cancel() tasks.forEach { $0.cancel() }
statusChanged.cancel()
} }
} }

View file

@ -0,0 +1,88 @@
import SwiftUI
struct NetworkView<Content: View>: View {
var color: Color
var content: () -> Content
private var gradient: LinearGradient {
LinearGradient(
colors: [
color.opacity(0.8),
color
],
startPoint: .init(x: 0.2, y: 0),
endPoint: .init(x: 0.8, y: 1)
)
}
var body: some View {
content()
.frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(gradient)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.white)
)
)
.shadow(color: .black.opacity(0.1), radius: 3.0, x: 0, y: 2)
}
}
struct AddNetworkView: View {
var body: some View {
Text("Add Network")
.frame(maxWidth: .infinity, minHeight: 175, maxHeight: 175)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(style: .init(lineWidth: 2, dash: [6]))
)
}
}
extension NetworkView where Content == AnyView {
init(network: any Network) {
color = network.backgroundColor
content = { AnyView(network.label) }
}
}
struct NetworkCarouselView: View {
var networks: [any Network] = [
HackClub(id: "1"),
HackClub(id: "2"),
WireGuard(id: "4"),
HackClub(id: "5"),
]
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(networks, id: \.id) { network in
NetworkView(network: network)
.containerRelativeFrame(.horizontal, count: 10, span: 7, spacing: 0, alignment: .center)
.scrollTransition(.interactive, axis: .horizontal) { content, phase in
content
.scaleEffect(1.0 - abs(phase.value) * 0.1)
}
}
AddNetworkView()
}
.scrollTargetLayout()
}
.scrollClipDisabled()
.scrollIndicators(.hidden)
.defaultScrollAnchor(.center)
.scrollTargetBehavior(.viewAligned)
.containerRelativeFrame(.horizontal)
}
}
#if DEBUG
struct NetworkCarouselView_Previews: PreviewProvider {
static var previews: some View {
NetworkCarouselView()
}
}
#endif

View file

@ -0,0 +1,23 @@
import SwiftUI
struct HackClub: Network {
var id: String
var backgroundColor: Color { .init("HackClub") }
var label: some View {
GeometryReader { reader in
VStack(alignment: .leading) {
Image("HackClub")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: reader.size.height / 4)
Spacer()
Text("@conradev")
.foregroundStyle(.white)
.font(.body.monospaced())
}
.padding()
.frame(maxWidth: .infinity)
}
}
}

View file

@ -0,0 +1,10 @@
import SwiftUI
protocol Network {
associatedtype Label: View
var id: String { get }
var backgroundColor: Color { get }
var label: Label { get }
}

View file

@ -0,0 +1,30 @@
import SwiftUI
struct WireGuard: Network {
var id: String
var backgroundColor: Color { .init("WireGuard") }
var label: some View {
GeometryReader { reader in
VStack(alignment: .leading) {
HStack {
Image("WireGuard")
.resizable()
.aspectRatio(contentMode: .fit)
Image("WireGuardTitle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: reader.size.width / 2)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: reader.size.height / 4)
Spacer()
Text("@conradev")
.foregroundStyle(.white)
.font(.body.monospaced())
}
.padding()
.frame(maxWidth: .infinity)
}
}
}

50
Apple/App/Tunnel.swift Normal file
View file

@ -0,0 +1,50 @@
import SwiftUI
protocol Tunnel {
var status: TunnelStatus { get }
func start()
func stop()
func enable()
}
enum TunnelStatus: Equatable, Hashable {
case unknown
case permissionRequired
case disabled
case connecting
case connected(Date)
case disconnecting
case disconnected
case reasserting
case invalid
case configurationReadWriteFailed
}
struct TunnelKey: EnvironmentKey {
static let defaultValue: any Tunnel = NetworkExtensionTunnel()
}
extension EnvironmentValues {
var tunnel: any Tunnel {
get { self[TunnelKey.self] }
set { self[TunnelKey.self] = newValue }
}
}
#if DEBUG
@Observable
class PreviewTunnel: Tunnel {
var status: TunnelStatus = .permissionRequired
func start() {
status = .connected(.now)
}
func stop() {
status = .disconnected
}
func enable() {
status = .disconnected
}
}
#endif

View file

@ -4,24 +4,21 @@ struct TunnelButton: View {
@Environment(\.tunnel) @Environment(\.tunnel)
var tunnel: any Tunnel var tunnel: any Tunnel
private var action: Action? { tunnel.action }
var body: some View { var body: some View {
if let action = tunnel.action {
Button { Button {
if let action {
tunnel.perform(action) tunnel.perform(action)
}
} label: { } label: {
Text(action.description) Text(action.description)
} }
.disabled(action.isDisabled)
.padding(.horizontal) .padding(.horizontal)
.buttonStyle(.floating) .buttonStyle(.floating)
} }
}
} }
extension Tunnel { extension Tunnel {
@MainActor fileprivate var action: TunnelButton.Action? { fileprivate var action: TunnelButton.Action? {
switch status { switch status {
case .permissionRequired, .invalid: case .permissionRequired, .invalid:
.enable .enable
@ -43,21 +40,12 @@ extension TunnelButton {
} }
} }
extension TunnelButton.Action? { extension TunnelButton.Action {
var description: LocalizedStringKey { var description: LocalizedStringKey {
switch self { switch self {
case .enable: "Enable" case .enable: "Enable"
case .start: "Start" case .start: "Start"
case .stop: "Stop" case .stop: "Stop"
case .none: "Start"
}
}
var isDisabled: Bool {
if case .none = self {
true
} else {
false
} }
} }
} }

View file

@ -10,7 +10,7 @@ struct TunnelStatusView: View {
} }
extension TunnelStatus: CustomStringConvertible { extension TunnelStatus: CustomStringConvertible {
public var description: String { var description: String {
switch self { switch self {
case .unknown: case .unknown:
"Unknown" "Unknown"

View file

@ -1,439 +0,0 @@
import XCTest
import UIKit
@MainActor
final class BurrowTailnetLoginUITests: XCTestCase {
private enum TailnetLoginMode: String, Decodable {
case tailscale
case discovered
}
private struct TestConfig: Decodable {
let email: String
let username: String
let password: String
let mode: TailnetLoginMode?
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testTailnetLoginThroughAuthentikWebSession() throws {
let config = try loadTestConfig()
let email = config.email
let username = config.username
let password = config.password
let mode = config.mode ?? .tailscale
let browserIdentity = mode == .tailscale ? email : username
let app = XCUIApplication()
app.launch()
let tailnetButton = app.buttons["quick-add-tailnet"]
XCTAssertTrue(tailnetButton.waitForExistence(timeout: 15), "Tailnet add button did not appear")
tailnetButton.tap()
configureTailnetIfNeeded(in: app, mode: mode)
let discoveryField = app.textFields["tailnet-discovery-email"]
XCTAssertTrue(discoveryField.waitForExistence(timeout: 10), "Tailnet discovery email field did not appear")
replaceText(in: discoveryField, with: email)
let serverCard = app.descendants(matching: .any)
.matching(identifier: "tailnet-server-card")
.firstMatch
XCTAssertTrue(serverCard.waitForExistence(timeout: 5), "Tailnet server card did not appear")
let signInButton = app.buttons["tailnet-start-sign-in"]
XCTAssertTrue(signInButton.waitForExistence(timeout: 10), "Tailnet sign-in button did not appear")
signInButton.tap()
acceptAuthenticationPromptIfNeeded(in: app, timeout: 20)
let webSession = webAuthenticationSession()
XCTAssertTrue(webSession.waitForExistence(timeout: 20), "Safari authentication session did not appear")
signIntoAuthentik(in: webSession, username: browserIdentity, password: password)
app.activate()
XCTAssertTrue(
waitForTailnetSignedIn(in: app, timeout: 60),
"Tailnet sign-in never reached the running state"
)
}
private func configureTailnetIfNeeded(in app: XCUIApplication, mode: TailnetLoginMode) {
guard mode == .discovered else { return }
openTailnetMenu(in: app)
tapMenuButton(named: "Edit Custom Server", in: app)
openTailnetMenu(in: app)
tapMenuButton(named: "Show Advanced Settings", in: app)
let authorityField = app.textFields["tailnet-authority"]
XCTAssertTrue(authorityField.waitForExistence(timeout: 10), "Tailnet authority field did not appear")
replaceText(in: authorityField, with: "")
}
private func openTailnetMenu(in app: XCUIApplication) {
let moreButton = app.buttons["More"]
XCTAssertTrue(moreButton.waitForExistence(timeout: 5), "Tailnet menu button did not appear")
moreButton.tap()
}
private func tapMenuButton(named title: String, in app: XCUIApplication) {
let menuButton = firstExistingElement(
from: [
app.buttons[title],
app.descendants(matching: .button)[title],
],
timeout: 5
)
XCTAssertTrue(menuButton.exists, "Menu action \(title) did not appear")
menuButton.tap()
}
private func acceptAuthenticationPromptIfNeeded(
in app: XCUIApplication,
timeout: TimeInterval
) {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let deadline = Date().addingTimeInterval(timeout)
repeat {
let promptCandidates = [
springboard.buttons["Continue"],
springboard.buttons["Allow"],
app.buttons["Continue"],
app.buttons["Allow"],
]
for button in promptCandidates where button.exists && button.isHittable {
button.tap()
return
}
RunLoop.current.run(until: Date().addingTimeInterval(0.25))
} while Date() < deadline
let promptCandidates = [
springboard.buttons["Continue"],
springboard.buttons["Allow"],
app.buttons["Continue"],
app.buttons["Allow"],
]
for button in promptCandidates where button.exists {
button.tap()
return
}
}
private func webAuthenticationSession() -> XCUIApplication {
let safariViewService = XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")
if safariViewService.waitForExistence(timeout: 5) {
return safariViewService
}
let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
_ = safari.waitForExistence(timeout: 5)
return safari
}
private func signIntoAuthentik(in webSession: XCUIApplication, username: String, password: String) {
followTailnetRedirectIfNeeded(in: webSession)
if !webSession.exists {
return
}
let immediatePasswordField = firstExistingSecureField(in: webSession, timeout: 2)
if immediatePasswordField.exists {
replaceSecureText(in: immediatePasswordField, within: webSession, with: password)
submitAuthenticationForm(in: webSession, focusedField: immediatePasswordField)
return
}
let usernameField = firstExistingElement(
in: webSession,
queries: [
{ $0.textFields["Username"] },
{ $0.textFields["Email or Username"] },
{ $0.textFields["Email address"] },
{ $0.textFields["Email"] },
{ $0.webViews.textFields["Username"] },
{ $0.webViews.textFields["Email or Username"] },
{ $0.descendants(matching: .textField).firstMatch },
],
timeout: 12
)
if !usernameField.exists {
return
}
replaceText(in: usernameField, with: username)
tapFirstExistingButton(
in: webSession,
titles: ["Continue", "Next", "Sign In", "Log in", "Login"],
timeout: 5
)
let passwordField = firstExistingSecureField(in: webSession, timeout: 20)
XCTAssertTrue(passwordField.exists, "Authentik password field did not appear")
replaceSecureText(in: passwordField, within: webSession, with: password)
submitAuthenticationForm(in: webSession, focusedField: passwordField)
}
private func followTailnetRedirectIfNeeded(in webSession: XCUIApplication) {
let redirectCandidates = [
webSession.links["Found"],
webSession.webViews.links["Found"],
webSession.buttons["Found"],
webSession.webViews.buttons["Found"],
]
let redirectLink = firstExistingElement(from: redirectCandidates, timeout: 8)
if redirectLink.exists {
redirectLink.tap()
}
}
private func firstExistingSecureField(in app: XCUIApplication, timeout: TimeInterval) -> XCUIElement {
let candidates = [
app.descendants(matching: .secureTextField).firstMatch,
app.secureTextFields["Password"],
app.secureTextFields["Password or Token"],
app.webViews.secureTextFields["Password"],
app.webViews.secureTextFields["Password or Token"],
]
return firstExistingElement(from: candidates, timeout: timeout)
}
private func tapFirstExistingButton(
in app: XCUIApplication,
titles: [String],
timeout: TimeInterval
) {
let candidates = titles.flatMap { title in
[
app.buttons[title],
app.webViews.buttons[title],
]
} + [app.descendants(matching: .button).firstMatch]
let button = firstExistingElement(from: candidates, timeout: timeout)
XCTAssertTrue(button.exists, "Expected one of \(titles.joined(separator: ", ")) to appear")
button.tap()
}
private func submitAuthenticationForm(in app: XCUIApplication, focusedField: XCUIElement) {
focus(focusedField)
focusedField.typeText("\n")
if waitForAny(
[
{ !focusedField.exists },
{ !app.staticTexts["Burrow Tailnet Authentication"].exists },
],
timeout: 1.5
) {
return
}
let keyboard = app.keyboards.firstMatch
if keyboard.waitForExistence(timeout: 2) {
let keyboardCandidates = [
"Return",
"return",
"Go",
"go",
"Continue",
"continue",
"Done",
"done",
"Join",
"join",
"Sign In",
"Log In",
"Login",
]
for title in keyboardCandidates {
let key = keyboard.buttons[title]
if key.exists && key.isHittable {
key.tap()
return
}
}
if let lastKey = keyboard.buttons.allElementsBoundByIndex.last,
lastKey.exists,
lastKey.isHittable
{
lastKey.tap()
return
}
}
tapFirstExistingButton(
in: app,
titles: ["Continue", "Sign In", "Log in", "Login"],
timeout: 5
)
}
private func loadTestConfig() throws -> TestConfig {
let environment = ProcessInfo.processInfo.environment
if let email = nonEmptyEnvironment("BURROW_UI_TEST_EMAIL"),
let password = nonEmptyEnvironment("BURROW_UI_TEST_PASSWORD")
{
return TestConfig(
email: email,
username: nonEmptyEnvironment("BURROW_UI_TEST_USERNAME") ?? email,
password: password,
mode: nonEmptyEnvironment("BURROW_UI_TEST_TAILNET_MODE")
.flatMap(TailnetLoginMode.init(rawValue:))
)
}
let configPath = environment["BURROW_UI_TEST_CONFIG_PATH"] ?? "/tmp/burrow-ui-test-config.json"
let configURL = URL(fileURLWithPath: configPath)
guard FileManager.default.fileExists(atPath: configURL.path) else {
throw XCTSkip(
"Missing UI test configuration. Expected env vars or config file at \(configURL.path)"
)
}
let data = try Data(contentsOf: configURL)
return try JSONDecoder().decode(TestConfig.self, from: data)
}
private func nonEmptyEnvironment(_ key: String) -> String? {
guard let value = ProcessInfo.processInfo.environment[key]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
else {
return nil
}
return value
}
private func waitForFieldValue(
_ field: XCUIElement,
containing substring: String,
timeout: TimeInterval
) -> Bool {
let predicate = NSPredicate(format: "value CONTAINS %@", substring)
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: field)
return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
}
private func waitForButtonLabel(
_ button: XCUIElement,
equals expected: String,
timeout: TimeInterval
) -> Bool {
let predicate = NSPredicate(format: "label == %@", expected)
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button)
return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed
}
private func waitForTailnetSignedIn(in app: XCUIApplication, timeout: TimeInterval) -> Bool {
let button = app.buttons["tailnet-start-sign-in"]
let deadline = Date().addingTimeInterval(timeout)
repeat {
acceptAuthenticationPromptIfNeeded(in: app, timeout: 1)
if button.exists, button.label == "Signed In" {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
} while Date() < deadline
return button.exists && button.label == "Signed In"
}
private func waitForAny(_ conditions: [() -> Bool], timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
repeat {
if conditions.contains(where: { $0() }) {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
} while Date() < deadline
return conditions.contains(where: { $0() })
}
private func firstExistingElement(
in app: XCUIApplication,
queries: [(XCUIApplication) -> XCUIElement],
timeout: TimeInterval
) -> XCUIElement {
firstExistingElement(from: queries.map { $0(app) }, timeout: timeout)
}
private func firstExistingElement(from candidates: [XCUIElement], timeout: TimeInterval) -> XCUIElement {
let deadline = Date().addingTimeInterval(timeout)
repeat {
for candidate in candidates where candidate.exists {
return candidate
}
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
} while Date() < deadline
return candidates[0]
}
private func replaceText(in element: XCUIElement, with value: String) {
focus(element)
clearText(in: element)
element.typeText(value)
}
private func replaceSecureText(in element: XCUIElement, within app: XCUIApplication, with value: String) {
UIPasteboard.general.string = value
focus(element)
for revealMenu in [
{ element.doubleTap() },
{ element.press(forDuration: 1.2) },
] {
revealMenu()
let pasteButton = firstExistingElement(from: pasteCandidates(in: app), timeout: 3)
if pasteButton.exists {
pasteButton.tap()
return
}
}
focus(element)
element.typeText(value)
}
private func clearText(in element: XCUIElement) {
guard let currentValue = element.value as? String, !currentValue.isEmpty else {
return
}
let deleteSequence = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
element.typeText(deleteSequence)
}
private func focus(_ element: XCUIElement) {
element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
RunLoop.current.run(until: Date().addingTimeInterval(0.3))
}
private func pasteCandidates(in app: XCUIApplication) -> [XCUIElement] {
let pasteLabels = ["Paste", "Incolla", "Paste from Clipboard"]
return pasteLabels.flatMap { label in
[
app.menuItems[label],
app.buttons[label],
app.webViews.buttons[label],
app.descendants(matching: .button).matching(NSPredicate(format: "label == %@", label)).firstMatch,
app.descendants(matching: .menuItem).matching(NSPredicate(format: "label == %@", label)).firstMatch,
]
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,132 +1,86 @@
{ {
"originHash" : "5d81b59cbecacd7aae6aa598b32e3513c636250dadab9280a55b905bb0e0ee60",
"pins" : [ "pins" : [
{ {
"identity" : "grpc-swift", "identity" : "collectionconcurrencykit",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift.git", "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
"state" : { "state" : {
"revision" : "6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1", "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version" : "1.23.0" "version" : "0.2.0"
} }
}, },
{ {
"identity" : "sparkle", "identity" : "cryptoswift",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle.git", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : { "state" : {
"revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b", "revision" : "7892a123f7e8d0fe62f9f03728b17bbd4f94df5c",
"version" : "2.9.2" "version" : "1.8.1"
} }
}, },
{ {
"identity" : "swift-async-algorithms", "identity" : "sourcekitten",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git", "location" : "https://github.com/jpsim/SourceKitten.git",
"state" : { "state" : {
"revision" : "6ae9a051f76b81cc668305ceed5b0e0a7fd93d20", "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
"version" : "1.0.1" "version" : "0.34.1"
} }
}, },
{ {
"identity" : "swift-atomics", "identity" : "swift-argument-parser",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git", "location" : "https://github.com/apple/swift-argument-parser.git",
"state" : { "state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985", "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.0" "version" : "1.2.3"
} }
}, },
{ {
"identity" : "swift-collections", "identity" : "swift-syntax",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git", "location" : "https://github.com/apple/swift-syntax.git",
"state" : { "state" : {
"revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
"version" : "1.1.3" "version" : "509.1.1"
} }
}, },
{ {
"identity" : "swift-http-types", "identity" : "swiftlint",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types", "location" : "https://github.com/realm/SwiftLint.git",
"state" : { "state" : {
"revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", "branch" : "main",
"version" : "1.3.0" "revision" : "7595ad3fafc1a31086dc40ba01fd898bf6b42d5f"
} }
}, },
{ {
"identity" : "swift-log", "identity" : "swiftytexttable",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git", "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git",
"state" : { "state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
"version" : "1.6.1" "version" : "0.9.0"
} }
}, },
{ {
"identity" : "swift-nio", "identity" : "swxmlhash",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git", "location" : "https://github.com/drmohundro/SWXMLHash.git",
"state" : { "state" : {
"revision" : "9746cf80e29edfef2a39924a66731249223f42a3", "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f",
"version" : "2.72.0" "version" : "7.0.2"
} }
}, },
{ {
"identity" : "swift-nio-extras", "identity" : "yams",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git", "location" : "https://github.com/jpsim/Yams.git",
"state" : { "state" : {
"revision" : "d1ead62745cc3269e482f1c51f27608057174379", "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "1.24.0" "version" : "5.0.6"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "b5f7062b60e4add1e8c343ba4eb8da2e324b3a94",
"version" : "1.34.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "7b84abbdcef69cc3be6573ac12440220789dcd69",
"version" : "2.27.2"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "38ac8221dd20674682148d6451367f89c2652980",
"version" : "1.21.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5",
"version" : "1.28.1"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "d2ba781702a1d8285419c15ee62fd734a9437ff5",
"version" : "1.3.2"
} }
} }
], ],
"version" : 3 "version" : 2
} }

View file

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1600" LastUpgradeVersion = "1520"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@ -28,20 +27,7 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "NO"> shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D11000082F70000100112233"
BuildableName = "BurrowUITests.xctest"
BlueprintName = "BurrowUITests"
ReferencedContainer = "container:Burrow.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"

View file

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1600" LastUpgradeVersion = "1520"
wasCreatedForAppExtension = "YES"
version = "2.0"> version = "2.0">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"

View file

@ -1,41 +0,0 @@
# Apple Certificates
Public Apple certificate material that can be inspected by release tooling lives
here. Private keys do not belong in this directory.
## Developer ID Application
- Apple certificate id: `9JKN6HXBHC`
- Certificate file: `Apple/Certificates/developer-id-application-9JKN6HXBHC.cer`
- Subject: `Developer ID Application: Conrad Kramer (87PW93R2ZR)`
- Issuer: `Developer ID Certification Authority`, `G2`
- Team id: `87PW93R2ZR`
- Validity: `2026-06-07T12:13:47Z` through `2031-06-08T12:13:46Z`
- SHA-256 fingerprint: `7B:2D:58:A5:BB:1E:34:5E:FB:FF:7F:E0:A1:CA:4F:B1:5B:6E:08:AF:CE:FD:BE:D0:2B:0A:E9:9C:82:E3:4F:74`
- Google KMS key:
`projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity/cryptoKeys/apple-developer-id-application/cryptoKeyVersions/1`
- KMS public-key SHA-256:
`9cfaf4755158f445f20a215ef52e7a7ba00da1a310e2e934ba7db5dbb29d2869`
The certificate was created from a CSR signed by the non-exportable Google KMS
key above. There is no `.p12` for this identity. Release signing must use a
signer that delegates the relevant Apple code-signing operations to Google KMS.
## iOS Distribution
- Apple certificate id: `3G42677598`
- Certificate file: `Apple/Certificates/ios-distribution-3G42677598.cer`
- Subject: `iPhone Distribution: Conrad Kramer (87PW93R2ZR)`
- Issuer: `Apple Worldwide Developer Relations Certification Authority`, `G3`
- Team id: `87PW93R2ZR`
- Validity: `2026-06-07T12:21:32Z` through `2027-06-07T12:21:31Z`
- SHA-256 fingerprint: `5A:2F:BA:6E:FB:CA:2D:58:01:1F:A0:C8:0F:CF:6B:58:BE:AA:73:7F:4E:F8:06:1C:8A:14:1B:22:8A:2A:29:2B`
- Google KMS key:
`projects/project-88c23ce9-918a-470a-b33/locations/global/keyRings/burrow-identity/cryptoKeys/apple-ios-distribution/cryptoKeyVersions/1`
- KMS public-key SHA-256:
`f8613ce059de90b29acf01cbd4a7deb37567653bd154bdf6c447098b5c4fae91`
The certificate was created from a CSR signed by the non-exportable Google KMS
key above through App Store Connect certificate creation. There is no `.p12`
for this identity. iOS signing requires a signing path that can delegate Apple
code-signing operations to Google KMS.

View file

@ -1,8 +1,10 @@
LD_EXPORT_SYMBOLS = NO
SKIP_INSTALL = NO SKIP_INSTALL = NO
MERGED_BINARY_TYPE = manual
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks LD_RUNPATH_SEARCH_PATHS[sdk=iphone*] = $(inherited) @executable_path/Frameworks
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) @executable_path/../Frameworks
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
ENABLE_PREVIEWS = YES

View file

@ -1,43 +1,61 @@
#include "Identity.xcconfig" #include "Identity.xcconfig"
#include "Debug.xcconfig"
#include "Version.xcconfig"
SDKROOT = auto SDKROOT = auto
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES
SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx SUPPORTED_PLATFORMS = iphoneos iphonesimulator macosx
SWIFT_VERSION = 6.0
IPHONEOS_DEPLOYMENT_TARGET = 17.0 IPHONEOS_DEPLOYMENT_TARGET = 17.0
MACOSX_DEPLOYMENT_TARGET = 14.0 MACOSX_DEPLOYMENT_TARGET = 14.0
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO
SUPPORTS_MACCATALYST = NO SUPPORTS_MACCATALYST = NO
ALWAYS_SEARCH_USER_PATHS = NO
PRODUCT_NAME = $(TARGET_NAME:c99extidentifier) PRODUCT_NAME = $(TARGET_NAME:c99extidentifier)
PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(PRODUCT_NAME) PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).$(PRODUCT_NAME)
CURRENT_PROJECT_VERSION = 1
MARKETING_VERSION = 0.1 MARKETING_VERSION = 0.1
SKIP_INSTALL = YES SKIP_INSTALL = YES
CODE_SIGN_IDENTITY = Apple Development CODE_SIGN_IDENTITY = Apple Development
GENERATE_INFOPLIST_FILE = YES
INFOPLIST_FILE = Configuration/Info.plist INFOPLIST_FILE = Configuration/Info.plist
GENERATE_INFOPLIST_FILE = YES
INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023-2024 Hack Club INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023-2024 Hack Club
INFOPLIST_KEY_CFBundleDisplayName = Burrow INFOPLIST_KEY_CFBundleDisplayName = Burrow
ENABLE_APP_SANDBOX[sdk=macosx*] = YES
ENABLE_BITCODE = NO ENABLE_BITCODE = NO
ALWAYS_SEARCH_USER_PATHS = NO
ENABLE_APP_SANDBOX[sdk=macosx*] = YES
ENABLE_HARDENED_RUNTIME[sdk=macosx*] = YES
COMBINE_HIDPI_IMAGES = YES COMBINE_HIDPI_IMAGES = YES
EAGER_LINKING = YES COPY_PHASE_STRIP = NO
FUSE_BUILD_SCRIPT_PHASES = YES FUSE_BUILD_SCRIPT_PHASES = YES
SWIFT_EMIT_LOC_STRINGS = YES
LOCALIZATION_PREFERS_STRING_CATALOGS = YES
ENABLE_DEBUG_DYLIB = NO
APP_GROUP_IDENTIFIER = group.$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER = group.$(APP_BUNDLE_IDENTIFIER)
APP_GROUP_IDENTIFIER[sdk=macosx*] = $(DEVELOPMENT_TEAM).$(APP_BUNDLE_IDENTIFIER) APP_GROUP_IDENTIFIER[sdk=macosx*] = $(DEVELOPMENT_TEAM).$(APP_BUNDLE_IDENTIFIER)
NETWORK_EXTENSION_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network NETWORK_EXTENSION_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).network
OTHER_SWIFT_FLAGS = $(inherited) // Swift
SWIFT_VERSION = 5.0
SWIFT_EMIT_LOC_STRINGS = YES
// Release
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
SWIFT_COMPILATION_MODE = wholemodule
SWIFT_OPTIMIZATION_LEVEL = -Osize
LLVM_LTO = YES
DEAD_CODE_STRIPPING = YES
VALIDATE_PRODUCT = YES
// Debug
ONLY_ACTIVE_ARCH[config=Debug] = YES
DEBUG_INFORMATION_FORMAT[config=Debug] = dwarf
ENABLE_TESTABILITY[config=Debug] = YES
SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone
SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = DEBUG
SWIFT_COMPILATION_MODE[config=Debug] = singlefile
LLVM_LTO[config=Debug] = NO
DEAD_CODE_STRIPPING[config=Debug] = NO
VALIDATE_PRODUCT[config=Debug] = NO

Some files were not shown because too many files have changed in this diff Show more