Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s

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

View file

@ -0,0 +1,284 @@
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_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
if command -v nix >/dev/null 2>&1; then
local built
built="$(nix 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
"$decrypt_bin" --identity "$KEY" --decrypt "$file_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_to_file() {
local output="$1"
python3 -c 'import base64, pathlib, sys; pathlib.Path(sys.argv[1]).write_bytes(base64.b64decode(sys.stdin.buffer.read()))' "$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" | python3 -c 'import base64,sys; print(base64.b64decode(sys.stdin.buffer.read()).decode(), end="")')"
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

@ -0,0 +1,98 @@
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@v4
with:
name: burrow-developer-id-kms-csr
path: dist/apple-developer-id

View file

@ -0,0 +1,99 @@
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@v4
with:
name: burrow-ios-distribution-kms-csr
path: dist/apple-ios-distribution

View file

@ -0,0 +1,67 @@
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

@ -0,0 +1,74 @@
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

@ -0,0 +1,443 @@
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: "false"
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_APPLE_CREATE_MISSING_BUNDLE_IDS: ${{ vars.BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS || 'false' }}
BURROW_APPLE_ENABLE_CAPABILITIES: ${{ vars.BURROW_APPLE_ENABLE_CAPABILITIES || 'false' }}
BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'false' }}
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 || 'Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=' }}
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@v4
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@v4
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: 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 || 'false' }}
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
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)"
"$NIX_BIN" develop .#ci --command bazel "${bazel_args[@]}" build \
--verbose_failures \
--show_timestamps \
--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_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_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@v4
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@v4
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 || 'false' }}
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

@ -0,0 +1,237 @@
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

@ -0,0 +1,63 @@
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

@ -0,0 +1,148 @@
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@v4
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

@ -0,0 +1,299 @@
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: "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-${{ github.event.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' }}
jobs:
upload-app-store:
name: Upload (App Store Connect)
if: ${{ github.event.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: ${{ github.event.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=(publish/apple/*.ipa publish/apple/*.pkg)
if (( ${#artifacts[@]} == 0 )); then
echo "::error ::No Apple upload artifacts found for build ${{ github.event.inputs.build_number }}."
exit 1
fi
for artifact in "${artifacts[@]}"; do
upload_type="ios"
if [[ "$artifact" == *.pkg ]]; then
upload_type="macos"
fi
xcrun altool --upload-app \
--type "$upload_type" \
--file "$artifact" \
--apiKey "$ASC_API_KEY_ID" \
--apiIssuer "$ASC_API_ISSUER_ID"
done
release-ios-testflight:
name: Release (TestFlight iOS Testers)
needs: upload-app-store
if: ${{ github.event.inputs.upload_app_store == '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: Distribute iOS Build To TestFlight
shell: bash
env:
BUILD_NUMBER: ${{ github.event.inputs.build_number }}
TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }}
TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }}
TESTFLIGHT_INTERNAL_GROUP: Internal
IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }}
run: |
set -euo pipefail
Scripts/ci/check-release-config.sh app-store
api_key_json="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-api-key.XXXXXX.json")"
key_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-key.XXXXXX.p8")"
if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then
cp "$ASC_API_KEY_PATH" "$key_file"
else
printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_file"
fi
KEY_FILE="$key_file" python3 -c 'import json, os, pathlib; print(json.dumps({"key_id": os.environ["ASC_API_KEY_ID"], "issuer_id": os.environ["ASC_API_ISSUER_ID"], "key": pathlib.Path(os.environ["KEY_FILE"]).read_text(), "duration": 1200, "in_house": False}))' > "$api_key_json"
distribute_external="${TESTFLIGHT_DISTRIBUTE_EXTERNAL:-false}"
if [[ -z "$distribute_external" ]]; then
distribute_external="false"
fi
uses_non_exempt="${IOS_USES_NON_EXEMPT_ENCRYPTION:-false}"
if [[ -z "$uses_non_exempt" ]]; then
uses_non_exempt="false"
fi
args=(
run pilot
"api_key_path:${api_key_json}"
"app_identifier:${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}"
"app_platform:ios"
"distribute_only:true"
"build_number:${BUILD_NUMBER}"
"wait_processing_interval:30"
"wait_processing_timeout_duration:1800"
"distribute_external:${distribute_external}"
"uses_non_exempt_encryption:${uses_non_exempt}"
)
if [[ -n "${TESTFLIGHT_GROUPS}" ]]; then
args+=("groups:${TESTFLIGHT_GROUPS}")
elif [[ "$distribute_external" != "true" && -n "${TESTFLIGHT_INTERNAL_GROUP:-}" ]]; then
args+=("groups:${TESTFLIGHT_INTERNAL_GROUP}")
fi
nix run nixpkgs#fastlane -- "${args[@]}"
rm -f "$api_key_json" "$key_file"
upload-sparkle:
name: Upload (Sparkle)
if: ${{ github.event.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: ${{ github.event.inputs.build_number }}
run: |
set -euo pipefail
channel="${{ github.event.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: ${{ github.event.inputs.build_number }}
CHANNEL: ${{ steps.channel.outputs.channel }}
SPARKLE_POINT_MAIN: ${{ github.event.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: ${{ github.event.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

@ -0,0 +1,89 @@
name: "Release: If Needed"
run-name: "Release: If Needed (${{ github.ref_name }})"
on:
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
inputs:
upload_app_store:
description: Trigger downstream App Store Connect upload workflow
required: false
default: "false"
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 || 'false' }}
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

@ -10,6 +10,10 @@ 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
@ -39,6 +43,19 @@ jobs:
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@v4
with: