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
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:
parent
97c569fb35
commit
002bd382e9
199 changed files with 14268 additions and 185 deletions
10
.bazelignore
Normal file
10
.bazelignore
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.build
|
||||
.derived-data
|
||||
.git
|
||||
.nscloud-cache
|
||||
Apple/build
|
||||
ci-artifacts
|
||||
dist
|
||||
publish
|
||||
release
|
||||
target
|
||||
1
.bazelversion
Normal file
1
.bazelversion
Normal file
|
|
@ -0,0 +1 @@
|
|||
9.1.0
|
||||
284
.forgejo/actions/decrypt-release-secrets/action.yml
Normal file
284
.forgejo/actions/decrypt-release-secrets/action.yml
Normal 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"
|
||||
98
.forgejo/workflows/apple-developer-id-kms-csr.yml
Normal file
98
.forgejo/workflows/apple-developer-id-kms-csr.yml
Normal 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
|
||||
99
.forgejo/workflows/apple-ios-distribution-kms-csr.yml
Normal file
99
.forgejo/workflows/apple-ios-distribution-kms-csr.yml
Normal 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
|
||||
67
.forgejo/workflows/backup-garage-storage.yml
Normal file
67
.forgejo/workflows/backup-garage-storage.yml
Normal 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
|
||||
74
.forgejo/workflows/build-android.yml
Normal file
74
.forgejo/workflows/build-android.yml
Normal 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
|
||||
443
.forgejo/workflows/build-release.yml
Normal file
443
.forgejo/workflows/build-release.yml
Normal 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"
|
||||
237
.forgejo/workflows/infra-opentofu.yml
Normal file
237
.forgejo/workflows/infra-opentofu.yml
Normal 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
|
||||
63
.forgejo/workflows/publish-nix-cache.yml
Normal file
63
.forgejo/workflows/publish-nix-cache.yml
Normal 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
|
||||
148
.forgejo/workflows/publish-package-repositories.yml
Normal file
148
.forgejo/workflows/publish-package-repositories.yml
Normal 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
|
||||
299
.forgejo/workflows/publish-store-uploads.yml
Normal file
299
.forgejo/workflows/publish-store-uploads.yml
Normal 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."
|
||||
89
.forgejo/workflows/release-if-needed.yml
Normal file
89
.forgejo/workflows/release-if-needed.yml
Normal 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})."
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -1,6 +1,10 @@
|
|||
# Xcode
|
||||
xcuserdata
|
||||
Apple/build/
|
||||
.derived-data/
|
||||
AuthKey_*.p8
|
||||
burrow-certs-pass.txt
|
||||
*.p12
|
||||
|
||||
# Swift
|
||||
Apple/Package/.swiftpm/
|
||||
|
|
@ -9,11 +13,19 @@ Apple/Package/.swiftpm/
|
|||
target/
|
||||
.env
|
||||
|
||||
# Bazel
|
||||
bazel-*
|
||||
|
||||
.DS_STORE
|
||||
.idea/
|
||||
|
||||
tmp/
|
||||
.cache/
|
||||
intake/
|
||||
dist/
|
||||
publish/
|
||||
release/
|
||||
ci-artifacts/
|
||||
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
|
|
|||
14
Android/README.md
Normal file
14
Android/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# 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
|
||||
```
|
||||
21
Android/app/build.gradle.kts
Normal file
21
Android/app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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)
|
||||
}
|
||||
16
Android/app/src/main/AndroidManifest.xml
Normal file
16
Android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="@string/app_name"
|
||||
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>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package net.burrow.android
|
||||
|
||||
object BurrowCore {
|
||||
init {
|
||||
System.loadLibrary("burrow_mobile_ffi")
|
||||
}
|
||||
|
||||
external fun version(): String
|
||||
}
|
||||
32
Android/app/src/main/java/net/burrow/android/MainActivity.kt
Normal file
32
Android/app/src/main/java/net/burrow/android/MainActivity.kt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
3
Android/app/src/main/res/values/strings.xml
Normal file
3
Android/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Burrow</string>
|
||||
</resources>
|
||||
8
Android/app/src/main/res/values/styles.xml
Normal file
8
Android/app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<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>
|
||||
4
Android/build.gradle.kts
Normal file
4
Android/build.gradle.kts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
plugins {
|
||||
id("com.android.application") version "8.10.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.21" apply false
|
||||
}
|
||||
18
Android/settings.gradle.kts
Normal file
18
Android/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "BurrowAndroid"
|
||||
include(":app")
|
||||
|
|
@ -18,6 +18,10 @@ INFOPLIST_KEY_LSUIElement[sdk=macosx*] = YES
|
|||
INFOPLIST_KEY_NSMainNibFile[sdk=macosx*] = MainMenu
|
||||
INFOPLIST_KEY_NSPrincipalClass[sdk=macosx*] = NSApplication
|
||||
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 = Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=
|
||||
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[sdk=macosx*] = App/App-macOS.entitlements
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
#if os(macOS)
|
||||
import AppKit
|
||||
import BurrowUI
|
||||
import Sparkle
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
@MainActor
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var windowController: NSWindowController?
|
||||
private let updaterController = SPUStandardUpdaterController(
|
||||
startingUpdater: true,
|
||||
updaterDelegate: nil,
|
||||
userDriverDelegate: nil
|
||||
)
|
||||
|
||||
private let quitItem: NSMenuItem = {
|
||||
let quitItem = NSMenuItem(
|
||||
|
|
@ -30,6 +36,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
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 = {
|
||||
let toggleView = NSHostingView(rootView: MenuItemToggleView())
|
||||
toggleView.frame.size = CGSize(width: 300, height: 32)
|
||||
|
|
@ -45,6 +61,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
menu.items = [
|
||||
toggleItem,
|
||||
openItem,
|
||||
checkForUpdatesItem,
|
||||
.separator(),
|
||||
quitItem
|
||||
]
|
||||
|
|
@ -86,5 +103,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
windowController = controller
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func checkForUpdates(_ sender: Any?) {
|
||||
updaterController.checkForUpdates(sender)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
D03383B02C8E67E300F7C44E /* NIOTransportServices in Frameworks */ = {isa = PBXBuildFile; productRef = D044EE952C8DAB2800778185 /* NIOTransportServices */; };
|
||||
D05B9F7629E39EEC008CB1F9 /* BurrowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B9F7529E39EEC008CB1F9 /* BurrowApp.swift */; };
|
||||
D09150422B9D2AF700BE3CB0 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D09150412B9D2AF700BE3CB0 /* MainMenu.xib */; platformFilters = (macos, ); };
|
||||
D0A11C102F90000100112233 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = D0A11C0F2F90000100112233 /* DequeModule */; };
|
||||
D0B1D1102C436152004B7823 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = D0B1D10F2C436152004B7823 /* AsyncAlgorithms */; };
|
||||
D0BCC6092A09A03E00AD070D /* libburrow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0BCC6032A09535900AD070D /* libburrow.a */; };
|
||||
D0BF09522C8E66F6000D8DEC /* BurrowConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5622C8D9BF4007F820A /* BurrowConfiguration.framework */; };
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
D0FA10022D10200100112233 /* burrow.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10042D10200100112233 /* burrow.grpc.swift */; };
|
||||
D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */ = {isa = PBXBuildFile; productRef = D0F7597D2C8DB30500126CF3 /* CGRPCZlib */; };
|
||||
D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4992C8D921A007F820A /* Client.swift */; };
|
||||
D0C0DE102FA0000100112233 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; platformFilters = (macos, ); productRef = D0C0DE122FA0000100112233 /* Sparkle */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -218,6 +220,7 @@
|
|||
D0BF09552C8E66FD000D8DEC /* BurrowConfiguration.framework in Frameworks */,
|
||||
D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */,
|
||||
D0D4E5892C8D9C94007F820A /* BurrowUI.framework in Frameworks */,
|
||||
D0C0DE102FA0000100112233 /* Sparkle in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -238,6 +241,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0A11C102F90000100112233 /* DequeModule in Frameworks */,
|
||||
D0D4E5702C8D9C62007F820A /* BurrowCore.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -460,6 +464,9 @@
|
|||
D020F65C29E4A697002790F6 /* PBXTargetDependency */,
|
||||
);
|
||||
name = App;
|
||||
packageProductDependencies = (
|
||||
D0C0DE122FA0000100112233 /* Sparkle */,
|
||||
);
|
||||
productName = Burrow;
|
||||
productReference = D05B9F7229E39EEC008CB1F9 /* Burrow.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
|
@ -504,6 +511,7 @@
|
|||
);
|
||||
name = UI;
|
||||
packageProductDependencies = (
|
||||
D0A11C0F2F90000100112233 /* DequeModule */,
|
||||
);
|
||||
productName = Core;
|
||||
productReference = D0D4E5582C8D9BF2007F820A /* BurrowUI.framework */;
|
||||
|
|
@ -567,6 +575,8 @@
|
|||
D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */,
|
||||
D044EE8F2C8DAB2000778185 /* XCRemoteSwiftPackageReference "swift-nio" */,
|
||||
D044EE942C8DAB2800778185 /* XCRemoteSwiftPackageReference "swift-nio-transport-services" */,
|
||||
D0A11C0D2F90000100112233 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
D0C0DE112FA0000100112233 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
);
|
||||
productRefGroup = D05B9F7329E39EEC008CB1F9 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -949,6 +959,14 @@
|
|||
minimumVersion = 1.21.0;
|
||||
};
|
||||
};
|
||||
D0A11C0D2F90000100112233 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.1.3;
|
||||
};
|
||||
};
|
||||
D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-async-algorithms.git";
|
||||
|
|
@ -957,6 +975,14 @@
|
|||
minimumVersion = 1.0.1;
|
||||
};
|
||||
};
|
||||
D0C0DE112FA0000100112233 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.9.2;
|
||||
};
|
||||
};
|
||||
D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/grpc/grpc-swift.git";
|
||||
|
|
@ -1001,11 +1027,21 @@
|
|||
package = D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */;
|
||||
productName = SwiftProtobuf;
|
||||
};
|
||||
D0A11C0F2F90000100112233 /* DequeModule */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D0A11C0D2F90000100112233 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = DequeModule;
|
||||
};
|
||||
D0B1D10F2C436152004B7823 /* AsyncAlgorithms */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */;
|
||||
productName = AsyncAlgorithms;
|
||||
};
|
||||
D0C0DE122FA0000100112233 /* Sparkle */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D0C0DE112FA0000100112233 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||
productName = Sparkle;
|
||||
};
|
||||
D0F7597D2C8DB30500126CF3 /* CGRPCZlib */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "fa512b990383b7e309c5854a5279817052294a8191a6d3c55c49cfb38e88c0c3",
|
||||
"originHash" : "5d81b59cbecacd7aae6aa598b32e3513c636250dadab9280a55b905bb0e0ee60",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "grpc-swift",
|
||||
|
|
@ -10,6 +10,15 @@
|
|||
"version" : "1.23.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle.git",
|
||||
"state" : {
|
||||
"revision" : "6276ba2b404829d139c45ff98427cf90e2efc59b",
|
||||
"version" : "2.9.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-async-algorithms",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
|
|||
41
Apple/Certificates/README.md
Normal file
41
Apple/Certificates/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# 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.
|
||||
BIN
Apple/Certificates/developer-id-application-9JKN6HXBHC.cer
Normal file
BIN
Apple/Certificates/developer-id-application-9JKN6HXBHC.cer
Normal file
Binary file not shown.
BIN
Apple/Certificates/ios-distribution-3G42677598.cer
Normal file
BIN
Apple/Certificates/ios-distribution-3G42677598.cer
Normal file
Binary file not shown.
|
|
@ -4,5 +4,9 @@
|
|||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(INFOPLIST_KEY_CFBundleDisplayName)</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>$(BURROW_SPARKLE_FEED_URL)</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>$(BURROW_SPARKLE_PUBLIC_ED_KEY)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
69
BUILD.bazel
Normal file
69
BUILD.bazel
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
filegroup(
|
||||
name = "apple_project_files",
|
||||
srcs = glob(
|
||||
[
|
||||
"Apple/**",
|
||||
"Scripts/ci/install-apple-signing-assets.sh",
|
||||
"Scripts/ci/package-apple-artifacts.sh",
|
||||
"Scripts/version.sh",
|
||||
],
|
||||
exclude = [
|
||||
"Apple/build/**",
|
||||
"Apple/**/xcuserdata/**",
|
||||
],
|
||||
),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "android_project_files",
|
||||
srcs = glob(
|
||||
[
|
||||
"Android/**",
|
||||
],
|
||||
exclude = [
|
||||
"Android/.gradle/**",
|
||||
"Android/**/build/**",
|
||||
],
|
||||
),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "mobile_rust_core_files",
|
||||
srcs = glob(
|
||||
[
|
||||
"crates/burrow-core/**",
|
||||
"crates/burrow-mobile-ffi/**",
|
||||
],
|
||||
) + [
|
||||
"Cargo.lock",
|
||||
"Cargo.toml",
|
||||
"rust-toolchain.toml",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "apple_release_ios",
|
||||
actual = "//bazel/apple:release_ios_stamp",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "apple_release_macos",
|
||||
actual = "//bazel/apple:release_macos_stamp",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "apple_release_all",
|
||||
actual = "//bazel/apple:release_all_stamp",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "android_check_stub",
|
||||
actual = "//bazel/android:check_stub_stamp",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
142
Cargo.lock
generated
142
Cargo.lock
generated
|
|
@ -767,6 +767,22 @@ dependencies = [
|
|||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "burrow-core"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "burrow-mobile-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"burrow-core",
|
||||
"jni",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "burrow-windows-stub"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
|
|
@ -845,6 +861,12 @@ dependencies = [
|
|||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
|
|
@ -1008,6 +1030,16 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.32"
|
||||
|
|
@ -2944,6 +2976,50 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
||||
dependencies = [
|
||||
"jni-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
|
|
@ -7471,6 +7547,15 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
|
@ -7516,6 +7601,21 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7573,6 +7673,12 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7591,6 +7697,12 @@ version = "0.53.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7609,6 +7721,12 @@ version = "0.53.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7639,6 +7757,12 @@ version = "0.53.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7657,6 +7781,12 @@ version = "0.53.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7675,6 +7805,12 @@ version = "0.53.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
|
|
@ -7693,6 +7829,12 @@ version = "0.53.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
[workspace]
|
||||
members = ["burrow", "tun"]
|
||||
members = [
|
||||
"burrow",
|
||||
"tun",
|
||||
"crates/burrow-core",
|
||||
"crates/burrow-mobile-ffi",
|
||||
"crates/burrow-windows-stub",
|
||||
]
|
||||
resolver = "2"
|
||||
exclude = ["burrow-gtk"]
|
||||
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ LABEL \
|
|||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
org.opencontainers.image.title="burrow" \
|
||||
org.opencontainers.image.description="Burrow is an open source tool for burrowing through firewalls, built by teenagers at Hack Club." \
|
||||
org.opencontainers.image.url="https://github.com/hackclub/burrow" \
|
||||
org.opencontainers.image.source="https://github.com/hackclub/burrow" \
|
||||
org.opencontainers.image.url="https://git.burrow.net/hackclub/burrow" \
|
||||
org.opencontainers.image.source="https://git.burrow.net/hackclub/burrow" \
|
||||
org.opencontainers.image.vendor="hackclub" \
|
||||
org.opencontainers.image.licenses="GPL-3.0"
|
||||
|
||||
|
|
|
|||
7
MODULE.bazel
Normal file
7
MODULE.bazel
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
module(
|
||||
name = "burrow",
|
||||
version = "0.1.0",
|
||||
)
|
||||
|
||||
bazel_dep(name = "platforms", version = "1.0.0")
|
||||
bazel_dep(name = "rules_shell", version = "0.6.1")
|
||||
448
MODULE.bazel.lock
generated
Normal file
448
MODULE.bazel.lock
generated
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
{
|
||||
"lockFileVersion": 26,
|
||||
"registryFileHashes": {
|
||||
"https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
|
||||
"https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f",
|
||||
"https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
|
||||
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
|
||||
"https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108",
|
||||
"https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
|
||||
"https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000",
|
||||
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
|
||||
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
|
||||
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
|
||||
"https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580",
|
||||
"https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96",
|
||||
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
|
||||
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
|
||||
"https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d",
|
||||
"https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42",
|
||||
"https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
|
||||
"https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a",
|
||||
"https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4",
|
||||
"https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
|
||||
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
|
||||
"https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68",
|
||||
"https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
|
||||
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
|
||||
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
|
||||
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96",
|
||||
"https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c",
|
||||
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
|
||||
"https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400",
|
||||
"https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
|
||||
},
|
||||
"selectedYankedVersions": {},
|
||||
"moduleExtensions": {
|
||||
"@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "Ga4z8lQy1YQ5rAMy+dOl0dqcCEBnYNCXku8x3YQmDZI=",
|
||||
"usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools"
|
||||
],
|
||||
"generatedRepoSpecs": {
|
||||
"com_github_jetbrains_kotlin_git": {
|
||||
"repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
|
||||
],
|
||||
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
|
||||
}
|
||||
},
|
||||
"com_github_jetbrains_kotlin": {
|
||||
"repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository",
|
||||
"attributes": {
|
||||
"git_repository_name": "com_github_jetbrains_kotlin_git",
|
||||
"compiler_version": "1.9.23"
|
||||
}
|
||||
},
|
||||
"com_github_google_ksp": {
|
||||
"repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
|
||||
],
|
||||
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
|
||||
"strip_version": "1.9.23-1.0.20"
|
||||
}
|
||||
},
|
||||
"com_github_pinterest_ktlint": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file",
|
||||
"attributes": {
|
||||
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
|
||||
"urls": [
|
||||
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
|
||||
],
|
||||
"executable": true
|
||||
}
|
||||
},
|
||||
"rules_android": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
|
||||
"strip_prefix": "rules_android-0.1.1",
|
||||
"urls": [
|
||||
"https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@rules_python+//python/extensions:config.bzl%config": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "iibnRYgg8LpcfmH7EAnVwYePC3jsVaJ6Id8XxUjSZps=",
|
||||
"usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:rules_python+,pypi__build rules_python++config+pypi__build",
|
||||
"REPO_MAPPING:rules_python+,pypi__click rules_python++config+pypi__click",
|
||||
"REPO_MAPPING:rules_python+,pypi__colorama rules_python++config+pypi__colorama",
|
||||
"REPO_MAPPING:rules_python+,pypi__importlib_metadata rules_python++config+pypi__importlib_metadata",
|
||||
"REPO_MAPPING:rules_python+,pypi__installer rules_python++config+pypi__installer",
|
||||
"REPO_MAPPING:rules_python+,pypi__more_itertools rules_python++config+pypi__more_itertools",
|
||||
"REPO_MAPPING:rules_python+,pypi__packaging rules_python++config+pypi__packaging",
|
||||
"REPO_MAPPING:rules_python+,pypi__pep517 rules_python++config+pypi__pep517",
|
||||
"REPO_MAPPING:rules_python+,pypi__pip rules_python++config+pypi__pip",
|
||||
"REPO_MAPPING:rules_python+,pypi__pip_tools rules_python++config+pypi__pip_tools",
|
||||
"REPO_MAPPING:rules_python+,pypi__pyproject_hooks rules_python++config+pypi__pyproject_hooks",
|
||||
"REPO_MAPPING:rules_python+,pypi__setuptools rules_python++config+pypi__setuptools",
|
||||
"REPO_MAPPING:rules_python+,pypi__tomli rules_python++config+pypi__tomli",
|
||||
"REPO_MAPPING:rules_python+,pypi__wheel rules_python++config+pypi__wheel",
|
||||
"REPO_MAPPING:rules_python+,pypi__zipp rules_python++config+pypi__zipp"
|
||||
],
|
||||
"generatedRepoSpecs": {
|
||||
"rules_python_internal": {
|
||||
"repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo",
|
||||
"attributes": {
|
||||
"transition_setting_generators": {},
|
||||
"transition_settings": []
|
||||
}
|
||||
},
|
||||
"pypi__build": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl",
|
||||
"sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__click": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl",
|
||||
"sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__colorama": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",
|
||||
"sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__importlib_metadata": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl",
|
||||
"sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__installer": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl",
|
||||
"sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__more_itertools": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl",
|
||||
"sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__packaging": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl",
|
||||
"sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pep517": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl",
|
||||
"sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pip": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl",
|
||||
"sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pip_tools": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl",
|
||||
"sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pyproject_hooks": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl",
|
||||
"sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__setuptools": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl",
|
||||
"sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__tomli": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
|
||||
"sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__wheel": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl",
|
||||
"sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__zipp": {
|
||||
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl",
|
||||
"sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@rules_python+//python/uv:uv.bzl%uv": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "ijW9KS7qsIY+yBVvJ+Nr1mzwQox09j13DnE3iIwaeTM=",
|
||||
"usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=",
|
||||
"recordedInputs": [
|
||||
"REPO_MAPPING:rules_python+,bazel_tools bazel_tools",
|
||||
"REPO_MAPPING:rules_python+,platforms platforms"
|
||||
],
|
||||
"generatedRepoSpecs": {
|
||||
"uv": {
|
||||
"repoRuleId": "@@rules_python+//python/uv/private:uv_toolchains_repo.bzl%uv_toolchains_repo",
|
||||
"attributes": {
|
||||
"toolchain_type": "'@@rules_python+//python/uv:uv_toolchain_type'",
|
||||
"toolchain_names": [
|
||||
"none"
|
||||
],
|
||||
"toolchain_implementations": {
|
||||
"none": "'@@rules_python+//python:none'"
|
||||
},
|
||||
"toolchain_compatible_with": {
|
||||
"none": [
|
||||
"@platforms//:incompatible"
|
||||
]
|
||||
},
|
||||
"toolchain_target_settings": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"facts": {}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
# Burrow
|
||||
|
||||
  
|
||||
|
||||
Burrow is an open source tool for burrowing through firewalls, built by teenagers at [Hack Club](https://hackclub.com/).
|
||||
|
||||
`burrow` provides a simple command-line tool to open virtual interfaces and direct traffic through them.
|
||||
Routine verification now runs unprivileged with `cargo test --workspace --all-features`; only tunnel startup needs elevation.
|
||||
The canonical source forge is [git.burrow.net/hackclub/burrow](https://git.burrow.net/hackclub/burrow).
|
||||
|
||||
The repository now carries its own design and deployment record:
|
||||
|
||||
|
|
@ -18,7 +17,7 @@ The repository now carries its own design and deployment record:
|
|||
|
||||
## Contributing
|
||||
|
||||
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow. Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. Forge and deployment scaffolding live in [`flake.nix`](./flake.nix), [`nixos/`](./nixos), and [`.forgejo/workflows/`](./.forgejo/workflows/). Hosted mail backup operations live in [`docs/FORWARDEMAIL.md`](./docs/FORWARDEMAIL.md) and [`Tools/forwardemail-custom-s3.sh`](./Tools/forwardemail-custom-s3.sh).
|
||||
Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow. Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. Forge and deployment scaffolding live in [`flake.nix`](./flake.nix), [`nixos/`](./nixos), and [`.forgejo/workflows/`](./.forgejo/workflows/). Hosted mail backup operations live in [`docs/FORWARDEMAIL.md`](./docs/FORWARDEMAIL.md) and [`Tools/forwardemail-custom-s3.sh`](./Tools/forwardemail-custom-s3.sh). Platform service and distribution rollout notes live in [`docs/PLATFORM_SERVICES.md`](./docs/PLATFORM_SERVICES.md) and [`docs/DISTRIBUTION.md`](./docs/DISTRIBUTION.md).
|
||||
|
||||
Agent and governance-sensitive work should start with [AGENTS.md](./AGENTS.md), [CONSTITUTION.md](./CONSTITUTION.md), and the relevant BEPs under [`evolution/proposals/`](./evolution/proposals/). Identity and bootstrap metadata now live in [`contributors.nix`](./contributors.nix).
|
||||
|
||||
|
|
@ -49,6 +48,4 @@ Burrow is open source and licensed under the [GNU General Public License v3.0](.
|
|||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/hackclub/burrow/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hackclub/burrow" />
|
||||
</a>
|
||||
See the canonical forge history for current contributors.
|
||||
|
|
|
|||
164
Scripts/apple/create-asc-certificate.mjs
Executable file
164
Scripts/apple/create-asc-certificate.mjs
Executable file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
const crypto = globalThis.crypto ?? webcrypto;
|
||||
|
||||
function b64url(input) {
|
||||
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
||||
return buf
|
||||
.toString("base64")
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { _: [] };
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--help" || arg === "-h") out.help = true;
|
||||
else if (arg.startsWith("--")) {
|
||||
const key = arg.slice(2);
|
||||
const value = argv[i + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`missing value for --${key}`);
|
||||
out[key] = value;
|
||||
i++;
|
||||
} else {
|
||||
out._.push(arg);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function makeJwt({ keyId, issuerId, keyPem }) {
|
||||
const header = { alg: "ES256", kid: keyId, typ: "JWT" };
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = { iss: issuerId, exp: now + 20 * 60, aud: "appstoreconnect-v1" };
|
||||
const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
|
||||
const pkcs8Der = Buffer.from(
|
||||
keyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s+/g, ""),
|
||||
"base64",
|
||||
);
|
||||
const key = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
pkcs8Der,
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const sig = await crypto.subtle.sign(
|
||||
{ name: "ECDSA", hash: "SHA-256" },
|
||||
key,
|
||||
new TextEncoder().encode(input),
|
||||
);
|
||||
return `${input}.${b64url(Buffer.from(sig))}`;
|
||||
}
|
||||
|
||||
async function ascFetch(jwt, url, opts = {}) {
|
||||
const res = await fetch(url, {
|
||||
...opts,
|
||||
headers: {
|
||||
...(opts.headers ?? {}),
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const text = await res.text();
|
||||
let body;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
body = { raw: text };
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`ASC ${res.status} ${url}: ${JSON.stringify(body).slice(0, 2000)}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function certificateFileName(type, id) {
|
||||
return `${type.toLowerCase().replace(/_/g, "-")}-${id}.cer`;
|
||||
}
|
||||
|
||||
function csrContentForApi(csrFile) {
|
||||
const bytes = fs.readFileSync(csrFile);
|
||||
const text = bytes.toString("ascii");
|
||||
const pemMatch = text.match(/-----BEGIN CERTIFICATE REQUEST-----([\s\S]+?)-----END CERTIFICATE REQUEST-----/);
|
||||
if (pemMatch) {
|
||||
return pemMatch[1].replace(/\s+/g, "");
|
||||
}
|
||||
return bytes.toString("base64");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
if (args.help) {
|
||||
console.log("Usage: create-asc-certificate.mjs --key-id ID --issuer-id ID --key-file AuthKey.p8 --certificate-type IOS_DISTRIBUTION --csr-file file.csr --out-dir DIR");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const keyId = args["key-id"] ?? process.env.APP_STORE_CONNECT_KEY_ID;
|
||||
const issuerId = args["issuer-id"] ?? process.env.APP_STORE_CONNECT_ISSUER_ID;
|
||||
const keyFile = args["key-file"] ?? process.env.APP_STORE_CONNECT_KEY_FILE;
|
||||
const certificateType = args["certificate-type"] ?? "IOS_DISTRIBUTION";
|
||||
const csrFile = args["csr-file"];
|
||||
const outDir = args["out-dir"] ?? ".";
|
||||
const outFile = args["out-file"];
|
||||
const metadataFile = args["metadata-file"];
|
||||
|
||||
if (!keyId || !issuerId || !keyFile || !csrFile) {
|
||||
throw new Error("required: --key-id --issuer-id --key-file --csr-file");
|
||||
}
|
||||
|
||||
const jwt = await makeJwt({
|
||||
keyId,
|
||||
issuerId,
|
||||
keyPem: fs.readFileSync(keyFile, "utf8"),
|
||||
});
|
||||
const csrContent = csrContentForApi(csrFile);
|
||||
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/certificates", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "certificates",
|
||||
attributes: {
|
||||
certificateType,
|
||||
csrContent,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const cert = created?.data;
|
||||
const id = cert?.id;
|
||||
const content = cert?.attributes?.certificateContent;
|
||||
if (!id || !content) {
|
||||
throw new Error(`ASC response did not include certificate id/content: ${JSON.stringify(created).slice(0, 2000)}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const certificatePath = outFile ?? path.join(outDir, certificateFileName(certificateType, id));
|
||||
fs.writeFileSync(certificatePath, Buffer.from(content, "base64"));
|
||||
|
||||
const metadata = {
|
||||
id,
|
||||
certificateType: cert?.attributes?.certificateType ?? certificateType,
|
||||
displayName: cert?.attributes?.displayName,
|
||||
name: cert?.attributes?.name,
|
||||
platform: cert?.attributes?.platform,
|
||||
expirationDate: cert?.attributes?.expirationDate,
|
||||
serialNumber: cert?.attributes?.serialNumber,
|
||||
certificatePath,
|
||||
};
|
||||
if (metadataFile) {
|
||||
fs.writeFileSync(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
249
Scripts/apple/google-kms-csr.py
Executable file
249
Scripts/apple/google-kms-csr.py
Executable file
|
|
@ -0,0 +1,249 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build an Apple-compatible PKCS#10 CSR backed by Google Cloud KMS.
|
||||
|
||||
The script intentionally implements only the RSA/SHA-256 subset Burrow needs for
|
||||
Apple Developer ID and iOS Distribution certificate requests. It fetches the KMS
|
||||
public key, builds CertificationRequestInfo locally, asks KMS to sign that DER
|
||||
payload, and writes a standard PEM CSR.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_PROJECT = "project-88c23ce9-918a-470a-b33"
|
||||
DEFAULT_LOCATION = "global"
|
||||
DEFAULT_KEYRING = "burrow-identity"
|
||||
DEFAULT_KEY = "apple-developer-id-application"
|
||||
DEFAULT_VERSION = "1"
|
||||
DEFAULT_COMMON_NAME = "Burrow Developer ID Application"
|
||||
DEFAULT_EMAIL = "release@burrow.net"
|
||||
|
||||
|
||||
def der_length(length: int) -> bytes:
|
||||
if length < 0:
|
||||
raise ValueError("negative DER length")
|
||||
if length < 0x80:
|
||||
return bytes([length])
|
||||
encoded = length.to_bytes((length.bit_length() + 7) // 8, "big")
|
||||
return bytes([0x80 | len(encoded)]) + encoded
|
||||
|
||||
|
||||
def der(tag: int, body: bytes) -> bytes:
|
||||
return bytes([tag]) + der_length(len(body)) + body
|
||||
|
||||
|
||||
def der_sequence(*items: bytes) -> bytes:
|
||||
return der(0x30, b"".join(items))
|
||||
|
||||
|
||||
def der_set(*items: bytes) -> bytes:
|
||||
return der(0x31, b"".join(items))
|
||||
|
||||
|
||||
def der_integer(value: int) -> bytes:
|
||||
if value < 0:
|
||||
raise ValueError("negative INTEGER is not supported")
|
||||
raw = value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big")
|
||||
if raw[0] & 0x80:
|
||||
raw = b"\x00" + raw
|
||||
return der(0x02, raw)
|
||||
|
||||
|
||||
def der_null() -> bytes:
|
||||
return der(0x05, b"")
|
||||
|
||||
|
||||
def der_bit_string(value: bytes) -> bytes:
|
||||
return der(0x03, b"\x00" + value)
|
||||
|
||||
|
||||
def der_utf8(value: str) -> bytes:
|
||||
return der(0x0C, value.encode("utf-8"))
|
||||
|
||||
|
||||
def der_printable(value: str) -> bytes:
|
||||
return der(0x13, value.encode("ascii"))
|
||||
|
||||
|
||||
def der_ia5(value: str) -> bytes:
|
||||
return der(0x16, value.encode("ascii"))
|
||||
|
||||
|
||||
def der_oid(dotted: str) -> bytes:
|
||||
parts = [int(part) for part in dotted.split(".")]
|
||||
if len(parts) < 2 or parts[0] > 2 or parts[1] > 39:
|
||||
raise ValueError(f"unsupported OID: {dotted}")
|
||||
body = bytes([40 * parts[0] + parts[1]])
|
||||
for value in parts[2:]:
|
||||
if value < 0:
|
||||
raise ValueError(f"negative OID component: {dotted}")
|
||||
encoded = [value & 0x7F]
|
||||
value >>= 7
|
||||
while value:
|
||||
encoded.append(0x80 | (value & 0x7F))
|
||||
value >>= 7
|
||||
body += bytes(reversed(encoded))
|
||||
return der(0x06, body)
|
||||
|
||||
|
||||
def name_attribute(oid: str, value_der: bytes) -> bytes:
|
||||
return der_set(der_sequence(der_oid(oid), value_der))
|
||||
|
||||
|
||||
def subject_name(common_name: str, email_address: str, country: str | None) -> bytes:
|
||||
attrs: list[bytes] = []
|
||||
if country:
|
||||
if len(country) != 2 or not country.isalpha():
|
||||
raise ValueError("--country must be a two-letter ISO country code")
|
||||
attrs.append(name_attribute("2.5.4.6", der_printable(country.upper())))
|
||||
if email_address:
|
||||
attrs.append(name_attribute("1.2.840.113549.1.9.1", der_ia5(email_address)))
|
||||
attrs.append(name_attribute("2.5.4.3", der_utf8(common_name)))
|
||||
return der_sequence(*attrs)
|
||||
|
||||
|
||||
def pem_to_der(pem: bytes) -> bytes:
|
||||
lines = [
|
||||
line.strip()
|
||||
for line in pem.splitlines()
|
||||
if line and not line.startswith(b"-----")
|
||||
]
|
||||
if not lines:
|
||||
raise ValueError("PEM input did not contain base64 data")
|
||||
return base64.b64decode(b"".join(lines), validate=True)
|
||||
|
||||
|
||||
def pem_wrap(label: str, body: bytes) -> str:
|
||||
encoded = base64.b64encode(body).decode("ascii")
|
||||
lines = [f"-----BEGIN {label}-----"]
|
||||
lines.extend(encoded[i : i + 64] for i in range(0, len(encoded), 64))
|
||||
lines.append(f"-----END {label}-----")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def run(command: list[str]) -> None:
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def fetch_public_key(args: argparse.Namespace, output: Path) -> None:
|
||||
command = [
|
||||
args.gcloud,
|
||||
"kms",
|
||||
"keys",
|
||||
"versions",
|
||||
"get-public-key",
|
||||
args.kms_version,
|
||||
"--location",
|
||||
args.kms_location,
|
||||
"--keyring",
|
||||
args.kms_keyring,
|
||||
"--key",
|
||||
args.kms_key,
|
||||
"--output-file",
|
||||
str(output),
|
||||
]
|
||||
if args.gcloud_project:
|
||||
command.extend(["--project", args.gcloud_project])
|
||||
run(command)
|
||||
|
||||
|
||||
def kms_sign(args: argparse.Namespace, payload: bytes) -> bytes:
|
||||
with tempfile.TemporaryDirectory(prefix="burrow-kms-csr.") as tmp:
|
||||
payload_path = Path(tmp) / "certification-request-info.der"
|
||||
signature_path = Path(tmp) / "signature.b64"
|
||||
payload_path.write_bytes(payload)
|
||||
command = [
|
||||
args.gcloud,
|
||||
"kms",
|
||||
"asymmetric-sign",
|
||||
"--location",
|
||||
args.kms_location,
|
||||
"--keyring",
|
||||
args.kms_keyring,
|
||||
"--key",
|
||||
args.kms_key,
|
||||
"--version",
|
||||
args.kms_version,
|
||||
"--digest-algorithm",
|
||||
"sha256",
|
||||
"--input-file",
|
||||
str(payload_path),
|
||||
"--signature-file",
|
||||
str(signature_path),
|
||||
]
|
||||
if args.gcloud_project:
|
||||
command.extend(["--project", args.gcloud_project])
|
||||
run(command)
|
||||
signature = signature_path.read_bytes()
|
||||
try:
|
||||
return base64.b64decode(signature.strip(), validate=True)
|
||||
except ValueError:
|
||||
return signature
|
||||
|
||||
|
||||
def build_csr(args: argparse.Namespace, spki_der: bytes) -> bytes:
|
||||
cri = der_sequence(
|
||||
der_integer(0),
|
||||
subject_name(args.common_name, args.email_address, args.country),
|
||||
spki_der,
|
||||
b"\xA0\x00",
|
||||
)
|
||||
signature = kms_sign(args, cri)
|
||||
signature_algorithm = der_sequence(
|
||||
der_oid("1.2.840.113549.1.1.11"),
|
||||
der_null(),
|
||||
)
|
||||
return der_sequence(cri, signature_algorithm, der_bit_string(signature))
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate an Apple-compatible CSR backed by a Google Cloud KMS RSA signing key.",
|
||||
)
|
||||
parser.add_argument("--gcloud-project", default=os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("BURROW_GOOGLE_PROJECT_ID") or DEFAULT_PROJECT)
|
||||
parser.add_argument("--kms-location", default=os.environ.get("BURROW_KMS_LOCATION", DEFAULT_LOCATION))
|
||||
parser.add_argument("--kms-keyring", default=os.environ.get("BURROW_KMS_KEYRING", DEFAULT_KEYRING))
|
||||
parser.add_argument("--kms-key", default=os.environ.get("BURROW_APPLE_KMS_KEY") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_KMS_KEY", DEFAULT_KEY))
|
||||
parser.add_argument("--kms-version", default=os.environ.get("BURROW_KMS_VERSION", DEFAULT_VERSION))
|
||||
parser.add_argument("--common-name", default=os.environ.get("BURROW_APPLE_CSR_COMMON_NAME") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_CSR_COMMON_NAME", DEFAULT_COMMON_NAME))
|
||||
parser.add_argument("--email-address", default=os.environ.get("BURROW_APPLE_CSR_EMAIL") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_CSR_EMAIL", DEFAULT_EMAIL))
|
||||
parser.add_argument("--country", default=os.environ.get("BURROW_APPLE_CSR_COUNTRY") or os.environ.get("BURROW_APPLE_DEVELOPER_ID_CSR_COUNTRY", "US"))
|
||||
parser.add_argument("--public-key-pem", help="Existing KMS public-key PEM path. If omitted, gcloud fetches it.")
|
||||
parser.add_argument("--public-key-output", help="Write the fetched/used public key PEM to this path.")
|
||||
parser.add_argument("--output", default="developer-id-application.certSigningRequest")
|
||||
parser.add_argument("--gcloud", default=os.environ.get("GCLOUD", "gcloud"))
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
output = Path(args.output)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="burrow-kms-csr.") as tmp:
|
||||
public_key_path = Path(args.public_key_pem) if args.public_key_pem else Path(tmp) / "kms-public.pem"
|
||||
if not args.public_key_pem:
|
||||
fetch_public_key(args, public_key_path)
|
||||
public_key_pem = public_key_path.read_bytes()
|
||||
spki_der = pem_to_der(public_key_pem)
|
||||
csr = build_csr(args, spki_der)
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(pem_wrap("CERTIFICATE REQUEST", csr), encoding="ascii")
|
||||
if args.public_key_output:
|
||||
public_output = Path(args.public_key_output)
|
||||
public_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
public_output.write_bytes(public_key_pem)
|
||||
print(f"wrote {output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
327
Scripts/apple/sync-provisioning-profiles.mjs
Executable file
327
Scripts/apple/sync-provisioning-profiles.mjs
Executable file
|
|
@ -0,0 +1,327 @@
|
|||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
const crypto = globalThis.crypto ?? webcrypto;
|
||||
|
||||
function b64url(input) {
|
||||
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
||||
return buf
|
||||
.toString("base64")
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { _: [] };
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--help" || arg === "-h") out.help = true;
|
||||
else if (arg.startsWith("--")) {
|
||||
const key = arg.slice(2);
|
||||
const value = argv[i + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`missing value for --${key}`);
|
||||
out[key] = value;
|
||||
i++;
|
||||
} else {
|
||||
out._.push(arg);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function makeJwt({ keyId, issuerId, keyPem }) {
|
||||
const header = { alg: "ES256", kid: keyId, typ: "JWT" };
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = { iss: issuerId, exp: now + 20 * 60, aud: "appstoreconnect-v1" };
|
||||
const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
|
||||
const pkcs8Der = Buffer.from(
|
||||
keyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s+/g, ""),
|
||||
"base64",
|
||||
);
|
||||
const key = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
pkcs8Der,
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
const sig = await crypto.subtle.sign(
|
||||
{ name: "ECDSA", hash: "SHA-256" },
|
||||
key,
|
||||
new TextEncoder().encode(input),
|
||||
);
|
||||
return `${input}.${b64url(Buffer.from(sig))}`;
|
||||
}
|
||||
|
||||
async function ascFetch(jwt, url, opts = {}) {
|
||||
const res = await fetch(url, {
|
||||
...opts,
|
||||
headers: {
|
||||
...(opts.headers ?? {}),
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const text = await res.text();
|
||||
let body;
|
||||
try {
|
||||
body = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
body = { raw: text };
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`ASC ${res.status} ${url}: ${JSON.stringify(body).slice(0, 2000)}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function truthy(value) {
|
||||
return ["1", "true", "yes", "on"].includes(String(value ?? "").toLowerCase());
|
||||
}
|
||||
|
||||
function safeFileName(identifier, suffix) {
|
||||
return `${identifier.replace(/[^A-Za-z0-9]/g, "").toLowerCase()}${suffix}`;
|
||||
}
|
||||
|
||||
function bundleDisplayName(identifier, appId) {
|
||||
return identifier === appId ? "Burrow App" : "Burrow Network Extension";
|
||||
}
|
||||
|
||||
async function findCertificate(jwt, preferredTypes, preferredId) {
|
||||
const certs = await ascFetch(
|
||||
jwt,
|
||||
"https://api.appstoreconnect.apple.com/v1/certificates?limit=200",
|
||||
);
|
||||
const data = certs.data ?? [];
|
||||
if (preferredId) {
|
||||
const cert = data.find((item) => item?.id === preferredId);
|
||||
if (!cert) throw new Error(`unable to locate certificate id ${preferredId} via ASC API`);
|
||||
const type = cert?.attributes?.certificateType;
|
||||
if (preferredTypes.length && !preferredTypes.includes(type)) {
|
||||
throw new Error(`certificate ${preferredId} has type ${type}, expected ${preferredTypes.join(" or ")}`);
|
||||
}
|
||||
return cert;
|
||||
}
|
||||
for (const type of preferredTypes) {
|
||||
const cert = data.find((item) => item?.attributes?.certificateType === type);
|
||||
if (cert) return cert;
|
||||
}
|
||||
throw new Error(`unable to locate certificate type ${preferredTypes.join(" or ")} via ASC API`);
|
||||
}
|
||||
|
||||
async function resolveBundle(jwt, target, { createMissing, platform }) {
|
||||
const bundleResp = await ascFetch(
|
||||
jwt,
|
||||
`https://api.appstoreconnect.apple.com/v1/bundleIds?limit=5&filter[identifier]=${encodeURIComponent(target.identifier)}`,
|
||||
);
|
||||
let bundle = (bundleResp.data ?? []).find(
|
||||
(item) => item?.attributes?.identifier === target.identifier,
|
||||
);
|
||||
if (bundle || !createMissing) return bundle;
|
||||
|
||||
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/bundleIds", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "bundleIds",
|
||||
attributes: {
|
||||
identifier: target.identifier,
|
||||
name: target.bundleName ?? target.name,
|
||||
platform,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
bundle = created?.data;
|
||||
if (bundle?.id) {
|
||||
process.stdout.write(`created bundle id ${target.identifier} (${platform})\n`);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
async function listCapabilityTypes(jwt, bundleId) {
|
||||
const resp = await ascFetch(
|
||||
jwt,
|
||||
`https://api.appstoreconnect.apple.com/v1/bundleIds/${bundleId}/bundleIdCapabilities`,
|
||||
);
|
||||
return new Set((resp.data ?? []).map((item) => item?.attributes?.capabilityType).filter(Boolean));
|
||||
}
|
||||
|
||||
async function enableCapability(jwt, bundleId, capabilityType, settings) {
|
||||
await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "bundleIdCapabilities",
|
||||
attributes: {
|
||||
capabilityType,
|
||||
...(settings ? { settings } : {}),
|
||||
},
|
||||
relationships: {
|
||||
bundleId: {
|
||||
data: {
|
||||
id: bundleId,
|
||||
type: "bundleIds",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function profileIncludesCertificate(jwt, profileId, certificateId) {
|
||||
const resp = await ascFetch(
|
||||
jwt,
|
||||
`https://api.appstoreconnect.apple.com/v1/profiles/${profileId}/certificates?limit=200`,
|
||||
);
|
||||
return (resp.data ?? []).some((item) => item?.id === certificateId);
|
||||
}
|
||||
|
||||
async function ensureCapabilities(jwt, bundle, capabilities) {
|
||||
const existing = await listCapabilityTypes(jwt, bundle.id);
|
||||
for (const capability of capabilities) {
|
||||
if (existing.has(capability.type)) continue;
|
||||
try {
|
||||
await enableCapability(jwt, bundle.id, capability.type, capability.settings);
|
||||
process.stdout.write(`enabled ${capability.type} for ${bundle.attributes.identifier}\n`);
|
||||
} catch (err) {
|
||||
process.stderr.write(`warning: could not enable ${capability.type} for ${bundle.attributes.identifier}: ${err.message}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function syncTarget(jwt, cert, profileType, target, outDir, options) {
|
||||
const bundle = await resolveBundle(jwt, target, options);
|
||||
if (!bundle) throw new Error(`bundleId not found in ASC: ${target.identifier}`);
|
||||
|
||||
if (options.enableCapabilities && target.capabilities?.length) {
|
||||
await ensureCapabilities(jwt, bundle, target.capabilities);
|
||||
}
|
||||
|
||||
const existing = await ascFetch(
|
||||
jwt,
|
||||
`https://api.appstoreconnect.apple.com/v1/profiles?limit=10&filter[profileType]=${encodeURIComponent(profileType)}&filter[name]=${encodeURIComponent(target.name)}`,
|
||||
);
|
||||
let profileId = (existing.data ?? []).find((item) => item?.attributes?.name === target.name)?.id;
|
||||
if (profileId && options.replaceCertificateMismatch) {
|
||||
const includesCertificate = await profileIncludesCertificate(jwt, profileId, cert.id);
|
||||
if (!includesCertificate) {
|
||||
await ascFetch(jwt, `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
process.stdout.write(`deleted stale profile ${target.name}; it did not include certificate ${cert.id}\n`);
|
||||
profileId = undefined;
|
||||
}
|
||||
}
|
||||
if (!profileId) {
|
||||
const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/profiles", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "profiles",
|
||||
attributes: { name: target.name, profileType },
|
||||
relationships: {
|
||||
bundleId: { data: { type: "bundleIds", id: bundle.id } },
|
||||
certificates: { data: [{ type: "certificates", id: cert.id }] },
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
profileId = created?.data?.id;
|
||||
}
|
||||
if (!profileId) throw new Error(`failed to resolve/create profile for ${target.identifier}`);
|
||||
|
||||
const profile = await ascFetch(jwt, `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}`);
|
||||
const content = profile?.data?.attributes?.profileContent;
|
||||
if (!content) throw new Error(`profileContent missing for ${target.name} (${profileId})`);
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const profileBytes = Buffer.from(content, "base64");
|
||||
const profilePath = path.join(outDir, target.file);
|
||||
fs.writeFileSync(profilePath, profileBytes);
|
||||
if (target.mobileFile) {
|
||||
fs.writeFileSync(path.join(outDir, target.mobileFile), profileBytes);
|
||||
}
|
||||
process.stdout.write(`synced ${target.identifier} -> ${profilePath}\n`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
if (args.help) {
|
||||
console.log("Usage: sync-provisioning-profiles.mjs --platform ios|macos|all --key-id ID --issuer-id ID --key-file AuthKey.p8 --out-dir DIR [--ios-certificate-id ID] [--macos-certificate-id ID] [--replace-certificate-mismatch true]");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const platform = args.platform ?? "all";
|
||||
const keyId = args["key-id"];
|
||||
const issuerId = args["issuer-id"];
|
||||
const keyFile = args["key-file"];
|
||||
const outDir = args["out-dir"] ?? ".forgejo/actions/export";
|
||||
const appId = args["app-id"] ?? "com.hackclub.burrow";
|
||||
const networkId = args["network-id"] ?? `${appId}.network`;
|
||||
const iosCertificateId = args["ios-certificate-id"] ?? process.env.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID;
|
||||
const macosCertificateId = args["macos-certificate-id"] ?? process.env.BURROW_DEVELOPER_ID_CERTIFICATE_ID;
|
||||
const createMissing = truthy(args["create-missing-bundle-ids"]);
|
||||
const enableCapabilities = createMissing || truthy(args["enable-capabilities"]);
|
||||
const replaceCertificateMismatch = truthy(args["replace-certificate-mismatch"]);
|
||||
const bundlePlatform = args["bundle-platform"] ?? "UNIVERSAL";
|
||||
const associatedDomains = (args["associated-domains"] ?? "applinks:burrow.rs?mode=developer,webcredentials:burrow.rs?mode=developer")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
const appCapabilities = [
|
||||
{ type: "NETWORK_EXTENSIONS" },
|
||||
{ type: "APP_GROUPS" },
|
||||
{
|
||||
type: "ASSOCIATED_DOMAINS",
|
||||
settings: [{ key: "ASSOCIATED_DOMAIN_IDS", options: associatedDomains.map((key) => ({ key, enabled: true })) }],
|
||||
},
|
||||
];
|
||||
const extensionCapabilities = [
|
||||
{ type: "NETWORK_EXTENSIONS" },
|
||||
{ type: "APP_GROUPS" },
|
||||
];
|
||||
const options = { createMissing, enableCapabilities, replaceCertificateMismatch, platform: bundlePlatform };
|
||||
|
||||
if (!keyId || !issuerId || !keyFile) throw new Error("required: --key-id --issuer-id --key-file");
|
||||
|
||||
const jwt = await makeJwt({ keyId, issuerId, keyPem: fs.readFileSync(keyFile, "utf8") });
|
||||
|
||||
if (platform === "ios" || platform === "all") {
|
||||
const cert = await findCertificate(jwt, ["IOS_DISTRIBUTION", "DISTRIBUTION", "APPLE_DISTRIBUTION"], iosCertificateId);
|
||||
const targets = [appId, networkId].map((identifier) => ({
|
||||
identifier,
|
||||
name: `${identifier}.app-store`,
|
||||
bundleName: bundleDisplayName(identifier, appId),
|
||||
file: safeFileName(identifier, "appstore.provisionprofile"),
|
||||
mobileFile: safeFileName(identifier, "appstore.mobileprovision"),
|
||||
capabilities: identifier === appId ? appCapabilities : extensionCapabilities,
|
||||
}));
|
||||
for (const target of targets) {
|
||||
await syncTarget(jwt, cert, "IOS_APP_STORE", target, outDir, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "macos" || platform === "all") {
|
||||
const cert = await findCertificate(jwt, ["DEVELOPER_ID_APPLICATION"], macosCertificateId);
|
||||
const targets = [appId, networkId].map((identifier) => ({
|
||||
identifier,
|
||||
name: `${identifier}.developer-id`,
|
||||
bundleName: bundleDisplayName(identifier, appId),
|
||||
file: safeFileName(identifier, "developerid.provisionprofile"),
|
||||
capabilities: identifier === appId ? appCapabilities : extensionCapabilities,
|
||||
}));
|
||||
for (const target of targets) {
|
||||
await syncTarget(jwt, cert, "MAC_APP_DIRECT", target, outDir, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@ bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
|||
directory_json="${AUTHENTIK_BURROW_DIRECTORY_JSON:-[]}"
|
||||
users_group="${AUTHENTIK_BURROW_USERS_GROUP:-burrow-users}"
|
||||
admins_group="${AUTHENTIK_BURROW_ADMINS_GROUP:-burrow-admins}"
|
||||
extra_groups_json="${AUTHENTIK_BURROW_EXTRA_GROUPS_JSON:-[]}"
|
||||
forgejo_application_slug="${AUTHENTIK_FORGEJO_APPLICATION_SLUG:-}"
|
||||
|
||||
usage() {
|
||||
|
|
@ -20,6 +21,7 @@ Optional environment:
|
|||
AUTHENTIK_URL
|
||||
AUTHENTIK_BURROW_USERS_GROUP
|
||||
AUTHENTIK_BURROW_ADMINS_GROUP
|
||||
AUTHENTIK_BURROW_EXTRA_GROUPS_JSON
|
||||
AUTHENTIK_FORGEJO_APPLICATION_SLUG
|
||||
EOF
|
||||
}
|
||||
|
|
@ -39,6 +41,11 @@ if ! printf '%s' "$directory_json" | jq -e 'type == "array"' >/dev/null; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$extra_groups_json" | jq -e 'type == "array"' >/dev/null; then
|
||||
echo "error: AUTHENTIK_BURROW_EXTRA_GROUPS_JSON must be a JSON array" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
|
|
@ -251,6 +258,9 @@ ensure_application_group_binding() {
|
|||
wait_for_authentik
|
||||
ensure_group "$users_group" >/dev/null
|
||||
ensure_group "$admins_group" >/dev/null
|
||||
while IFS= read -r group_name; do
|
||||
ensure_group "$group_name" >/dev/null
|
||||
done < <(printf '%s\n' "$extra_groups_json" | jq -r '.[]')
|
||||
|
||||
while IFS= read -r user_spec; do
|
||||
ensure_user "$user_spec"
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ client_secret="${AUTHENTIK_FORGEJO_CLIENT_SECRET:-}"
|
|||
launch_url="${AUTHENTIK_FORGEJO_LAUNCH_URL:-https://git.burrow.net/}"
|
||||
redirect_uris_json="${AUTHENTIK_FORGEJO_REDIRECT_URIS_JSON:-[
|
||||
\"https://git.burrow.net/user/oauth2/burrow.net/callback\",
|
||||
\"https://git.burrow.net/user/oauth2/authentik/callback\",
|
||||
\"https://git.burrow.net/user/oauth2/GitHub/callback\"
|
||||
\"https://git.burrow.net/user/oauth2/authentik/callback\"
|
||||
]}"
|
||||
|
||||
usage() {
|
||||
|
|
|
|||
285
Scripts/authentik-sync-google-wif-oidc.sh
Executable file
285
Scripts/authentik-sync-google-wif-oidc.sh
Executable file
|
|
@ -0,0 +1,285 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}"
|
||||
bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
||||
application_slug="${AUTHENTIK_GOOGLE_WIF_APPLICATION_SLUG:-google-cloud}"
|
||||
application_name="${AUTHENTIK_GOOGLE_WIF_APPLICATION_NAME:-Google Cloud WIF}"
|
||||
provider_name="${AUTHENTIK_GOOGLE_WIF_PROVIDER_NAME:-Google Cloud WIF}"
|
||||
template_slug="${AUTHENTIK_GOOGLE_WIF_TEMPLATE_SLUG:-ts}"
|
||||
client_id="${AUTHENTIK_GOOGLE_WIF_CLIENT_ID:-google-wif.burrow.net}"
|
||||
client_secret="${AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET:-}"
|
||||
launch_url="${AUTHENTIK_GOOGLE_WIF_LAUNCH_URL:-https://console.cloud.google.com/}"
|
||||
redirect_uris_json="${AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON:-[]}"
|
||||
access_group="${AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP:-burrow-automation}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/authentik-sync-google-wif-oidc.sh
|
||||
|
||||
Reconciles the Authentik OIDC provider used as the Google Workload Identity
|
||||
Federation issuer for Burrow release runners.
|
||||
|
||||
Required environment:
|
||||
AUTHENTIK_BOOTSTRAP_TOKEN
|
||||
AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET
|
||||
|
||||
Optional environment:
|
||||
AUTHENTIK_URL
|
||||
AUTHENTIK_GOOGLE_WIF_APPLICATION_SLUG
|
||||
AUTHENTIK_GOOGLE_WIF_APPLICATION_NAME
|
||||
AUTHENTIK_GOOGLE_WIF_PROVIDER_NAME
|
||||
AUTHENTIK_GOOGLE_WIF_TEMPLATE_SLUG
|
||||
AUTHENTIK_GOOGLE_WIF_CLIENT_ID
|
||||
AUTHENTIK_GOOGLE_WIF_LAUNCH_URL
|
||||
AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON
|
||||
AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$bootstrap_token" ]]; then
|
||||
echo "error: AUTHENTIK_BOOTSTRAP_TOKEN is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$client_secret" || "$client_secret" == PENDING* ]]; then
|
||||
echo "Google WIF OIDC client secret is not configured; skipping Authentik Google WIF sync." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$redirect_uris_json" | jq -e 'type == "array"' >/dev/null; then
|
||||
echo "error: AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON must be a JSON array" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local data="${3:-}"
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
curl -fsS \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"${authentik_url}${path}"
|
||||
else
|
||||
curl -fsS \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
"${authentik_url}${path}"
|
||||
fi
|
||||
}
|
||||
|
||||
api_with_status() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local data="${3:-}"
|
||||
local response_file status
|
||||
|
||||
response_file="$(mktemp)"
|
||||
trap 'rm -f "$response_file"' RETURN
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
status="$(
|
||||
curl -sS \
|
||||
-o "$response_file" \
|
||||
-w '%{http_code}' \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"${authentik_url}${path}"
|
||||
)"
|
||||
else
|
||||
status="$(
|
||||
curl -sS \
|
||||
-o "$response_file" \
|
||||
-w '%{http_code}' \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
"${authentik_url}${path}"
|
||||
)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$status"
|
||||
cat "$response_file"
|
||||
}
|
||||
|
||||
wait_for_authentik() {
|
||||
for _ in $(seq 1 90); do
|
||||
if curl -fsS "${authentik_url}/-/health/ready/" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "error: Authentik did not become ready at ${authentik_url}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_authentik
|
||||
|
||||
template_provider="$(
|
||||
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
||||
| jq -c --arg slug "$template_slug" '.results[]? | select(.assigned_application_slug == $slug)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -z "$template_provider" ]]; then
|
||||
echo "error: could not resolve Authentik OAuth provider template ${template_slug}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
authorization_flow="$(printf '%s\n' "$template_provider" | jq -r '.authorization_flow')"
|
||||
invalidation_flow="$(printf '%s\n' "$template_provider" | jq -r '.invalidation_flow')"
|
||||
property_mappings="$(printf '%s\n' "$template_provider" | jq -c '.property_mappings')"
|
||||
signing_key="$(printf '%s\n' "$template_provider" | jq -r '.signing_key')"
|
||||
|
||||
provider_payload="$(
|
||||
jq -n \
|
||||
--arg name "$provider_name" \
|
||||
--arg authorization_flow "$authorization_flow" \
|
||||
--arg invalidation_flow "$invalidation_flow" \
|
||||
--arg client_id "$client_id" \
|
||||
--arg client_secret "$client_secret" \
|
||||
--arg signing_key "$signing_key" \
|
||||
--argjson property_mappings "$property_mappings" \
|
||||
--argjson redirect_uris "$redirect_uris_json" \
|
||||
'{
|
||||
name: $name,
|
||||
authorization_flow: $authorization_flow,
|
||||
invalidation_flow: $invalidation_flow,
|
||||
client_type: "confidential",
|
||||
grant_types: ["client_credentials"],
|
||||
client_id: $client_id,
|
||||
client_secret: $client_secret,
|
||||
include_claims_in_id_token: true,
|
||||
redirect_uris: ($redirect_uris | map({matching_mode: "strict", url: .})),
|
||||
property_mappings: $property_mappings,
|
||||
signing_key: $signing_key,
|
||||
issuer_mode: "per_provider",
|
||||
sub_mode: "hashed_user_id"
|
||||
}'
|
||||
)"
|
||||
|
||||
existing_provider="$(
|
||||
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
||||
| jq -c \
|
||||
--arg application_slug "$application_slug" \
|
||||
--arg provider_name "$provider_name" \
|
||||
'.results[]? | select(.assigned_application_slug == $application_slug or .name == $provider_name)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -n "$existing_provider" ]]; then
|
||||
provider_pk="$(printf '%s\n' "$existing_provider" | jq -r '.pk')"
|
||||
api PATCH "/api/v3/providers/oauth2/${provider_pk}/" "$provider_payload" >/dev/null
|
||||
else
|
||||
provider_pk="$(
|
||||
api POST "/api/v3/providers/oauth2/" "$provider_payload" \
|
||||
| jq -r '.pk // empty'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [[ -z "${provider_pk:-}" ]]; then
|
||||
echo "error: Google WIF OIDC provider did not return a primary key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
application_payload="$(
|
||||
jq -n \
|
||||
--arg name "$application_name" \
|
||||
--arg slug "$application_slug" \
|
||||
--arg provider "$provider_pk" \
|
||||
--arg launch_url "$launch_url" \
|
||||
'{
|
||||
name: $name,
|
||||
slug: $slug,
|
||||
provider: ($provider | tonumber),
|
||||
meta_launch_url: $launch_url,
|
||||
open_in_new_tab: false,
|
||||
policy_engine_mode: "any"
|
||||
}'
|
||||
)"
|
||||
|
||||
existing_application="$(
|
||||
api GET "/api/v3/core/applications/?page_size=200" \
|
||||
| jq -c --arg slug "$application_slug" '.results[]? | select(.slug == $slug)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -n "$existing_application" ]]; then
|
||||
application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')"
|
||||
api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null
|
||||
else
|
||||
create_application_result="$(
|
||||
api_with_status POST "/api/v3/core/applications/" "$application_payload"
|
||||
)"
|
||||
create_application_status="$(printf '%s\n' "$create_application_result" | sed -n '1p')"
|
||||
create_application_body="$(printf '%s\n' "$create_application_result" | sed '1d')"
|
||||
|
||||
if [[ "$create_application_status" =~ ^20[01]$ ]]; then
|
||||
application_pk="$(printf '%s\n' "$create_application_body" | jq -r '.pk // empty')"
|
||||
elif [[ "$create_application_status" == "400" ]] && printf '%s\n' "$create_application_body" | jq -e '
|
||||
(.slug // [] | index("Application with this slug already exists.")) != null
|
||||
or (.provider // [] | index("Application with this provider already exists.")) != null
|
||||
' >/dev/null; then
|
||||
application_pk="$(
|
||||
api GET "/api/v3/core/applications/?page_size=200" \
|
||||
| jq -r --arg slug "$application_slug" '.results[]? | select(.slug == $slug) | .pk // empty' \
|
||||
| head -n1
|
||||
)"
|
||||
else
|
||||
printf '%s\n' "$create_application_body" >&2
|
||||
echo "error: could not reconcile Authentik application ${application_slug}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${application_pk:-}" ]]; then
|
||||
echo "error: Google WIF OIDC application did not return a primary key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$access_group" ]]; then
|
||||
group_pk="$(
|
||||
api GET "/api/v3/core/groups/?page_size=200" \
|
||||
| jq -r --arg name "$access_group" '.results[]? | select(.name == $name) | .pk' \
|
||||
| head -n1
|
||||
)"
|
||||
if [[ -z "$group_pk" ]]; then
|
||||
echo "warning: Authentik Google WIF access group ${access_group} was not found; application policy left unchanged." >&2
|
||||
else
|
||||
api POST "/api/v3/policies/bindings/" "$(
|
||||
jq -n \
|
||||
--arg target "$application_pk" \
|
||||
--arg group "$group_pk" \
|
||||
'{
|
||||
target: $target,
|
||||
group: $group,
|
||||
negate: false,
|
||||
order: 0,
|
||||
enabled: true,
|
||||
timeout: 30,
|
||||
failure_result: false
|
||||
}'
|
||||
)" >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS "${authentik_url}/application/o/${application_slug}/.well-known/openid-configuration" >/dev/null 2>&1; then
|
||||
echo "Synced Authentik Google WIF OIDC application ${application_slug} (${application_name})."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "warning: Google WIF OIDC issuer document for ${application_slug} was not immediately readable; keeping reconciled config." >&2
|
||||
echo "Synced Authentik Google WIF OIDC application ${application_slug} (${application_name})."
|
||||
332
Scripts/authentik-sync-grafana-oidc.sh
Executable file
332
Scripts/authentik-sync-grafana-oidc.sh
Executable file
|
|
@ -0,0 +1,332 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
authentik_url="${AUTHENTIK_URL:-https://auth.burrow.net}"
|
||||
bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
||||
application_slug="${AUTHENTIK_GRAFANA_APPLICATION_SLUG:-grafana}"
|
||||
application_name="${AUTHENTIK_GRAFANA_APPLICATION_NAME:-Burrow Grafana}"
|
||||
provider_name="${AUTHENTIK_GRAFANA_PROVIDER_NAME:-Burrow Grafana}"
|
||||
template_slug="${AUTHENTIK_GRAFANA_TEMPLATE_SLUG:-ts}"
|
||||
client_id="${AUTHENTIK_GRAFANA_CLIENT_ID:-graphs.burrow.net}"
|
||||
client_secret="${AUTHENTIK_GRAFANA_CLIENT_SECRET:-}"
|
||||
launch_url="${AUTHENTIK_GRAFANA_LAUNCH_URL:-https://graphs.burrow.net/}"
|
||||
access_group="${AUTHENTIK_GRAFANA_ACCESS_GROUP:-}"
|
||||
redirect_uris_json="${AUTHENTIK_GRAFANA_REDIRECT_URIS_JSON:-[
|
||||
\"https://graphs.burrow.net/login/generic_oauth\"
|
||||
]}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/authentik-sync-grafana-oidc.sh
|
||||
|
||||
Required environment:
|
||||
AUTHENTIK_BOOTSTRAP_TOKEN
|
||||
AUTHENTIK_GRAFANA_CLIENT_SECRET
|
||||
|
||||
Optional environment:
|
||||
AUTHENTIK_URL
|
||||
AUTHENTIK_GRAFANA_APPLICATION_SLUG
|
||||
AUTHENTIK_GRAFANA_APPLICATION_NAME
|
||||
AUTHENTIK_GRAFANA_PROVIDER_NAME
|
||||
AUTHENTIK_GRAFANA_TEMPLATE_SLUG
|
||||
AUTHENTIK_GRAFANA_CLIENT_ID
|
||||
AUTHENTIK_GRAFANA_LAUNCH_URL
|
||||
AUTHENTIK_GRAFANA_REDIRECT_URIS_JSON
|
||||
AUTHENTIK_GRAFANA_ACCESS_GROUP
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$bootstrap_token" ]]; then
|
||||
echo "error: AUTHENTIK_BOOTSTRAP_TOKEN is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$client_secret" || "$client_secret" == PENDING* ]]; then
|
||||
echo "Grafana OIDC client secret is not configured; skipping Authentik Grafana sync." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! printf '%s' "$redirect_uris_json" | jq -e 'type == "array" and length > 0' >/dev/null; then
|
||||
echo "error: AUTHENTIK_GRAFANA_REDIRECT_URIS_JSON must be a non-empty JSON array" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local data="${3:-}"
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
curl -fsS \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"${authentik_url}${path}"
|
||||
else
|
||||
curl -fsS \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
"${authentik_url}${path}"
|
||||
fi
|
||||
}
|
||||
|
||||
api_with_status() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local data="${3:-}"
|
||||
local response_file status
|
||||
|
||||
response_file="$(mktemp)"
|
||||
trap 'rm -f "$response_file"' RETURN
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
status="$(
|
||||
curl -sS \
|
||||
-o "$response_file" \
|
||||
-w '%{http_code}' \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"${authentik_url}${path}"
|
||||
)"
|
||||
else
|
||||
status="$(
|
||||
curl -sS \
|
||||
-o "$response_file" \
|
||||
-w '%{http_code}' \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${bootstrap_token}" \
|
||||
"${authentik_url}${path}"
|
||||
)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$status"
|
||||
cat "$response_file"
|
||||
}
|
||||
|
||||
wait_for_authentik() {
|
||||
for _ in $(seq 1 90); do
|
||||
if curl -fsS "${authentik_url}/-/health/ready/" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "error: Authentik did not become ready at ${authentik_url}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
lookup_group_pk() {
|
||||
local group_name="$1"
|
||||
|
||||
api GET "/api/v3/core/groups/?page_size=200" \
|
||||
| jq -r --arg group_name "$group_name" '.results[]? | select(.name == $group_name) | .pk // empty' \
|
||||
| head -n1
|
||||
}
|
||||
|
||||
lookup_application_pk() {
|
||||
local slug="$1"
|
||||
local application_pk lookup_result lookup_status
|
||||
|
||||
application_pk="$(
|
||||
api GET "/api/v3/core/applications/?page_size=200" \
|
||||
| jq -r --arg slug "$slug" '.results[]? | select(.slug == $slug) | .pk // empty' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -n "$application_pk" ]]; then
|
||||
printf '%s\n' "$application_pk"
|
||||
return 0
|
||||
fi
|
||||
|
||||
lookup_result="$(api_with_status GET "/api/v3/core/applications/${slug}/")"
|
||||
lookup_status="$(printf '%s\n' "$lookup_result" | sed -n '1p')"
|
||||
if [[ "$lookup_status" =~ ^20[01]$ ]]; then
|
||||
printf '%s\n' "$lookup_result" | sed '1d' | jq -r '.pk // empty'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_application_group_binding() {
|
||||
local application_slug="$1"
|
||||
local group_name="$2"
|
||||
local application_pk group_pk existing payload binding_pk
|
||||
|
||||
application_pk="$(lookup_application_pk "$application_slug")"
|
||||
if [[ -z "$application_pk" ]]; then
|
||||
echo "warning: could not resolve Authentik application ${application_slug}; skipping application group binding" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
group_pk="$(lookup_group_pk "$group_name")"
|
||||
if [[ -z "$group_pk" ]]; then
|
||||
echo "error: could not resolve Authentik group ${group_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
existing="$(
|
||||
api GET "/api/v3/policies/bindings/?page_size=200&target=${application_pk}" \
|
||||
| jq -c --arg group_pk "$group_pk" '.results[]? | select(.group == $group_pk)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
payload="$(
|
||||
jq -cn \
|
||||
--arg target "$application_pk" \
|
||||
--arg group "$group_pk" \
|
||||
'{
|
||||
group: $group,
|
||||
target: $target,
|
||||
negate: false,
|
||||
enabled: true,
|
||||
order: 100,
|
||||
timeout: 30,
|
||||
failure_result: false
|
||||
}'
|
||||
)"
|
||||
|
||||
if [[ -n "$existing" ]]; then
|
||||
binding_pk="$(printf '%s\n' "$existing" | jq -r '.pk')"
|
||||
api PATCH "/api/v3/policies/bindings/${binding_pk}/" "$payload" >/dev/null
|
||||
else
|
||||
api POST "/api/v3/policies/bindings/" "$payload" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_authentik
|
||||
|
||||
template_provider="$(
|
||||
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
||||
| jq -c --arg template_slug "$template_slug" '.results[]? | select(.assigned_application_slug == $template_slug)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -z "$template_provider" ]]; then
|
||||
echo "error: could not resolve the Authentik OAuth provider template ${template_slug}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
authorization_flow="$(printf '%s\n' "$template_provider" | jq -r '.authorization_flow')"
|
||||
invalidation_flow="$(printf '%s\n' "$template_provider" | jq -r '.invalidation_flow')"
|
||||
property_mappings="$(printf '%s\n' "$template_provider" | jq -c '.property_mappings')"
|
||||
signing_key="$(printf '%s\n' "$template_provider" | jq -r '.signing_key')"
|
||||
|
||||
provider_payload="$(
|
||||
jq -n \
|
||||
--arg name "$provider_name" \
|
||||
--arg authorization_flow "$authorization_flow" \
|
||||
--arg invalidation_flow "$invalidation_flow" \
|
||||
--arg client_id "$client_id" \
|
||||
--arg client_secret "$client_secret" \
|
||||
--arg signing_key "$signing_key" \
|
||||
--argjson property_mappings "$property_mappings" \
|
||||
--argjson redirect_uris "$redirect_uris_json" \
|
||||
'{
|
||||
name: $name,
|
||||
authorization_flow: $authorization_flow,
|
||||
invalidation_flow: $invalidation_flow,
|
||||
client_type: "confidential",
|
||||
client_id: $client_id,
|
||||
client_secret: $client_secret,
|
||||
include_claims_in_id_token: true,
|
||||
redirect_uris: ($redirect_uris | map({matching_mode: "strict", url: .})),
|
||||
property_mappings: $property_mappings,
|
||||
signing_key: $signing_key,
|
||||
issuer_mode: "per_provider",
|
||||
sub_mode: "hashed_user_id"
|
||||
}'
|
||||
)"
|
||||
|
||||
existing_provider="$(
|
||||
api GET "/api/v3/providers/oauth2/?page_size=200" \
|
||||
| jq -c \
|
||||
--arg application_slug "$application_slug" \
|
||||
--arg provider_name "$provider_name" \
|
||||
'.results[]? | select(.assigned_application_slug == $application_slug or .name == $provider_name)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -n "$existing_provider" ]]; then
|
||||
provider_pk="$(printf '%s\n' "$existing_provider" | jq -r '.pk')"
|
||||
api PATCH "/api/v3/providers/oauth2/${provider_pk}/" "$provider_payload" >/dev/null
|
||||
else
|
||||
provider_pk="$(
|
||||
api POST "/api/v3/providers/oauth2/" "$provider_payload" \
|
||||
| jq -r '.pk // empty'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [[ -z "${provider_pk:-}" ]]; then
|
||||
echo "error: Grafana OIDC provider did not return a primary key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
application_payload="$(
|
||||
jq -n \
|
||||
--arg name "$application_name" \
|
||||
--arg slug "$application_slug" \
|
||||
--arg provider "$provider_pk" \
|
||||
--arg launch_url "$launch_url" \
|
||||
'{
|
||||
name: $name,
|
||||
slug: $slug,
|
||||
provider: ($provider | tonumber),
|
||||
meta_launch_url: $launch_url,
|
||||
open_in_new_tab: false,
|
||||
policy_engine_mode: "any"
|
||||
}'
|
||||
)"
|
||||
|
||||
existing_application="$(
|
||||
api GET "/api/v3/core/applications/?page_size=200" \
|
||||
| jq -c --arg slug "$application_slug" '.results[]? | select(.slug == $slug)' \
|
||||
| head -n1
|
||||
)"
|
||||
|
||||
if [[ -n "$existing_application" ]]; then
|
||||
application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')"
|
||||
else
|
||||
create_application_result="$(
|
||||
api_with_status POST "/api/v3/core/applications/" "$application_payload"
|
||||
)"
|
||||
create_application_status="$(printf '%s\n' "$create_application_result" | sed -n '1p')"
|
||||
create_application_body="$(printf '%s\n' "$create_application_result" | sed '1d')"
|
||||
|
||||
if [[ "$create_application_status" =~ ^20[01]$ ]]; then
|
||||
application_pk="$(printf '%s\n' "$create_application_body" | jq -r '.pk // empty')"
|
||||
elif [[ "$create_application_status" == "400" ]] && printf '%s\n' "$create_application_body" | jq -e '
|
||||
(.slug // [] | index("Application with this slug already exists.")) != null
|
||||
or (.provider // [] | index("Application with this provider already exists.")) != null
|
||||
' >/dev/null; then
|
||||
application_pk="existing-duplicate"
|
||||
else
|
||||
printf '%s\n' "$create_application_body" >&2
|
||||
echo "error: could not reconcile Authentik application ${application_slug}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${application_pk:-}" ]]; then
|
||||
echo "error: Grafana OIDC application did not return a primary key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$access_group" ]]; then
|
||||
ensure_application_group_binding "$application_slug" "$access_group"
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS "${authentik_url}/application/o/${application_slug}/.well-known/openid-configuration" >/dev/null 2>&1; then
|
||||
echo "Synced Authentik Grafana OIDC application ${application_slug} (${application_name})."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "warning: Grafana OIDC issuer document for ${application_slug} was not immediately readable; keeping reconciled config." >&2
|
||||
echo "Synced Authentik Grafana OIDC application ${application_slug} (${application_name})."
|
||||
|
|
@ -75,6 +75,7 @@ set -euo pipefail
|
|||
|
||||
base_services=(
|
||||
forgejo.service
|
||||
grafana.service
|
||||
caddy.service
|
||||
burrow-forgejo-bootstrap.service
|
||||
burrow-forgejo-runner-bootstrap.service
|
||||
|
|
@ -89,6 +90,7 @@ nsc_services=(
|
|||
tailnet_services=(
|
||||
burrow-authentik-runtime.service
|
||||
burrow-authentik-ready.service
|
||||
burrow-authentik-grafana-oidc.service
|
||||
headscale.service
|
||||
headscale-bootstrap.service
|
||||
)
|
||||
|
|
@ -161,6 +163,8 @@ if [[ "${EXPECT_TAILNET}" == "1" ]]; then
|
|||
echo "== agenix =="
|
||||
ls -l /run/agenix || true
|
||||
test -s /run/agenix/burrowAuthentikEnv
|
||||
test -s /run/agenix/burrowGrafanaAdminPassword
|
||||
test -s /run/agenix/burrowGrafanaOidcClientSecret
|
||||
test -s /run/agenix/burrowHeadscaleOidcClientSecret
|
||||
fi
|
||||
|
||||
|
|
@ -177,6 +181,7 @@ if command -v curl >/dev/null 2>&1; then
|
|||
curl -fsS -o /dev/null -w 'forgejo_login %{http_code}\n' http://127.0.0.1:3000/user/login
|
||||
curl -fsS -o /dev/null -H 'Host: burrow.net' -w 'burrow_root %{http_code}\n' http://127.0.0.1/
|
||||
curl -fsS -o /dev/null -H 'Host: git.burrow.net' -w 'git_login %{http_code}\n' http://127.0.0.1/user/login
|
||||
curl -fsS -o /dev/null -H 'Host: graphs.burrow.net' -w 'grafana_login %{http_code}\n' http://127.0.0.1/login
|
||||
if [[ "${EXPECT_TAILNET}" == "1" ]]; then
|
||||
curl -fsS -o /dev/null -H 'Host: auth.burrow.net' -w 'authentik_ready %{http_code}\n' http://127.0.0.1/-/health/ready/
|
||||
curl -sS -o /dev/null -H 'Host: ts.burrow.net' -w 'headscale_root %{http_code}\n' http://127.0.0.1/ || true
|
||||
|
|
|
|||
102
Scripts/ci/backup-garage-to-gcs.sh
Executable file
102
Scripts/ci/backup-garage-to-gcs.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BURROW_GARAGE_ENDPOINT:?BURROW_GARAGE_ENDPOINT is required}"
|
||||
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required for Garage backup}"
|
||||
: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required for Garage backup}"
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
echo "aws CLI is required for Garage backup" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! command -v gcloud >/dev/null 2>&1; then
|
||||
echo "gcloud is required for GCS backup" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q .; then
|
||||
Scripts/ci/google-wif-auth.sh
|
||||
fi
|
||||
|
||||
export AWS_DEFAULT_REGION="${BURROW_GARAGE_REGION:-garage}"
|
||||
export AWS_EC2_METADATA_DISABLED=true
|
||||
|
||||
requested_releases=0
|
||||
requested_packages=0
|
||||
requested_nix_cache=0
|
||||
IFS=',' read -r -a requested_items <<< "${BURROW_GARAGE_BACKUP_SET:-all}"
|
||||
|
||||
for raw_item in "${requested_items[@]}"; do
|
||||
item="${raw_item//[[:space:]]/}"
|
||||
case "$item" in
|
||||
"" )
|
||||
;;
|
||||
all )
|
||||
requested_releases=1
|
||||
requested_packages=1
|
||||
requested_nix_cache=1
|
||||
;;
|
||||
release|releases )
|
||||
requested_releases=1
|
||||
;;
|
||||
package|packages )
|
||||
requested_packages=1
|
||||
;;
|
||||
attic|nix|nix-cache|nix_cache )
|
||||
requested_nix_cache=1
|
||||
;;
|
||||
* )
|
||||
echo "unknown Garage backup set item: $item" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$requested_releases" == "0" && "$requested_packages" == "0" && "$requested_nix_cache" == "0" ]]; then
|
||||
echo "no Garage backup buckets selected" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
work_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-garage-backup"
|
||||
rsync_flags=(--recursive)
|
||||
if [[ "${BURROW_GARAGE_BACKUP_DELETE:-false}" == "true" ]]; then
|
||||
rsync_flags+=(--delete-unmatched-destination-objects)
|
||||
fi
|
||||
|
||||
sync_bucket() {
|
||||
local name="$1"
|
||||
local garage_bucket="$2"
|
||||
local gcs_bucket="$3"
|
||||
local source_dir="${work_root}/${name}"
|
||||
|
||||
rm -rf "$source_dir"
|
||||
mkdir -p "$source_dir"
|
||||
|
||||
aws --endpoint-url "$BURROW_GARAGE_ENDPOINT" s3 sync "s3://${garage_bucket}" "$source_dir" --no-progress
|
||||
gcloud storage rsync "${rsync_flags[@]}" "$source_dir" "gs://${gcs_bucket}"
|
||||
}
|
||||
|
||||
if [[ "$requested_releases" == "1" ]]; then
|
||||
sync_bucket \
|
||||
releases \
|
||||
"${BURROW_RELEASE_GARAGE_BUCKET:-burrow-releases}" \
|
||||
"${BURROW_RELEASE_GCS_BUCKET:-burrow-net-releases}"
|
||||
fi
|
||||
|
||||
if [[ "$requested_packages" == "1" ]]; then
|
||||
sync_bucket \
|
||||
packages \
|
||||
"${BURROW_PACKAGE_GARAGE_BUCKET:-burrow-packages}" \
|
||||
"${BURROW_PACKAGE_GCS_BUCKET:-burrow-net-packages}"
|
||||
fi
|
||||
|
||||
if [[ "$requested_nix_cache" == "1" ]]; then
|
||||
sync_bucket \
|
||||
nix-cache \
|
||||
"${BURROW_NIX_CACHE_GARAGE_BUCKET:-attic}" \
|
||||
"${BURROW_NIX_CACHE_GCS_BUCKET:-burrow-net-nix-cache}"
|
||||
fi
|
||||
|
|
@ -4,17 +4,8 @@ set -euo pipefail
|
|||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
release_ref="${RELEASE_REF:-manual-${GITHUB_SHA:-unknown}}"
|
||||
target="x86_64-unknown-linux-gnu"
|
||||
out_dir="${repo_root}/dist"
|
||||
staging="${out_dir}/burrow-${release_ref}-${target}"
|
||||
build_number="${BUILD_NUMBER:-${RELEASE_REF:-manual-${GITHUB_SHA:-unknown}}}"
|
||||
export BUILD_NUMBER="$build_number"
|
||||
export BURROW_RELEASE_OUT="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
|
||||
|
||||
mkdir -p "${staging}"
|
||||
|
||||
cargo build --locked --release -p burrow --bin burrow
|
||||
install -m 0755 target/release/burrow "${staging}/burrow"
|
||||
cp README.md "${staging}/README.md"
|
||||
|
||||
tarball="${out_dir}/burrow-${release_ref}-${target}.tar.gz"
|
||||
tar -C "${out_dir}" -czf "${tarball}" "$(basename "${staging}")"
|
||||
shasum -a 256 "${tarball}" > "${tarball}.sha256"
|
||||
Scripts/ci/package-release-artifacts.sh "${1:-all}"
|
||||
|
|
|
|||
68
Scripts/ci/check-release-config.sh
Executable file
68
Scripts/ci/check-release-config.sh
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
require_env() {
|
||||
local name="$1"
|
||||
if [[ -z "${!name:-}" ]]; then
|
||||
echo "missing required environment variable: ${name}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_file_or_env() {
|
||||
local file_name="$1"
|
||||
local env_name="$2"
|
||||
if [[ -n "${!file_name:-}" && -f "${!file_name:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${!env_name:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
echo "missing required ${file_name} file or ${env_name} environment value" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
case "${1:-all}" in
|
||||
gcs)
|
||||
require_env BURROW_GOOGLE_PROJECT_ID
|
||||
require_env BURROW_GOOGLE_PROJECT_NUMBER
|
||||
require_env BURROW_GOOGLE_WIF_POOL_ID
|
||||
require_env BURROW_GOOGLE_WIF_PROVIDER_ID
|
||||
require_env BURROW_GOOGLE_WIF_SERVICE_ACCOUNT
|
||||
require_env BURROW_AUTHENTIK_WIF_ISSUER
|
||||
require_env BURROW_AUTHENTIK_WIF_AUDIENCE
|
||||
require_env BURROW_AUTHENTIK_WIF_CLIENT_ID
|
||||
if [[ -z "${BURROW_AUTHENTIK_WIF_TOKEN:-}" && -z "${BURROW_AUTHENTIK_WIF_CLIENT_SECRET:-}" ]]; then
|
||||
if [[ -z "${BURROW_AUTHENTIK_WIF_TOKEN_FILE:-}" || ! -f "${BURROW_AUTHENTIK_WIF_TOKEN_FILE:-}" ]]; then
|
||||
echo "missing Authentik WIF token, token file, or client secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
app-store)
|
||||
require_env ASC_API_KEY_ID
|
||||
require_env ASC_API_ISSUER_ID
|
||||
require_file_or_env ASC_API_KEY_PATH ASC_API_KEY_BASE64
|
||||
;;
|
||||
apple-signing)
|
||||
require_file_or_env APPLE_SIGNING_CERTIFICATE_PATH APPLE_SIGNING_CERTIFICATE_BASE64
|
||||
require_env APPLE_SIGNING_CERTIFICATE_PASSWORD
|
||||
;;
|
||||
sparkle)
|
||||
"$0" gcs
|
||||
;;
|
||||
microsoft-store)
|
||||
require_env MICROSOFT_TENANT_ID
|
||||
require_env MICROSOFT_CLIENT_ID
|
||||
require_env MICROSOFT_CLIENT_SECRET
|
||||
;;
|
||||
all)
|
||||
"$0" gcs
|
||||
"$0" app-store
|
||||
"$0" apple-signing
|
||||
;;
|
||||
*)
|
||||
echo "unknown release config group: $1" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
|
@ -143,13 +143,18 @@ if [[ -e "${config_file}" && ! -w "${config_file}" ]]; then
|
|||
fi
|
||||
|
||||
mkdir -p "$(dirname -- "${config_file}")"
|
||||
cat > "${config_file}" <<'EOF'
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = true
|
||||
fallback = true
|
||||
substituters = https://cache.nixos.org
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
|
||||
EOF
|
||||
{
|
||||
echo "experimental-features = nix-command flakes"
|
||||
echo "sandbox = true"
|
||||
echo "fallback = true"
|
||||
if [[ -n "${BURROW_NIX_CACHE_PUBLIC_KEY:-}" ]]; then
|
||||
echo "substituters = ${BURROW_NIX_CACHE_URL:-https://nix.burrow.net/${BURROW_NIX_CACHE_NAME:-burrow}} https://cache.nixos.org"
|
||||
echo "trusted-public-keys = ${BURROW_NIX_CACHE_PUBLIC_KEY} cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
else
|
||||
echo "substituters = https://cache.nixos.org"
|
||||
echo "trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
fi
|
||||
} > "${config_file}"
|
||||
|
||||
command -v nix >/dev/null 2>&1 || {
|
||||
echo "nix is still unavailable after bootstrap" >&2
|
||||
|
|
|
|||
73
Scripts/ci/ensure-nsc.sh
Executable file
73
Scripts/ci/ensure-nsc.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if command -v nsc >/dev/null 2>&1; then
|
||||
nsc version || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "::error ::curl is required to install nsc" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo "::error ::tar is required to install nsc" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
arch="$(uname -m)"
|
||||
case "$arch" in
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
aarch64|arm64) arch="arm64" ;;
|
||||
*)
|
||||
echo "::error ::Unsupported arch for nsc: ${arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$os" != "linux" ]]; then
|
||||
echo "::warning ::nsc bootstrap is only supported on Linux; relying on the runner or Nix shell for ${os}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="${NSC_VERSION:-0.0.506}"
|
||||
expected_sha256=""
|
||||
case "${version}/${arch}" in
|
||||
0.0.506/amd64) expected_sha256="4a093e0269878a9514e7c00d99b88a924f307ad15fb9d1595ee5f6582f99a0f0" ;;
|
||||
0.0.506/arm64) expected_sha256="6acf074b67fcbaaf59a4437f6bd40da7e38438c427849c1b5fcf4621ce75aee2" ;;
|
||||
esac
|
||||
|
||||
asset="nsc_${version}_linux_${arch}.tar.gz"
|
||||
url="https://get.namespace.so/packages/nsc/v${version}/${asset}"
|
||||
tmp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-nsc.$$"
|
||||
install_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/nsc-bin"
|
||||
mkdir -p "$tmp_dir" "$install_dir"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
curl -fsSL -o "${tmp_dir}/${asset}" "$url"
|
||||
if [[ -n "$expected_sha256" ]] && command -v sha256sum >/dev/null 2>&1; then
|
||||
actual_sha256="$(sha256sum "${tmp_dir}/${asset}" | awk '{print $1}')"
|
||||
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
|
||||
echo "::error ::nsc ${asset} sha256 mismatch: expected ${expected_sha256}, got ${actual_sha256}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
|
||||
for bin in nsc bazel-credential-nsc docker-credential-nsc; do
|
||||
if [[ -x "${tmp_dir}/${bin}" ]]; then
|
||||
install -m 0755 "${tmp_dir}/${bin}" "${install_dir}/${bin}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -x "${install_dir}/nsc" || ! -x "${install_dir}/bazel-credential-nsc" ]]; then
|
||||
echo "::error ::Failed to install nsc and bazel-credential-nsc from ${asset}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${GITHUB_PATH:-}" ]]; then
|
||||
echo "${install_dir}" >> "${GITHUB_PATH}"
|
||||
fi
|
||||
export PATH="${install_dir}:$PATH"
|
||||
nsc version || true
|
||||
60
Scripts/ci/ensure-spacectl.sh
Executable file
60
Scripts/ci/ensure-spacectl.sh
Executable file
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if command -v spacectl >/dev/null 2>&1; then
|
||||
spacectl version || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "::error ::curl is required to install spacectl" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo "::error ::tar is required to install spacectl" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
arch="$(uname -m)"
|
||||
case "$arch" in
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
aarch64|arm64) arch="arm64" ;;
|
||||
*)
|
||||
echo "::error ::Unsupported arch for spacectl: ${arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$os" != "linux" && "$os" != "darwin" ]]; then
|
||||
echo "::error ::Unsupported OS for spacectl: ${os}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="${SPACECTL_VERSION:-v0.5.0}"
|
||||
version_num="${version#v}"
|
||||
asset="spacectl_${version_num}_${os}_${arch}.tar.gz"
|
||||
url="https://github.com/namespacelabs/spacectl/releases/download/${version}/${asset}"
|
||||
tmp_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-spacectl.$$"
|
||||
install_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/spacectl-bin"
|
||||
mkdir -p "$tmp_dir" "$install_dir"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
curl -fsSL -o "${tmp_dir}/${asset}" "$url"
|
||||
tar -xzf "${tmp_dir}/${asset}" -C "$tmp_dir"
|
||||
|
||||
bin="${tmp_dir}/spacectl"
|
||||
if [[ ! -x "$bin" ]]; then
|
||||
bin="$(find "$tmp_dir" -maxdepth 3 -type f -name spacectl -perm -111 | head -n1 || true)"
|
||||
fi
|
||||
if [[ -z "${bin}" || ! -x "${bin}" ]]; then
|
||||
echo "::error ::Failed to locate spacectl binary in ${asset}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0755 "$bin" "${install_dir}/spacectl"
|
||||
if [[ -n "${GITHUB_PATH:-}" ]]; then
|
||||
echo "${install_dir}" >> "${GITHUB_PATH}"
|
||||
fi
|
||||
export PATH="${install_dir}:$PATH"
|
||||
spacectl version || true
|
||||
126
Scripts/ci/google-wif-auth.sh
Executable file
126
Scripts/ci/google-wif-auth.sh
Executable file
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_id="${GOOGLE_CLOUD_PROJECT:-${BURROW_GOOGLE_PROJECT_ID:-project-88c23ce9-918a-470a-b33}}"
|
||||
project_number="${BURROW_GOOGLE_PROJECT_NUMBER:-416198671487}"
|
||||
pool_id="${BURROW_GOOGLE_WIF_POOL_ID:-burrow-authentik}"
|
||||
provider_id="${BURROW_GOOGLE_WIF_PROVIDER_ID:-forgejo-runners}"
|
||||
service_account="${BURROW_GOOGLE_WIF_SERVICE_ACCOUNT:-burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com}"
|
||||
authentik_issuer="${BURROW_AUTHENTIK_WIF_ISSUER:-https://auth.burrow.net/application/o/google-cloud/}"
|
||||
authentik_audience="${BURROW_AUTHENTIK_WIF_AUDIENCE:-google-wif.burrow.net}"
|
||||
authentik_client_id="${BURROW_AUTHENTIK_WIF_CLIENT_ID:-${AUTHENTIK_GOOGLE_WIF_CLIENT_ID:-google-wif.burrow.net}}"
|
||||
authentik_client_secret="${BURROW_AUTHENTIK_WIF_CLIENT_SECRET:-${AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET:-}}"
|
||||
authentik_token_file="${BURROW_AUTHENTIK_WIF_TOKEN_FILE:-}"
|
||||
authentik_token="${BURROW_AUTHENTIK_WIF_TOKEN:-}"
|
||||
work_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-google-wif"
|
||||
emit_env=1
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/ci/google-wif-auth.sh [--no-emit-env]
|
||||
|
||||
Authenticates gcloud to the Burrow Google project through Authentik-issued OIDC
|
||||
and Google Workload Identity Federation. No Google service-account JSON key is
|
||||
used or written.
|
||||
|
||||
Inputs:
|
||||
GOOGLE_CLOUD_PROJECT / BURROW_GOOGLE_PROJECT_ID
|
||||
BURROW_GOOGLE_PROJECT_NUMBER
|
||||
BURROW_GOOGLE_WIF_POOL_ID
|
||||
BURROW_GOOGLE_WIF_PROVIDER_ID
|
||||
BURROW_GOOGLE_WIF_SERVICE_ACCOUNT
|
||||
BURROW_AUTHENTIK_WIF_ISSUER
|
||||
BURROW_AUTHENTIK_WIF_AUDIENCE
|
||||
BURROW_AUTHENTIK_WIF_TOKEN or BURROW_AUTHENTIK_WIF_TOKEN_FILE
|
||||
|
||||
If no token is supplied, the script requests one from Authentik using
|
||||
BURROW_AUTHENTIK_WIF_CLIENT_ID and BURROW_AUTHENTIK_WIF_CLIENT_SECRET.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-emit-env)
|
||||
emit_env=0
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "$1 is required for Google WIF authentication" >&2
|
||||
exit 127
|
||||
fi
|
||||
}
|
||||
|
||||
require_command gcloud
|
||||
require_command curl
|
||||
require_command jq
|
||||
|
||||
mkdir -p "$work_dir"
|
||||
chmod 0700 "$work_dir"
|
||||
|
||||
token_path="$work_dir/authentik-oidc-token.jwt"
|
||||
credential_config="$work_dir/google-wif-credentials.json"
|
||||
|
||||
if [[ -n "$authentik_token_file" ]]; then
|
||||
if [[ ! -s "$authentik_token_file" ]]; then
|
||||
echo "Authentik WIF token file is empty or missing: $authentik_token_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$authentik_token_file" "$token_path"
|
||||
elif [[ -n "$authentik_token" ]]; then
|
||||
printf '%s' "$authentik_token" > "$token_path"
|
||||
else
|
||||
if [[ -z "$authentik_client_secret" ]]; then
|
||||
echo "missing Authentik WIF token. Set BURROW_AUTHENTIK_WIF_TOKEN, BURROW_AUTHENTIK_WIF_TOKEN_FILE, or BURROW_AUTHENTIK_WIF_CLIENT_SECRET." >&2
|
||||
exit 1
|
||||
fi
|
||||
token_endpoint="${authentik_issuer%/}/token/"
|
||||
curl -fsS \
|
||||
-u "${authentik_client_id}:${authentik_client_secret}" \
|
||||
-d grant_type=client_credentials \
|
||||
-d scope="openid profile email groups" \
|
||||
-d audience="$authentik_audience" \
|
||||
"$token_endpoint" \
|
||||
| jq -r '.id_token // empty' > "$token_path"
|
||||
fi
|
||||
|
||||
chmod 0600 "$token_path"
|
||||
if [[ ! -s "$token_path" ]]; then
|
||||
echo "Authentik did not return an OIDC token for Google WIF" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
provider_resource="projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/providers/${provider_id}"
|
||||
gcloud iam workload-identity-pools create-cred-config "$provider_resource" \
|
||||
--service-account "$service_account" \
|
||||
--subject-token-type="urn:ietf:params:oauth:token-type:jwt" \
|
||||
--credential-source-file "$token_path" \
|
||||
--output-file "$credential_config"
|
||||
|
||||
chmod 0600 "$credential_config"
|
||||
gcloud auth login --cred-file="$credential_config" --brief
|
||||
gcloud config set project "$project_id" >/dev/null
|
||||
|
||||
{
|
||||
echo "GOOGLE_APPLICATION_CREDENTIALS=$credential_config"
|
||||
echo "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=$credential_config"
|
||||
echo "CLOUDSDK_CORE_PROJECT=$project_id"
|
||||
echo "GOOGLE_CLOUD_PROJECT=$project_id"
|
||||
} > "$work_dir/google-wif.env"
|
||||
|
||||
if [[ "$emit_env" == "1" && -n "${GITHUB_ENV:-}" ]]; then
|
||||
cat "$work_dir/google-wif.env" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
echo "Authenticated gcloud through Authentik WIF as ${service_account} in ${project_id}."
|
||||
124
Scripts/ci/install-apple-signing-assets.sh
Executable file
124
Scripts/ci/install-apple-signing-assets.sh
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
signing_ready=0
|
||||
keychain_path=""
|
||||
|
||||
write_env() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
echo "${key}=${value}" >> "$GITHUB_ENV"
|
||||
fi
|
||||
}
|
||||
|
||||
install_profile_file() {
|
||||
local source="$1"
|
||||
[[ -f "$source" ]] || return 0
|
||||
|
||||
local uuid
|
||||
uuid="$(security cms -D -i "$source" 2>/dev/null | plutil -extract UUID raw -o - - 2>/dev/null || true)"
|
||||
if [[ -z "$uuid" ]]; then
|
||||
uuid="$(basename "$source")"
|
||||
fi
|
||||
|
||||
local profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$profiles_dir"
|
||||
cp "$source" "${profiles_dir}/${uuid}.provisionprofile"
|
||||
echo "Installed provisioning profile ${uuid}."
|
||||
}
|
||||
|
||||
install_profile_base64() {
|
||||
local name="$1"
|
||||
local value="${!name:-}"
|
||||
[[ -n "$value" ]] || return 0
|
||||
|
||||
local out="${RUNNER_TEMP:-/tmp}/${name}.provisionprofile"
|
||||
printf '%s' "$value" | base64 --decode > "$out"
|
||||
install_profile_file "$out"
|
||||
}
|
||||
|
||||
install_profile_env_path() {
|
||||
local name="$1"
|
||||
local path="${!name:-}"
|
||||
[[ -n "$path" ]] || return 0
|
||||
if [[ ! -f "$path" ]]; then
|
||||
echo "::error ::${name} points to a missing provisioning profile: ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
install_profile_file "$path"
|
||||
}
|
||||
|
||||
for profile in \
|
||||
Apple/Profiles/*.provisionprofile \
|
||||
Apple/Profiles/*.mobileprovision \
|
||||
.forgejo/actions/export/*.provisionprofile \
|
||||
.forgejo/actions/export/*.mobileprovision; do
|
||||
[[ -e "$profile" ]] || continue
|
||||
install_profile_file "$profile"
|
||||
done
|
||||
install_profile_env_path APPLE_PROVISIONING_PROFILE_PATH
|
||||
install_profile_env_path APPLE_NETWORK_EXTENSION_PROVISIONING_PROFILE_PATH
|
||||
install_profile_env_path APPLE_MACOS_PROVISIONING_PROFILE_PATH
|
||||
install_profile_base64 APPLE_PROVISIONING_PROFILE_BASE64
|
||||
install_profile_base64 APPLE_NETWORK_EXTENSION_PROVISIONING_PROFILE_BASE64
|
||||
install_profile_base64 APPLE_MACOS_PROVISIONING_PROFILE_BASE64
|
||||
|
||||
cert_base64="${APPLE_SIGNING_CERTIFICATE_BASE64:-${APPLE_DISTRIBUTION_CERTIFICATE_BASE64:-}}"
|
||||
cert_path_env="${APPLE_SIGNING_CERTIFICATE_PATH:-${APPLE_DISTRIBUTION_CERTIFICATE_PATH:-}}"
|
||||
cert_password="${APPLE_SIGNING_CERTIFICATE_PASSWORD:-${APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD:-}}"
|
||||
keychain_password="${APPLE_KEYCHAIN_PASSWORD:-${cert_password}}"
|
||||
|
||||
if [[ -n "$cert_base64" || -n "$cert_path_env" ]]; then
|
||||
if [[ -z "$cert_password" ]]; then
|
||||
echo "::error ::APPLE_SIGNING_CERTIFICATE_PASSWORD is required when Apple signing certificate material is set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cert_path="${RUNNER_TEMP:-/tmp}/burrow-apple-signing.p12"
|
||||
keychain_path="$HOME/Library/Keychains/BurrowRelease.keychain-db"
|
||||
if [[ -n "$cert_path_env" ]]; then
|
||||
if [[ ! -f "$cert_path_env" ]]; then
|
||||
echo "::error ::APPLE_SIGNING_CERTIFICATE_PATH points to a missing file: ${cert_path_env}" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$cert_path_env" "$cert_path"
|
||||
else
|
||||
printf '%s' "$cert_base64" | base64 --decode > "$cert_path"
|
||||
fi
|
||||
|
||||
security create-keychain -p "$keychain_password" "$keychain_path" || true
|
||||
security set-keychain-settings -lut 21600 "$keychain_path"
|
||||
security unlock-keychain -p "$keychain_password" "$keychain_path"
|
||||
security import "$cert_path" \
|
||||
-k "$keychain_path" \
|
||||
-P "$cert_password" \
|
||||
-T /usr/bin/codesign \
|
||||
-T /usr/bin/security \
|
||||
-T /usr/bin/productbuild \
|
||||
-T /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path" >/dev/null
|
||||
|
||||
existing="$(security list-keychains -d user | sed 's/[ "]//g' || true)"
|
||||
if ! printf '%s\n' "$existing" | grep -Fxq "$keychain_path"; then
|
||||
security list-keychains -d user -s "$keychain_path" $existing
|
||||
fi
|
||||
|
||||
signing_ready=1
|
||||
write_env BURROW_APPLE_KEYCHAIN_PATH "$keychain_path"
|
||||
else
|
||||
identities="$(security find-identity -v -p codesigning 2>/dev/null || true)"
|
||||
if printf '%s\n' "$identities" | grep -Eq '"(Apple Distribution|Developer ID Application):'; then
|
||||
signing_ready=1
|
||||
fi
|
||||
fi
|
||||
|
||||
write_env BURROW_APPLE_SIGNING_READY "$signing_ready"
|
||||
if [[ "$signing_ready" == "1" ]]; then
|
||||
echo "Apple signing assets are available."
|
||||
else
|
||||
echo "::notice ::Apple signing certificate is not configured; release lane will build unsigned validation artifacts only."
|
||||
fi
|
||||
225
Scripts/ci/nscloud-cache.sh
Executable file
225
Scripts/ci/nscloud-cache.sh
Executable file
|
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
echo "No cache modes specified; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
want_bazel_cache=0
|
||||
for mode in "$@"; do
|
||||
if [[ "$mode" == "bazel" ]]; then
|
||||
want_bazel_cache=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
portable_mkdir() {
|
||||
hash -r >/dev/null 2>&1 || true
|
||||
command -p mkdir "$@"
|
||||
}
|
||||
|
||||
append_bazel_storage_cache() {
|
||||
local bazelrc_path="$1"
|
||||
local cache_root="${BURROW_BAZEL_STORAGE_CACHE:-}"
|
||||
if [[ -z "$cache_root" && -n "${NSC_CACHE_PATH:-}" ]]; then
|
||||
cache_root="${NSC_CACHE_PATH}/bazel"
|
||||
fi
|
||||
if [[ -z "$cache_root" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
portable_mkdir -p "${cache_root}/disk" "${cache_root}/repository"
|
||||
{
|
||||
echo "build --disk_cache=${cache_root}/disk"
|
||||
echo "build --repository_cache=${cache_root}/repository"
|
||||
} >> "$bazelrc_path"
|
||||
echo "Namespace Bazel storage cache configured: ${cache_root}"
|
||||
}
|
||||
|
||||
configure_bazel_remote_cache() {
|
||||
if [[ "$want_bazel_cache" != "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
export BURROW_BAZEL_NSCCACHE_ACTIVE=0
|
||||
if ! command -v nsc >/dev/null 2>&1; then
|
||||
if bash Scripts/ci/ensure-nsc.sh; then
|
||||
nsc_bin_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/nsc-bin"
|
||||
if [[ -x "${nsc_bin_dir}/nsc" ]]; then
|
||||
export PATH="${nsc_bin_dir}:$PATH"
|
||||
fi
|
||||
else
|
||||
echo "::warning ::nsc not found; using local Bazel disk cache only."
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
echo "BURROW_BAZEL_NSCCACHE_ACTIVE=0" >> "$GITHUB_ENV"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local bazelrc_path
|
||||
bazelrc_path="${NSC_BAZELRC_PATH:-${TMPDIR:-/tmp}/burrow-nsc-cache.bazelrc}"
|
||||
portable_mkdir -p "$(dirname "$bazelrc_path")"
|
||||
if nsc auth check-login >/dev/null 2>&1 && nsc cache bazel setup --bazelrc "$bazelrc_path" >/dev/null 2>&1; then
|
||||
append_bazel_storage_cache "$bazelrc_path"
|
||||
export BURROW_BAZEL_NSCCACHE_BAZELRC="$bazelrc_path"
|
||||
export BURROW_BAZEL_NSCCACHE_ACTIVE=1
|
||||
echo "Namespace Bazel cache configured: $bazelrc_path"
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
{
|
||||
echo "BURROW_BAZEL_NSCCACHE_ACTIVE=1"
|
||||
echo "BURROW_BAZEL_NSCCACHE_BAZELRC=$bazelrc_path"
|
||||
echo "NSC_BAZELRC_PATH=$bazelrc_path"
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
else
|
||||
append_bazel_storage_cache "$bazelrc_path"
|
||||
echo "::warning ::Namespace Bazel remote cache unavailable; using local Bazel repository/disk cache."
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
echo "BURROW_BAZEL_NSCCACHE_ACTIVE=0" >> "$GITHUB_ENV"
|
||||
echo "BURROW_BAZEL_NSCCACHE_BAZELRC=$bazelrc_path" >> "$GITHUB_ENV"
|
||||
echo "NSC_BAZELRC_PATH=$bazelrc_path" >> "$GITHUB_ENV"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cache_root="${NSC_CACHE_PATH:-}"
|
||||
if [[ -z "$cache_root" ]]; then
|
||||
ensure_dir() {
|
||||
local dir="$1"
|
||||
if portable_mkdir -p "$dir" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$(id -u)" != "0" ]] && command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
|
||||
sudo mkdir -p "$dir" >/dev/null 2>&1 || return 1
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "$(uname -s 2>/dev/null || true)" == "Linux" ]]; then
|
||||
tmp_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/burrow-nscloud-cache"
|
||||
for candidate in "/cache/nscloud" "$tmp_root" "$PWD/.nscloud-cache"; do
|
||||
if ensure_dir "$candidate"; then
|
||||
cache_root="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
elif ensure_dir "$PWD/.nscloud-cache"; then
|
||||
cache_root="$PWD/.nscloud-cache"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$cache_root" ]]; then
|
||||
echo "NSCloud cache volume not configured (NSC_CACHE_PATH missing); skipping."
|
||||
configure_bazel_remote_cache
|
||||
exit 0
|
||||
fi
|
||||
|
||||
export NSC_CACHE_PATH="$cache_root"
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
echo "NSC_CACHE_PATH=$cache_root" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
spacectl_bin_dir="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/spacectl-bin"
|
||||
if ! command -v spacectl >/dev/null 2>&1; then
|
||||
if ! bash Scripts/ci/ensure-spacectl.sh; then
|
||||
echo "::warning ::Unable to install spacectl; skipping filesystem cache mount."
|
||||
configure_bazel_remote_cache
|
||||
exit 0
|
||||
fi
|
||||
if [[ -x "${spacectl_bin_dir}/spacectl" ]]; then
|
||||
export PATH="${spacectl_bin_dir}:$PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
mode_args=()
|
||||
for mode in "$@"; do
|
||||
[[ -z "$mode" ]] && continue
|
||||
if [[ "$mode" == "bazel" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "$mode" == "nix" && "${BURROW_NSC_DIRECT_NIX_VOLUME:-0}" == "1" ]]; then
|
||||
echo "Namespace Nix cache volume is mounted directly at /nix; skipping spacectl nix mount."
|
||||
continue
|
||||
fi
|
||||
if [[ "$mode" == "nix" && "$(uname -s)" == "Darwin" ]]; then
|
||||
echo "Skipping late Nix cache mount on macOS; using existing Nix profile and Bazel cache."
|
||||
continue
|
||||
fi
|
||||
mode_args+=("--mode=${mode}")
|
||||
done
|
||||
|
||||
if [[ "${#mode_args[@]}" -eq 0 ]]; then
|
||||
echo "No filesystem cache modes require mounting; skipping spacectl cache mount."
|
||||
configure_bazel_remote_cache
|
||||
exit 0
|
||||
fi
|
||||
|
||||
portable_mkdir -p "$cache_root" 2>/dev/null || true
|
||||
|
||||
args=(-o json cache mount "--dry_run=false" "--cache_root=${cache_root}" "${mode_args[@]}")
|
||||
echo "Mounting NSCloud cache: spacectl ${args[*]//${cache_root}/<cache_root>}"
|
||||
|
||||
spacectl_bin="$(command -v spacectl)"
|
||||
run_mount=("$spacectl_bin" "${args[@]}")
|
||||
if [[ "$(id -u)" != "0" ]]; then
|
||||
if ! command -v sudo >/dev/null 2>&1 || ! sudo -n true >/dev/null 2>&1; then
|
||||
echo "::warning ::spacectl cache mount requires passwordless sudo; skipping filesystem cache."
|
||||
configure_bazel_remote_cache
|
||||
exit 0
|
||||
fi
|
||||
run_mount=(sudo -n env "PATH=$PATH" "$spacectl_bin" "${args[@]}")
|
||||
fi
|
||||
|
||||
tmp_err="$(mktemp "${TMPDIR:-/tmp}/spacectl.XXXXXX")"
|
||||
cleanup() {
|
||||
rm -f "$tmp_err"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
json=""
|
||||
if ! json="$("${run_mount[@]}" 2>"$tmp_err")"; then
|
||||
echo "::warning ::spacectl cache mount failed; skipping filesystem cache."
|
||||
if [[ -n "$json" ]]; then
|
||||
printf '%s\n' "$json" || true
|
||||
fi
|
||||
if [[ -s "$tmp_err" ]]; then
|
||||
sed -n '1,200p' "$tmp_err" || true
|
||||
fi
|
||||
configure_bazel_remote_cache
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
NSC_MOUNT_JSON="$json" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
raw = os.environ.get("NSC_MOUNT_JSON", "").strip()
|
||||
if not raw:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except Exception as exc:
|
||||
print(f"::warning ::Unable to parse spacectl JSON output; skipping cache env export: {exc}")
|
||||
sys.exit(0)
|
||||
|
||||
if payload.get("error"):
|
||||
print(f"::warning ::spacectl cache mount skipped: {payload.get('message') or 'error=true'}")
|
||||
sys.exit(0)
|
||||
|
||||
add_envs = (payload.get("output") or {}).get("add_envs") or {}
|
||||
gh_env = os.environ.get("GITHUB_ENV")
|
||||
if gh_env and isinstance(add_envs, dict):
|
||||
with open(gh_env, "a", encoding="utf-8") as f:
|
||||
for key, value in add_envs.items():
|
||||
if key and value is not None:
|
||||
f.write(f"{key}={value}\n")
|
||||
PY
|
||||
fi
|
||||
|
||||
configure_bazel_remote_cache
|
||||
287
Scripts/ci/package-apple-artifacts.sh
Executable file
287
Scripts/ci/package-apple-artifacts.sh
Executable file
|
|
@ -0,0 +1,287 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
platform="${1:-all}"
|
||||
build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}"
|
||||
out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
|
||||
apple_root="${out_root}/apple"
|
||||
archives_root="${apple_root}/archives"
|
||||
derived_data="${BURROW_DERIVED_DATA_PATH:-${repo_root}/.derived-data/apple}"
|
||||
source_packages="${BURROW_SWIFTPM_CACHE_PATH:-${XDG_CACHE_HOME:-${HOME}/.cache}/burrow/swiftpm}"
|
||||
project="Apple/Burrow.xcodeproj"
|
||||
scheme="${BURROW_APPLE_SCHEME:-App}"
|
||||
bundle_id="${BURROW_APPLE_BUNDLE_ID:-com.hackclub.burrow}"
|
||||
network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}"
|
||||
signing_ready="${BURROW_APPLE_SIGNING_READY:-0}"
|
||||
require_signing="${BURROW_APPLE_REQUIRE_SIGNING:-false}"
|
||||
sparkle_feed_url="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}"
|
||||
sparkle_public_ed_key="${BURROW_SPARKLE_PUBLIC_ED_KEY:-Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=}"
|
||||
sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}"
|
||||
|
||||
mkdir -p "$apple_root" "$archives_root" "$derived_data" "$source_packages"
|
||||
|
||||
truthy() {
|
||||
case "${1:-}" in
|
||||
1|true|TRUE|True|yes|YES|Yes|y|Y|on|ON|On) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
sha256_file() {
|
||||
local file="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" > "${file}.sha256"
|
||||
else
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
fi
|
||||
}
|
||||
|
||||
write_version() {
|
||||
mkdir -p Apple/Configuration
|
||||
printf 'CURRENT_PROJECT_VERSION = %s\n' "$build_number" > Apple/Configuration/Version.xcconfig
|
||||
}
|
||||
|
||||
xcode_common_args=(
|
||||
-project "$project"
|
||||
-scheme "$scheme"
|
||||
-configuration Release
|
||||
-derivedDataPath "$derived_data"
|
||||
-clonedSourcePackagesDirPath "$source_packages"
|
||||
CURRENT_PROJECT_VERSION="$build_number"
|
||||
APP_BUNDLE_IDENTIFIER="$bundle_id"
|
||||
NETWORK_EXTENSION_BUNDLE_IDENTIFIER="$network_extension_bundle_id"
|
||||
BURROW_SPARKLE_FEED_URL="$sparkle_feed_url"
|
||||
BURROW_SPARKLE_PUBLIC_ED_KEY="$sparkle_public_ed_key"
|
||||
)
|
||||
|
||||
xcode_auth_args=()
|
||||
if [[ -n "${ASC_API_KEY_ID:-}" && -n "${ASC_API_ISSUER_ID:-}" && -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then
|
||||
xcode_auth_args+=(
|
||||
-authenticationKeyID "$ASC_API_KEY_ID"
|
||||
-authenticationKeyIssuerID "$ASC_API_ISSUER_ID"
|
||||
-authenticationKeyPath "$ASC_API_KEY_PATH"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -n "${BURROW_APPLE_KEYCHAIN_PATH:-}" ]]; then
|
||||
xcode_common_args+=(OTHER_CODE_SIGN_FLAGS="--keychain ${BURROW_APPLE_KEYCHAIN_PATH}")
|
||||
fi
|
||||
|
||||
require_xcodebuild() {
|
||||
if ! command -v xcodebuild >/dev/null 2>&1; then
|
||||
echo "xcodebuild is required for Apple release artifacts" >&2
|
||||
exit 1
|
||||
fi
|
||||
xcodebuild -version
|
||||
xcrun --find swiftc >/dev/null
|
||||
}
|
||||
|
||||
unsigned_note() {
|
||||
local platform_name="$1"
|
||||
local artifact_name="$2"
|
||||
{
|
||||
echo "Burrow ${platform_name} signing assets were not available."
|
||||
echo
|
||||
echo "The lane built an unsigned validation artifact instead of an uploadable release artifact."
|
||||
echo "Seal the Apple release credentials with agenix and let CI sync provisioning profiles from App Store Connect to produce ${artifact_name}."
|
||||
} > "${apple_root}/README-${platform_name}-unsigned.txt"
|
||||
}
|
||||
|
||||
ensure_signing_or_note() {
|
||||
local platform_name="$1"
|
||||
local artifact_name="$2"
|
||||
if [[ "$signing_ready" == "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if truthy "$require_signing"; then
|
||||
echo "Apple signing is required for ${platform_name}, but signing assets are not configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
unsigned_note "$platform_name" "$artifact_name"
|
||||
return 1
|
||||
}
|
||||
|
||||
write_export_options() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
cat > "$path" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>${method}</string>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>${DEVELOPMENT_TEAM:-P6PV2R9443}</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
}
|
||||
|
||||
build_ios_unsigned() {
|
||||
local product_dir zip_path
|
||||
xcodebuild build \
|
||||
"${xcode_common_args[@]}" \
|
||||
-destination "generic/platform=iOS Simulator" \
|
||||
ARCHS=arm64 \
|
||||
ONLY_ACTIVE_ARCH=YES \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO
|
||||
|
||||
product_dir="${derived_data}/Build/Products/Release-iphonesimulator"
|
||||
zip_path="${apple_root}/Burrow-iOS-simulator-${build_number}.unsigned.zip"
|
||||
if [[ -d "${product_dir}/Burrow.app" ]]; then
|
||||
(cd "$product_dir" && zip -qr "$zip_path" Burrow.app)
|
||||
sha256_file "$zip_path"
|
||||
fi
|
||||
}
|
||||
|
||||
build_ios_release() {
|
||||
local archive_path export_dir export_options ipa
|
||||
if ! ensure_signing_or_note "iOS" "Burrow-iOS-${build_number}.ipa"; then
|
||||
build_ios_unsigned
|
||||
return 0
|
||||
fi
|
||||
|
||||
archive_path="${archives_root}/Burrow-iOS-${build_number}.xcarchive"
|
||||
export_dir="${apple_root}/ios-export"
|
||||
export_options="${apple_root}/ExportOptions-iOS.plist"
|
||||
write_export_options "app-store-connect" "$export_options"
|
||||
|
||||
xcodebuild archive \
|
||||
"${xcode_common_args[@]}" \
|
||||
-destination "generic/platform=iOS" \
|
||||
-archivePath "$archive_path" \
|
||||
ARCHS=arm64 \
|
||||
-allowProvisioningUpdates \
|
||||
"${xcode_auth_args[@]}"
|
||||
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$archive_path" \
|
||||
-exportPath "$export_dir" \
|
||||
-exportOptionsPlist "$export_options" \
|
||||
-allowProvisioningUpdates \
|
||||
"${xcode_auth_args[@]}"
|
||||
|
||||
ipa="$(find "$export_dir" -maxdepth 1 -type f -name '*.ipa' | sort | head -n 1 || true)"
|
||||
if [[ -z "$ipa" ]]; then
|
||||
echo "iOS export completed but did not produce an IPA" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$ipa" "${apple_root}/Burrow-iOS-${build_number}.ipa"
|
||||
sha256_file "${apple_root}/Burrow-iOS-${build_number}.ipa"
|
||||
}
|
||||
|
||||
build_macos_unsigned() {
|
||||
local product_dir zip_path
|
||||
xcodebuild build \
|
||||
"${xcode_common_args[@]}" \
|
||||
-destination "platform=macOS" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO
|
||||
|
||||
product_dir="${derived_data}/Build/Products/Release"
|
||||
zip_path="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip"
|
||||
if [[ -d "${product_dir}/Burrow.app" ]]; then
|
||||
ditto -c -k --keepParent "${product_dir}/Burrow.app" "$zip_path"
|
||||
sha256_file "$zip_path"
|
||||
fi
|
||||
}
|
||||
|
||||
build_macos_release() {
|
||||
local archive_path export_dir export_options app dmg channel sparkle_dir length pubdate url
|
||||
if ! ensure_signing_or_note "macOS" "Burrow-macOS-${build_number}.dmg"; then
|
||||
build_macos_unsigned
|
||||
return 0
|
||||
fi
|
||||
|
||||
archive_path="${archives_root}/Burrow-macOS-${build_number}.xcarchive"
|
||||
export_dir="${apple_root}/macos-export"
|
||||
export_options="${apple_root}/ExportOptions-macOS.plist"
|
||||
write_export_options "${BURROW_MACOS_EXPORT_METHOD:-developer-id}" "$export_options"
|
||||
|
||||
xcodebuild archive \
|
||||
"${xcode_common_args[@]}" \
|
||||
-destination "generic/platform=macOS" \
|
||||
-archivePath "$archive_path" \
|
||||
-allowProvisioningUpdates \
|
||||
"${xcode_auth_args[@]}"
|
||||
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$archive_path" \
|
||||
-exportPath "$export_dir" \
|
||||
-exportOptionsPlist "$export_options" \
|
||||
-allowProvisioningUpdates \
|
||||
"${xcode_auth_args[@]}"
|
||||
|
||||
app="$(find "$export_dir" -maxdepth 2 -type d -name 'Burrow.app' | sort | head -n 1 || true)"
|
||||
if [[ -z "$app" ]]; then
|
||||
echo "macOS export completed but did not produce Burrow.app" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dmg="${apple_root}/Burrow-macOS-${build_number}.dmg"
|
||||
rm -f "$dmg"
|
||||
hdiutil create -volname "Burrow ${build_number}" -srcfolder "$app" -ov -format UDZO "$dmg"
|
||||
sha256_file "$dmg"
|
||||
|
||||
channel="${SPARKLE_CHANNEL:-build-${build_number}}"
|
||||
sparkle_dir="${out_root}/sparkle/${channel}"
|
||||
mkdir -p "$sparkle_dir"
|
||||
cp "$dmg" "$sparkle_dir/"
|
||||
length="$(wc -c < "$dmg" | tr -d ' ')"
|
||||
pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
||||
url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/$(basename "$dmg")"
|
||||
cat > "${sparkle_dir}/appcast.xml" <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||
<channel>
|
||||
<title>Burrow ${channel}</title>
|
||||
<item>
|
||||
<title>Burrow ${build_number}</title>
|
||||
<pubDate>${pubdate}</pubDate>
|
||||
<sparkle:version>${build_number}</sparkle:version>
|
||||
<sparkle:shortVersionString>0.1</sparkle:shortVersionString>
|
||||
<enclosure url="${url}" sparkle:version="${build_number}" length="${length}" type="application/octet-stream" />
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
EOF
|
||||
|
||||
if truthy "$sparkle_sign_with_kms"; then
|
||||
Scripts/sparkle/sign-appcast-kms.py \
|
||||
--appcast "${sparkle_dir}/appcast.xml" \
|
||||
--artifact-dir "$sparkle_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
require_xcodebuild
|
||||
write_version
|
||||
|
||||
case "$platform" in
|
||||
all)
|
||||
build_ios_release
|
||||
build_macos_release
|
||||
;;
|
||||
ios)
|
||||
build_ios_release
|
||||
;;
|
||||
macos)
|
||||
build_macos_release
|
||||
;;
|
||||
*)
|
||||
echo "unknown Apple release platform: $platform" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
find "$out_root" -type f -print | sort
|
||||
332
Scripts/ci/package-release-artifacts.sh
Executable file
332
Scripts/ci/package-release-artifacts.sh
Executable file
|
|
@ -0,0 +1,332 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
platform="${1:-all}"
|
||||
build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}"
|
||||
out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}"
|
||||
|
||||
mkdir -p "${out_root}"
|
||||
|
||||
sha256_file() {
|
||||
local file="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" > "${file}.sha256"
|
||||
else
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
fi
|
||||
}
|
||||
|
||||
burrow_crate_version() {
|
||||
awk -F '"' '/^version = / { print $2; exit }' burrow/Cargo.toml
|
||||
}
|
||||
|
||||
package_release_suffix() {
|
||||
printf '%s' "$build_number" | tr -c 'A-Za-z0-9.+~' '.'
|
||||
}
|
||||
|
||||
arch_package_suffix() {
|
||||
printf '%s' "$build_number" | tr -c 'A-Za-z0-9.+_' '.'
|
||||
}
|
||||
|
||||
deb_arch_for_target() {
|
||||
case "$1" in
|
||||
x86_64-unknown-linux-gnu) printf 'amd64' ;;
|
||||
aarch64-unknown-linux-gnu) printf 'arm64' ;;
|
||||
riscv64gc-unknown-linux-gnu) printf 'riscv64' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
rpm_arch_for_target() {
|
||||
case "$1" in
|
||||
x86_64-unknown-linux-gnu) printf 'x86_64' ;;
|
||||
aarch64-unknown-linux-gnu) printf 'aarch64' ;;
|
||||
riscv64gc-unknown-linux-gnu) printf 'riscv64' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
pacman_arch_for_target() {
|
||||
case "$1" in
|
||||
x86_64-unknown-linux-gnu) printf 'x86_64' ;;
|
||||
aarch64-unknown-linux-gnu) printf 'aarch64' ;;
|
||||
riscv64gc-unknown-linux-gnu) printf 'riscv64' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
copy_license_readme() {
|
||||
local dst="$1"
|
||||
cp README.md "$dst/README.md"
|
||||
if [[ -f LICENSE ]]; then
|
||||
cp LICENSE "$dst/LICENSE"
|
||||
elif [[ -f LICENSE.md ]]; then
|
||||
cp LICENSE.md "$dst/LICENSE.md"
|
||||
fi
|
||||
}
|
||||
|
||||
package_deb() {
|
||||
local target binary deb_arch pkg_version suffix staging deb_file installed_size
|
||||
target="$1"
|
||||
binary="$2"
|
||||
if ! deb_arch="$(deb_arch_for_target "$target")"; then
|
||||
echo "warning: no Debian architecture mapping for ${target}; skipping .deb package" >&2
|
||||
return 0
|
||||
fi
|
||||
if ! command -v dpkg-deb >/dev/null 2>&1; then
|
||||
echo "warning: dpkg-deb is not available; skipping .deb package" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
suffix="$(package_release_suffix)"
|
||||
pkg_version="${BURROW_PACKAGE_VERSION:-$(burrow_crate_version)+${suffix}}"
|
||||
staging="${out_root}/linux/pkg-deb/burrow_${pkg_version}_${deb_arch}"
|
||||
rm -rf "$staging"
|
||||
mkdir -p \
|
||||
"$staging/DEBIAN" \
|
||||
"$staging/usr/bin" \
|
||||
"$staging/usr/lib/systemd/system" \
|
||||
"$staging/usr/share/doc/burrow" \
|
||||
"$staging/usr/share/licenses/burrow"
|
||||
|
||||
install -m 0755 "$binary" "$staging/usr/bin/burrow"
|
||||
install -m 0644 systemd/burrow.service "$staging/usr/lib/systemd/system/burrow.service"
|
||||
install -m 0644 systemd/burrow.socket "$staging/usr/lib/systemd/system/burrow.socket"
|
||||
install -m 0644 README.md "$staging/usr/share/doc/burrow/README.md"
|
||||
install -m 0644 LICENSE.md "$staging/usr/share/licenses/burrow/LICENSE.md"
|
||||
installed_size="$(du -sk "$staging/usr" | awk '{ print $1 }')"
|
||||
|
||||
cat > "$staging/DEBIAN/control" <<EOF
|
||||
Package: burrow
|
||||
Version: ${pkg_version}
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: ${deb_arch}
|
||||
Maintainer: Burrow Release Automation <packages@burrow.net>
|
||||
Installed-Size: ${installed_size}
|
||||
Depends: systemd
|
||||
Description: Burrow VPN daemon and CLI
|
||||
Burrow installs the daemon entrypoint and systemd socket used by the desktop UI.
|
||||
EOF
|
||||
|
||||
deb_file="${out_root}/linux/burrow_${pkg_version}_${deb_arch}.deb"
|
||||
dpkg-deb --build --root-owner-group "$staging" "$deb_file"
|
||||
sha256_file "$deb_file"
|
||||
}
|
||||
|
||||
package_rpm() {
|
||||
local target binary rpm_arch
|
||||
target="$1"
|
||||
binary="$2"
|
||||
if ! rpm_arch="$(rpm_arch_for_target "$target")"; then
|
||||
echo "warning: no RPM architecture mapping for ${target}; skipping .rpm package" >&2
|
||||
return 0
|
||||
fi
|
||||
if ! command -v cargo-generate-rpm >/dev/null 2>&1; then
|
||||
echo "warning: cargo-generate-rpm is not available; skipping .rpm package" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p target/release
|
||||
cp "$binary" target/release/burrow
|
||||
if cargo generate-rpm -p burrow; then
|
||||
find target/generate-rpm -type f -name '*.rpm' -exec cp {} "${out_root}/linux/" \; 2>/dev/null || true
|
||||
find "${out_root}/linux" -maxdepth 1 -type f -name '*.rpm' -print | while read -r rpm; do
|
||||
sha256_file "$rpm"
|
||||
done
|
||||
else
|
||||
echo "warning: cargo-generate-rpm failed for ${rpm_arch}; skipping .rpm package" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
package_pacman() {
|
||||
local target binary pacman_arch pkg_version pkgrel work source_sum service_sum socket_sum readme_sum license_sum
|
||||
target="$1"
|
||||
binary="$2"
|
||||
if ! pacman_arch="$(pacman_arch_for_target "$target")"; then
|
||||
echo "warning: no pacman architecture mapping for ${target}; skipping Arch package" >&2
|
||||
return 0
|
||||
fi
|
||||
if ! command -v makepkg >/dev/null 2>&1; then
|
||||
echo "warning: makepkg is not available; skipping Arch package" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
pkg_version="${BURROW_ARCH_PKGVER:-$(burrow_crate_version)_$(arch_package_suffix)}"
|
||||
pkgrel="${BURROW_ARCH_PKGREL:-1}"
|
||||
work="${out_root}/linux/pkg-arch/${pacman_arch}"
|
||||
rm -rf "$work"
|
||||
mkdir -p "$work"
|
||||
install -m 0755 "$binary" "$work/burrow"
|
||||
install -m 0644 systemd/burrow.service "$work/burrow.service"
|
||||
install -m 0644 systemd/burrow.socket "$work/burrow.socket"
|
||||
install -m 0644 README.md "$work/README.md"
|
||||
install -m 0644 LICENSE.md "$work/LICENSE.md"
|
||||
|
||||
source_sum="$(sha256sum "$work/burrow" | awk '{ print $1 }')"
|
||||
service_sum="$(sha256sum "$work/burrow.service" | awk '{ print $1 }')"
|
||||
socket_sum="$(sha256sum "$work/burrow.socket" | awk '{ print $1 }')"
|
||||
readme_sum="$(sha256sum "$work/README.md" | awk '{ print $1 }')"
|
||||
license_sum="$(sha256sum "$work/LICENSE.md" | awk '{ print $1 }')"
|
||||
|
||||
cat > "$work/PKGBUILD" <<EOF
|
||||
# Maintainer: Burrow Release Automation <packages@burrow.net>
|
||||
pkgname=burrow
|
||||
pkgver=${pkg_version}
|
||||
pkgrel=${pkgrel}
|
||||
pkgdesc="Burrow VPN daemon and CLI"
|
||||
arch=("${pacman_arch}")
|
||||
url="https://git.burrow.net/hackclub/burrow"
|
||||
license=("GPL-3.0-or-later")
|
||||
depends=("systemd")
|
||||
source=("burrow" "burrow.service" "burrow.socket" "README.md" "LICENSE.md")
|
||||
sha256sums=("${source_sum}" "${service_sum}" "${socket_sum}" "${readme_sum}" "${license_sum}")
|
||||
|
||||
package() {
|
||||
install -Dm755 "\${srcdir}/burrow" "\${pkgdir}/usr/bin/burrow"
|
||||
install -Dm644 "\${srcdir}/burrow.service" "\${pkgdir}/usr/lib/systemd/system/burrow.service"
|
||||
install -Dm644 "\${srcdir}/burrow.socket" "\${pkgdir}/usr/lib/systemd/system/burrow.socket"
|
||||
install -Dm644 "\${srcdir}/README.md" "\${pkgdir}/usr/share/doc/burrow/README.md"
|
||||
install -Dm644 "\${srcdir}/LICENSE.md" "\${pkgdir}/usr/share/licenses/burrow/LICENSE.md"
|
||||
}
|
||||
EOF
|
||||
|
||||
(
|
||||
cd "$work"
|
||||
makepkg --force --nodeps --noconfirm
|
||||
)
|
||||
find "$work" -maxdepth 1 -type f -name '*.pkg.tar.*' -exec cp {} "${out_root}/linux/" \;
|
||||
find "${out_root}/linux" -maxdepth 1 -type f -name '*.pkg.tar.*' -print | while read -r package; do
|
||||
sha256_file "$package"
|
||||
done
|
||||
}
|
||||
|
||||
package_linux() {
|
||||
local target host archive staging
|
||||
host="$(rustc -vV | awk '/^host:/ { print $2 }')"
|
||||
target="${BURROW_LINUX_TARGET:-$host}"
|
||||
if [[ "$target" != *-linux-* && -z "${BURROW_LINUX_TARGET:-}" ]]; then
|
||||
mkdir -p "${out_root}/linux"
|
||||
echo "warning: host target ${target} is not Linux; skipping Linux artifact on this host" >&2
|
||||
printf 'Linux build was skipped on host target %s. Run this lane on a Linux Namespace runner or set BURROW_LINUX_TARGET explicitly.\n' "$target" > "${out_root}/linux/README.txt"
|
||||
return 0
|
||||
fi
|
||||
staging="${out_root}/linux/burrow-${build_number}-${target}"
|
||||
mkdir -p "$staging"
|
||||
|
||||
cargo build --locked --release -p burrow --bin burrow --target "$target"
|
||||
install -m 0755 "target/${target}/release/burrow" "$staging/burrow"
|
||||
copy_license_readme "$staging"
|
||||
|
||||
archive="${out_root}/linux/burrow-${build_number}-${target}.tar.gz"
|
||||
tar -C "${out_root}/linux" -czf "$archive" "$(basename "$staging")"
|
||||
sha256_file "$archive"
|
||||
|
||||
package_deb "$target" "target/${target}/release/burrow"
|
||||
package_rpm "$target" "target/${target}/release/burrow"
|
||||
package_pacman "$target" "target/${target}/release/burrow"
|
||||
}
|
||||
|
||||
package_windows_stub_unix() {
|
||||
local target exe archive staging
|
||||
target="${BURROW_WINDOWS_TARGET:-x86_64-pc-windows-gnu}"
|
||||
staging="${out_root}/windows/burrow-${build_number}-${target}"
|
||||
mkdir -p "$staging"
|
||||
|
||||
target_libdir="$(rustc --print target-libdir --target "$target" 2>/dev/null || true)"
|
||||
if [[ -z "$target_libdir" || ! -d "$target_libdir" ]]; then
|
||||
echo "warning: Rust target ${target} is not installed on this host" >&2
|
||||
printf 'Windows stub build for %s was skipped on this host. Use the Namespace Windows lane for the native artifact.\n' "$target" > "${out_root}/windows/README.txt"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if cargo build --locked --release -p burrow-windows-stub --target "$target"; then
|
||||
exe="target/${target}/release/burrow.exe"
|
||||
else
|
||||
echo "warning: unable to build the Windows release stub on this host" >&2
|
||||
printf 'Windows stub build for %s was skipped on this host. Use the Namespace Windows lane for the native artifact.\n' "$target" > "${out_root}/windows/README.txt"
|
||||
return 0
|
||||
fi
|
||||
|
||||
install -m 0755 "$exe" "$staging/burrow.exe"
|
||||
copy_license_readme "$staging"
|
||||
printf 'Burrow Windows stub build %s\n' "$build_number" > "$staging/README-Windows.txt"
|
||||
|
||||
archive="${out_root}/windows/burrow-${build_number}-${target}.zip"
|
||||
(cd "${out_root}/windows" && zip -qr "$archive" "$(basename "$staging")")
|
||||
sha256_file "$archive"
|
||||
}
|
||||
|
||||
package_sparkle_unsigned() {
|
||||
local macos_dir appcast channel dmg
|
||||
channel="${SPARKLE_CHANNEL:-build-${build_number}}"
|
||||
macos_dir="${out_root}/macos"
|
||||
appcast="${out_root}/sparkle/${channel}/appcast.xml"
|
||||
mkdir -p "$(dirname "$appcast")"
|
||||
|
||||
dmg="$(find "$macos_dir" -maxdepth 1 -type f -name '*.dmg' -print 2>/dev/null | sort | tail -n 1 || true)"
|
||||
if [[ -z "$dmg" ]]; then
|
||||
echo "No macOS DMG exists yet; Sparkle appcast generation is waiting on the macOS signed artifact." > "${out_root}/sparkle/${channel}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local name length pubdate url
|
||||
name="$(basename "$dmg")"
|
||||
length="$(wc -c < "$dmg" | tr -d ' ')"
|
||||
pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
||||
url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/${name}"
|
||||
cat > "$appcast" <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
|
||||
<channel>
|
||||
<title>Burrow ${channel}</title>
|
||||
<item>
|
||||
<title>Burrow ${build_number}</title>
|
||||
<pubDate>${pubdate}</pubDate>
|
||||
<sparkle:version>${build_number}</sparkle:version>
|
||||
<sparkle:shortVersionString>0.1</sparkle:shortVersionString>
|
||||
<enclosure url="${url}" sparkle:version="${build_number}" length="${length}" type="application/octet-stream" />
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
EOF
|
||||
cp "$dmg" "${out_root}/sparkle/${channel}/"
|
||||
}
|
||||
|
||||
case "$platform" in
|
||||
all)
|
||||
package_linux
|
||||
package_windows_stub_unix
|
||||
if [[ "$(uname -s)" == "Darwin" ]] && command -v xcodebuild >/dev/null 2>&1; then
|
||||
Scripts/ci/package-apple-artifacts.sh all
|
||||
else
|
||||
mkdir -p "${out_root}/apple"
|
||||
printf 'Apple artifacts require a macOS runner with Xcode. The release workflow builds them in the dedicated Apple lane.\n' > "${out_root}/apple/README.txt"
|
||||
fi
|
||||
package_sparkle_unsigned
|
||||
;;
|
||||
linux)
|
||||
package_linux
|
||||
;;
|
||||
windows-stub|windows)
|
||||
package_windows_stub_unix
|
||||
;;
|
||||
sparkle)
|
||||
package_sparkle_unsigned
|
||||
;;
|
||||
apple)
|
||||
Scripts/ci/package-apple-artifacts.sh all
|
||||
;;
|
||||
ios|macos)
|
||||
Scripts/ci/package-apple-artifacts.sh "$platform"
|
||||
;;
|
||||
*)
|
||||
echo "unknown release platform: $platform" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
find "$out_root" -type f -print | sort
|
||||
|
|
@ -7,7 +7,8 @@ set -euo pipefail
|
|||
: "${TOKEN:?TOKEN is required}"
|
||||
|
||||
release_api="${API_URL}/repos/${REPOSITORY}/releases"
|
||||
tag_api="${release_api}/tags/${RELEASE_TAG}"
|
||||
encoded_release_tag="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "${RELEASE_TAG}")"
|
||||
tag_api="${release_api}/tags/${encoded_release_tag}"
|
||||
release_json="$(mktemp)"
|
||||
create_json="$(mktemp)"
|
||||
trap 'rm -f "${release_json}" "${create_json}"' EXIT
|
||||
|
|
@ -50,6 +51,9 @@ if [[ -z "${release_id}" || "${release_id}" == "null" ]]; then
|
|||
fi
|
||||
|
||||
for file in dist/*; do
|
||||
if [[ ! -f "${file}" ]]; then
|
||||
continue
|
||||
fi
|
||||
name="$(basename "${file}")"
|
||||
asset_id="$(jq -r --arg name "${name}" '.assets[]? | select(.name == $name) | .id' "${release_json}" | head -n1)"
|
||||
if [[ -n "${asset_id}" ]]; then
|
||||
|
|
|
|||
49
Scripts/ci/publish-nix-cache.sh
Executable file
49
Scripts/ci/publish-nix-cache.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
cache_server_name="${BURROW_NIX_CACHE_SERVER_NAME:-burrow}"
|
||||
cache_server_url="${BURROW_NIX_CACHE_SERVER_URL:-https://nix.burrow.net}"
|
||||
cache_name="${BURROW_NIX_CACHE_NAME:-burrow}"
|
||||
cache_ref="${cache_server_name}:${cache_name}"
|
||||
token="${BURROW_NIX_CACHE_PUSH_TOKEN:-}"
|
||||
|
||||
if [[ -z "$token" ]]; then
|
||||
if [[ "${BURROW_NIX_CACHE_REQUIRED:-false}" == "true" ]]; then
|
||||
echo "BURROW_NIX_CACHE_PUSH_TOKEN is required to publish Nix cache paths" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "::notice ::BURROW_NIX_CACHE_PUSH_TOKEN is not set; skipping Nix cache publish."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v attic >/dev/null 2>&1; then
|
||||
echo "attic is required to publish Nix cache paths" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
if ! command -v nix >/dev/null 2>&1; then
|
||||
echo "nix is required to build paths for cache publishing" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
attic login "$cache_server_name" "$cache_server_url" "$token" --set-default
|
||||
|
||||
push_inputs=("$@")
|
||||
if [[ "${#push_inputs[@]}" -eq 0 ]]; then
|
||||
read -r -a attrs <<< "${BURROW_NIX_CACHE_ATTRS:-.#burrow .#forgejo-nsc-dispatcher .#forgejo-nsc-autoscaler}"
|
||||
if [[ "${#attrs[@]}" -eq 0 ]]; then
|
||||
echo "no Nix attrs were supplied for cache publishing" >&2
|
||||
exit 1
|
||||
fi
|
||||
mapfile -t push_inputs < <(nix build --no-link --print-out-paths "${attrs[@]}")
|
||||
fi
|
||||
|
||||
if [[ "${#push_inputs[@]}" -eq 0 ]]; then
|
||||
echo "no Nix output paths were produced for cache publishing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
attic push "$cache_ref" "${push_inputs[@]}"
|
||||
56
Scripts/ci/resolve-nix-bin.sh
Executable file
56
Scripts/ci/resolve-nix-bin.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
find_nix_bin() {
|
||||
local candidate
|
||||
for candidate in \
|
||||
"${NIX_BIN:-}" \
|
||||
"$(command -v nix 2>/dev/null || true)" \
|
||||
"${HOME:-}/.nix-profile/bin/nix" \
|
||||
"${HOME:-}/.local/state/nix/profile/bin/nix" \
|
||||
"/nix/var/nix/profiles/per-user/${USER:-runner}/profile/bin/nix" \
|
||||
/nix/var/nix/profiles/default/bin/nix \
|
||||
/run/current-system/sw/bin/nix \
|
||||
/etc/profiles/per-user/runner/bin/nix \
|
||||
/Users/runner/.nix-profile/bin/nix \
|
||||
/Users/runner/.local/state/nix/profile/bin/nix \
|
||||
/home/runner/.nix-profile/bin/nix \
|
||||
/home/runner/.local/state/nix/profile/bin/nix \
|
||||
/nix/profile/bin/nix; do
|
||||
if [[ -n "$candidate" && -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
candidate="$(
|
||||
find \
|
||||
"${HOME:-/nonexistent}" \
|
||||
/Users/runner \
|
||||
/home/runner \
|
||||
/nix/var/nix/profiles \
|
||||
/etc/profiles/per-user \
|
||||
-path '*/bin/nix' -type f 2>/dev/null | head -n 1 || true
|
||||
)"
|
||||
if [[ -n "$candidate" && -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if nix_bin="$(find_nix_bin)"; then
|
||||
printf '%s\n' "$nix_bin"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if bash Scripts/ci/ensure-nix.sh >/dev/null 2>&1; then
|
||||
if nix_bin="$(find_nix_bin)"; then
|
||||
printf '%s\n' "$nix_bin"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "::error ::nix is required but is not on PATH and no standard install path exists" >&2
|
||||
exit 1
|
||||
72
Scripts/ci/sync-apple-provisioning-profiles.sh
Executable file
72
Scripts/ci/sync-apple-provisioning-profiles.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$repo_root"
|
||||
|
||||
platform="${1:-all}"
|
||||
out_dir="${BURROW_APPLE_PROFILES_OUT_DIR:-.forgejo/actions/export}"
|
||||
app_id="${BURROW_APPLE_BUNDLE_ID:-com.hackclub.burrow}"
|
||||
network_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${app_id}.network}"
|
||||
replace_certificate_mismatch="${BURROW_APPLE_REPLACE_CERTIFICATE_MISMATCH:-false}"
|
||||
if [[ -n "${BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID:-}" || -n "${BURROW_DEVELOPER_ID_CERTIFICATE_ID:-}" ]]; then
|
||||
replace_certificate_mismatch="${BURROW_APPLE_REPLACE_CERTIFICATE_MISMATCH:-true}"
|
||||
fi
|
||||
|
||||
key_id="${ASC_API_KEY_ID:-${APPSTORE_KEY_ID:-}}"
|
||||
issuer_id="${ASC_API_ISSUER_ID:-${APPSTORE_KEY_ISSUER_ID:-}}"
|
||||
key_path="${ASC_API_KEY_PATH:-${APPSTORE_KEY_PATH:-}}"
|
||||
key_base64="${ASC_API_KEY_BASE64:-}"
|
||||
|
||||
if [[ -z "$key_id" || -z "$issuer_id" ]]; then
|
||||
echo "::notice ::App Store Connect credentials are not available; provisioning profile sync skipped."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
temp_key=""
|
||||
cleanup() {
|
||||
[[ -z "$temp_key" ]] || rm -f "$temp_key"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -z "$key_path" ]]; then
|
||||
if [[ -z "$key_base64" ]]; then
|
||||
echo "::notice ::ASC_API_KEY_PATH/ASC_API_KEY_BASE64 is not available; provisioning profile sync skipped."
|
||||
exit 0
|
||||
fi
|
||||
temp_key="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-asc-key.XXXXXX.p8")"
|
||||
printf '%s' "$key_base64" | base64 --decode > "$temp_key"
|
||||
chmod 600 "$temp_key"
|
||||
key_path="$temp_key"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$key_path" ]]; then
|
||||
echo "::error ::App Store Connect key path does not exist: ${key_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$out_dir"
|
||||
profile_args=(
|
||||
--platform "$platform"
|
||||
--key-id "$key_id"
|
||||
--issuer-id "$issuer_id"
|
||||
--key-file "$key_path"
|
||||
--out-dir "$out_dir"
|
||||
--app-id "$app_id"
|
||||
--network-id "$network_id"
|
||||
--create-missing-bundle-ids "${BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS:-false}"
|
||||
--enable-capabilities "${BURROW_APPLE_ENABLE_CAPABILITIES:-false}"
|
||||
--replace-certificate-mismatch "$replace_certificate_mismatch"
|
||||
--bundle-platform "${BURROW_APPLE_BUNDLE_PLATFORM:-UNIVERSAL}"
|
||||
--associated-domains "${BURROW_APPLE_ASSOCIATED_DOMAINS:-applinks:burrow.rs?mode=developer,webcredentials:burrow.rs?mode=developer}"
|
||||
)
|
||||
|
||||
if [[ -n "${BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID:-}" ]]; then
|
||||
profile_args+=(--ios-certificate-id "$BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID")
|
||||
fi
|
||||
|
||||
if [[ -n "${BURROW_DEVELOPER_ID_CERTIFICATE_ID:-}" ]]; then
|
||||
profile_args+=(--macos-certificate-id "$BURROW_DEVELOPER_ID_CERTIFICATE_ID")
|
||||
fi
|
||||
|
||||
node Scripts/apple/sync-provisioning-profiles.mjs "${profile_args[@]}"
|
||||
35
Scripts/ci/upload-package-garage.sh
Executable file
35
Scripts/ci/upload-package-garage.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BURROW_GARAGE_ENDPOINT:?BURROW_GARAGE_ENDPOINT is required}"
|
||||
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required for Garage upload}"
|
||||
: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required for Garage upload}"
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
source_dir="${BURROW_PACKAGE_REPOSITORY_DIR:-${repo_root}/publish/repositories}"
|
||||
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "package repository directory not found: $source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
echo "aws CLI is required for Garage package repository upload" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
bucket="${BURROW_PACKAGE_GARAGE_BUCKET:-burrow-packages}"
|
||||
prefix="${BURROW_PACKAGE_GARAGE_PREFIX:-${BURROW_PACKAGE_GCS_PREFIX:-}}"
|
||||
region="${BURROW_GARAGE_REGION:-garage}"
|
||||
destination="s3://${bucket}"
|
||||
|
||||
if [[ -n "$prefix" ]]; then
|
||||
destination="${destination}/${prefix}"
|
||||
fi
|
||||
|
||||
export AWS_DEFAULT_REGION="$region"
|
||||
export AWS_EC2_METADATA_DISABLED=true
|
||||
|
||||
aws --endpoint-url "$BURROW_GARAGE_ENDPOINT" s3 sync "$source_dir" "$destination" --no-progress
|
||||
28
Scripts/ci/upload-package-repositories.sh
Executable file
28
Scripts/ci/upload-package-repositories.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
prefix="${BURROW_PACKAGE_GCS_PREFIX:-}"
|
||||
source_dir="${BURROW_PACKAGE_REPOSITORY_DIR:-${repo_root}/publish/repositories}"
|
||||
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "package repository directory not found: $source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bucket="${BURROW_PACKAGE_GCS_BUCKET:-burrow-net-packages}"
|
||||
if ! command -v gcloud >/dev/null 2>&1; then
|
||||
echo "gcloud is required for GCS package repository upload" >&2
|
||||
exit 127
|
||||
fi
|
||||
gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q . || {
|
||||
echo "gcloud has no active account; run Scripts/ci/google-wif-auth.sh first" >&2
|
||||
exit 1
|
||||
}
|
||||
destination="gs://${bucket}"
|
||||
if [[ -n "$prefix" ]]; then
|
||||
destination="${destination}/${prefix}"
|
||||
fi
|
||||
gcloud storage rsync --recursive "$source_dir" "$destination"
|
||||
39
Scripts/ci/upload-package-storage.sh
Executable file
39
Scripts/ci/upload-package-storage.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
uploaded=0
|
||||
garage_configured=0
|
||||
|
||||
if [[ -n "${BURROW_GARAGE_ENDPOINT:-}" && -n "${AWS_ACCESS_KEY_ID:-}" && -n "${AWS_SECRET_ACCESS_KEY:-}" ]]; then
|
||||
garage_configured=1
|
||||
fi
|
||||
|
||||
if [[ "$garage_configured" == "1" ]]; then
|
||||
Scripts/ci/upload-package-garage.sh
|
||||
uploaded=1
|
||||
elif [[ "${BURROW_PACKAGE_REQUIRE_GARAGE:-false}" == "true" ]]; then
|
||||
echo "Garage package repository upload is required, but BURROW_GARAGE_ENDPOINT or AWS credentials are missing" >&2
|
||||
exit 1
|
||||
else
|
||||
echo "::notice ::Garage package repository upload not configured; skipping primary S3-compatible target."
|
||||
fi
|
||||
|
||||
if [[ "${BURROW_PACKAGE_GCS_BACKUP:-true}" != "false" ]]; then
|
||||
if ! command -v gcloud >/dev/null 2>&1; then
|
||||
echo "gcloud is required for GCS package repository backup" >&2
|
||||
exit 127
|
||||
fi
|
||||
if ! gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q .; then
|
||||
Scripts/ci/google-wif-auth.sh
|
||||
fi
|
||||
Scripts/ci/upload-package-repositories.sh
|
||||
uploaded=1
|
||||
fi
|
||||
|
||||
if [[ "$uploaded" != "1" ]]; then
|
||||
echo "no package repository storage targets were enabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
31
Scripts/ci/upload-release-garage.sh
Executable file
31
Scripts/ci/upload-release-garage.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
|
||||
: "${BURROW_GARAGE_ENDPOINT:?BURROW_GARAGE_ENDPOINT is required}"
|
||||
: "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required for Garage upload}"
|
||||
: "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is required for Garage upload}"
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
source_dir="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${BUILD_NUMBER}}"
|
||||
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "release artifact directory not found: $source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
echo "aws CLI is required for Garage release upload" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
bucket="${BURROW_RELEASE_GARAGE_BUCKET:-burrow-releases}"
|
||||
region="${BURROW_GARAGE_REGION:-garage}"
|
||||
destination="s3://${bucket}/builds/${BUILD_NUMBER}"
|
||||
|
||||
export AWS_DEFAULT_REGION="$region"
|
||||
export AWS_EC2_METADATA_DISABLED=true
|
||||
|
||||
aws --endpoint-url "$BURROW_GARAGE_ENDPOINT" s3 sync "$source_dir" "$destination" --no-progress
|
||||
25
Scripts/ci/upload-release-gcs.sh
Executable file
25
Scripts/ci/upload-release-gcs.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
source_dir="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${BUILD_NUMBER}}"
|
||||
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "release artifact directory not found: $source_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bucket="${BURROW_RELEASE_GCS_BUCKET:-burrow-net-releases}"
|
||||
if ! command -v gcloud >/dev/null 2>&1; then
|
||||
echo "gcloud is required for GCS release upload" >&2
|
||||
exit 127
|
||||
fi
|
||||
gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q . || {
|
||||
echo "gcloud has no active account; run Scripts/ci/google-wif-auth.sh first" >&2
|
||||
exit 1
|
||||
}
|
||||
gcloud storage rsync --recursive "$source_dir" "gs://${bucket}/builds/${BUILD_NUMBER}"
|
||||
41
Scripts/ci/upload-release-storage.sh
Executable file
41
Scripts/ci/upload-release-storage.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${repo_root}"
|
||||
|
||||
uploaded=0
|
||||
garage_configured=0
|
||||
|
||||
if [[ -n "${BURROW_GARAGE_ENDPOINT:-}" && -n "${AWS_ACCESS_KEY_ID:-}" && -n "${AWS_SECRET_ACCESS_KEY:-}" ]]; then
|
||||
garage_configured=1
|
||||
fi
|
||||
|
||||
if [[ "$garage_configured" == "1" ]]; then
|
||||
Scripts/ci/upload-release-garage.sh
|
||||
uploaded=1
|
||||
elif [[ "${BURROW_RELEASE_REQUIRE_GARAGE:-false}" == "true" ]]; then
|
||||
echo "Garage release upload is required, but BURROW_GARAGE_ENDPOINT or AWS credentials are missing" >&2
|
||||
exit 1
|
||||
else
|
||||
echo "::notice ::Garage release upload not configured; skipping primary S3-compatible target."
|
||||
fi
|
||||
|
||||
if [[ "${BURROW_RELEASE_GCS_BACKUP:-true}" != "false" ]]; then
|
||||
if ! command -v gcloud >/dev/null 2>&1; then
|
||||
echo "gcloud is required for GCS release backup" >&2
|
||||
exit 127
|
||||
fi
|
||||
if ! gcloud auth list --filter=status:ACTIVE --format='value(account)' | grep -q .; then
|
||||
Scripts/ci/google-wif-auth.sh
|
||||
fi
|
||||
Scripts/ci/upload-release-gcs.sh
|
||||
uploaded=1
|
||||
fi
|
||||
|
||||
if [[ "$uploaded" != "1" ]]; then
|
||||
echo "no release storage targets were enabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
205
Scripts/forgejo-dispatch-via-host.sh
Executable file
205
Scripts/forgejo-dispatch-via-host.sh
Executable file
|
|
@ -0,0 +1,205 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/forgejo-dispatch-via-host.sh <workflow> [--ref <ref>] [--inputs <json>] [--inputs-file <path>] [--return-run-info]
|
||||
|
||||
Dispatch a Burrow Forgejo workflow by SSHing to the live forge host and using
|
||||
the managed dispatcher token from the host runtime.
|
||||
|
||||
Environment:
|
||||
FORGEJO_BASE_URL Default: https://git.burrow.net
|
||||
FORGEJO_REPO Default: hackclub/burrow
|
||||
BURROW_FORGE_HOST Default: root@git.burrow.net
|
||||
BURROW_FORGE_HOST_ADDRESS Optional SSH HostName override
|
||||
BURROW_FORGE_SSH_KEY Optional explicit SSH private key
|
||||
AGE_FORGE_SSH_KEY Optional injected SSH private key material
|
||||
BURROW_FORGE_DISPATCHER_CONFIG_FILE Default: /run/agenix/burrowForgejoNscDispatcherConfig
|
||||
EOF
|
||||
}
|
||||
|
||||
base_url="${FORGEJO_BASE_URL:-https://git.burrow.net}"
|
||||
repo_slug="${FORGEJO_REPO:-hackclub/burrow}"
|
||||
forge_host="${BURROW_FORGE_HOST:-root@git.burrow.net}"
|
||||
forge_host_address="${BURROW_FORGE_HOST_ADDRESS:-}"
|
||||
dispatcher_config_file="${BURROW_FORGE_DISPATCHER_CONFIG_FILE:-/run/agenix/burrowForgejoNscDispatcherConfig}"
|
||||
workflow=""
|
||||
ref="main"
|
||||
inputs_json=""
|
||||
return_run_info="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--ref)
|
||||
ref="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--inputs)
|
||||
inputs_json="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--inputs-file)
|
||||
inputs_json="$(cat "${2:-}")"
|
||||
shift 2
|
||||
;;
|
||||
--return-run-info)
|
||||
return_run_info="true"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$workflow" ]]; then
|
||||
workflow="$1"
|
||||
shift
|
||||
else
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$workflow" ]]; then
|
||||
echo "Missing workflow name or file, for example build-release.yml." >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
resolved_forge_ssh_key="$("${script_dir}/resolve-forge-ssh-key.sh")"
|
||||
eval "$resolved_forge_ssh_key"
|
||||
ssh_key="${BURROW_FORGE_SSH_KEY}"
|
||||
|
||||
if [[ -n "$inputs_json" ]]; then
|
||||
if ! python3 - "$inputs_json" <<'PY' >/dev/null
|
||||
import json
|
||||
import sys
|
||||
|
||||
value = json.loads(sys.argv[1])
|
||||
if not isinstance(value, dict):
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
then
|
||||
echo "--inputs must be a JSON object." >&2
|
||||
exit 64
|
||||
fi
|
||||
fi
|
||||
|
||||
workflow_path="$(python3 - "$workflow" <<'PY'
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
print(urllib.parse.quote(sys.argv[1], safe=""))
|
||||
PY
|
||||
)"
|
||||
|
||||
payload="$(python3 - "$ref" "$inputs_json" "$return_run_info" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
ref = sys.argv[1]
|
||||
inputs_arg = sys.argv[2]
|
||||
return_run_info = sys.argv[3] == "true"
|
||||
|
||||
payload = {"ref": ref}
|
||||
if inputs_arg:
|
||||
inputs = json.loads(inputs_arg)
|
||||
payload["inputs"] = {key: str(value) for key, value in inputs.items()}
|
||||
if return_run_info:
|
||||
payload["return_run_info"] = True
|
||||
print(json.dumps(payload))
|
||||
PY
|
||||
)"
|
||||
|
||||
payload_b64="$(python3 - "$payload" <<'PY'
|
||||
import base64
|
||||
import sys
|
||||
|
||||
print(base64.b64encode(sys.argv[1].encode()).decode())
|
||||
PY
|
||||
)"
|
||||
|
||||
ssh_opts=(
|
||||
-i "$ssh_key"
|
||||
-o IdentitiesOnly=yes
|
||||
-o UserKnownHostsFile=/dev/null
|
||||
-o GlobalKnownHostsFile=/dev/null
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
)
|
||||
if [[ -n "$forge_host_address" ]]; then
|
||||
ssh_opts+=(-o "HostName=$forge_host_address")
|
||||
fi
|
||||
|
||||
response="$(
|
||||
ssh "${ssh_opts[@]}" "$forge_host" bash -s -- \
|
||||
"$base_url" "$repo_slug" "$workflow_path" "$dispatcher_config_file" "$payload_b64" <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
base_url="$1"
|
||||
repo_slug="$2"
|
||||
workflow_path="$3"
|
||||
dispatcher_config_file="$4"
|
||||
payload_b64="$5"
|
||||
|
||||
payload="$(python3 - "$payload_b64" <<'PY'
|
||||
import base64
|
||||
import sys
|
||||
|
||||
print(base64.b64decode(sys.argv[1]).decode())
|
||||
PY
|
||||
)"
|
||||
|
||||
if [[ ! -s "$dispatcher_config_file" ]]; then
|
||||
echo "Forge dispatcher config is missing or empty: $dispatcher_config_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
runtime_token="$(
|
||||
awk '
|
||||
/^[A-Za-z0-9_-]+:/ { in_forgejo = ($0 ~ /^forgejo:/); next }
|
||||
in_forgejo && /^[[:space:]]+token:/ {
|
||||
sub(/^[[:space:]]+token:[[:space:]]*/, "")
|
||||
gsub(/^"/, "")
|
||||
gsub(/"$/, "")
|
||||
gsub(/^\047/, "")
|
||||
gsub(/\047$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$dispatcher_config_file" | tr -d "\r\n"
|
||||
)"
|
||||
|
||||
if [[ -z "$runtime_token" ]]; then
|
||||
echo "Forge dispatcher token not found in $dispatcher_config_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
url="${base_url%/}/api/v1/repos/${repo_slug}/actions/workflows/${workflow_path}/dispatches"
|
||||
response_file="$(mktemp)"
|
||||
http_code="$(
|
||||
curl -sS -o "$response_file" -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${runtime_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary "$payload" \
|
||||
"$url"
|
||||
)"
|
||||
if [[ "$http_code" != 2* ]]; then
|
||||
cat "$response_file" >&2
|
||||
rm -f "$response_file"
|
||||
exit 22
|
||||
fi
|
||||
cat "$response_file"
|
||||
rm -f "$response_file"
|
||||
EOF
|
||||
)"
|
||||
|
||||
if [[ "$return_run_info" == "true" ]]; then
|
||||
printf '%s\n' "$response"
|
||||
else
|
||||
echo "Dispatched ${workflow} on ${ref} via ${forge_host}."
|
||||
fi
|
||||
240
Scripts/forgejo-dispatch.sh
Executable file
240
Scripts/forgejo-dispatch.sh
Executable file
|
|
@ -0,0 +1,240 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/forgejo-dispatch.sh <workflow> [--ref <ref>] [--inputs <json>] [--inputs-file <path>] [--return-run-info]
|
||||
|
||||
Dispatch a Burrow Forgejo workflow through the Forgejo API.
|
||||
|
||||
Environment:
|
||||
FORGEJO_BASE_URL Default: https://git.burrow.net
|
||||
FORGEJO_REPO Default: hackclub/burrow
|
||||
FORGEJO_PAT Token string
|
||||
FORGEJO_PAT_FILE Path to plaintext token
|
||||
FORGEJO_DISPATCH_HOST_FALLBACK auto, always, or never. Default: auto
|
||||
EOF
|
||||
}
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
base_url="${FORGEJO_BASE_URL:-https://git.burrow.net}"
|
||||
repo_slug="${FORGEJO_REPO:-hackclub/burrow}"
|
||||
host_fallback="${FORGEJO_DISPATCH_HOST_FALLBACK:-auto}"
|
||||
original_args=("$@")
|
||||
workflow=""
|
||||
ref="main"
|
||||
inputs_json=""
|
||||
return_run_info="false"
|
||||
|
||||
case "$host_fallback" in
|
||||
auto|always|never) ;;
|
||||
*)
|
||||
echo "FORGEJO_DISPATCH_HOST_FALLBACK must be auto, always, or never." >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
host_dispatch() {
|
||||
local reason="$1"
|
||||
local fallback="${repo_root}/Scripts/forgejo-dispatch-via-host.sh"
|
||||
|
||||
if [[ ! -x "$fallback" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Forgejo API dispatch ${reason}; retrying through the forge host." >&2
|
||||
exec "$fallback" "${original_args[@]}"
|
||||
}
|
||||
|
||||
if [[ "$host_fallback" == "always" ]]; then
|
||||
if ! host_dispatch "forced by FORGEJO_DISPATCH_HOST_FALLBACK=always"; then
|
||||
echo "Forge host dispatcher is not available." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--ref)
|
||||
ref="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--inputs)
|
||||
inputs_json="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--inputs-file)
|
||||
inputs_json="$(cat "${2:-}")"
|
||||
shift 2
|
||||
;;
|
||||
--return-run-info)
|
||||
return_run_info="true"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$workflow" ]]; then
|
||||
workflow="$1"
|
||||
shift
|
||||
else
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$workflow" ]]; then
|
||||
echo "Missing workflow name or file, for example build-release.yml." >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
extract_dispatcher_token() {
|
||||
python3 - "$1" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
in_forgejo = False
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if re.match(r"^[A-Za-z0-9_-]+:", line):
|
||||
in_forgejo = line.startswith("forgejo:")
|
||||
continue
|
||||
if not in_forgejo:
|
||||
continue
|
||||
match = re.match(r"\s+token:\s*['\"]?([^'\"]+)['\"]?\s*$", line)
|
||||
if match:
|
||||
print(match.group(1))
|
||||
break
|
||||
PY
|
||||
}
|
||||
|
||||
decrypt_age_secret() {
|
||||
local path="$1"
|
||||
|
||||
if [[ ! -f "$path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
nix run "${repo_root}#agenix" -- -d "$path" 2>/dev/null && return 0
|
||||
fi
|
||||
|
||||
if command -v agenix >/dev/null 2>&1; then
|
||||
agenix -d "$path" 2>/dev/null && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
pat="${FORGEJO_PAT:-}"
|
||||
pat_source="none"
|
||||
if [[ -n "$pat" ]]; then
|
||||
pat_source="FORGEJO_PAT"
|
||||
fi
|
||||
|
||||
if [[ -z "$pat" && -n "${FORGEJO_PAT_FILE:-}" && -f "$FORGEJO_PAT_FILE" ]]; then
|
||||
pat="$(tr -d '\r\n' < "$FORGEJO_PAT_FILE")"
|
||||
if [[ -n "$pat" ]]; then
|
||||
pat_source="FORGEJO_PAT_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$pat" ]]; then
|
||||
for default_file in \
|
||||
"${repo_root}/intake/forgejo_dispatch_pat.txt" \
|
||||
"${repo_root}/intake/forgejo_nsc_dispatcher.yaml"
|
||||
do
|
||||
if [[ -s "$default_file" ]]; then
|
||||
if [[ "$default_file" == *.yaml ]]; then
|
||||
pat="$(extract_dispatcher_token "$default_file" | tr -d '\r\n')"
|
||||
else
|
||||
pat="$(tr -d '\r\n' < "$default_file")"
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$pat" ]]; then
|
||||
pat_source="$default_file"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$pat" ]]; then
|
||||
decrypted_config="$(mktemp)"
|
||||
trap 'rm -f "$decrypted_config"' EXIT
|
||||
if decrypt_age_secret "${repo_root}/secrets/infra/forgejo-nsc-dispatcher-config.age" > "$decrypted_config"; then
|
||||
pat="$(extract_dispatcher_token "$decrypted_config" | tr -d '\r\n')"
|
||||
if [[ -n "$pat" ]]; then
|
||||
pat_source="secrets/infra/forgejo-nsc-dispatcher-config.age"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$pat" ]]; then
|
||||
if [[ "$host_fallback" == "auto" ]]; then
|
||||
if host_dispatch "has no local token"; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "Forgejo token not found. Set FORGEJO_PAT or FORGEJO_PAT_FILE." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$inputs_json" ]]; then
|
||||
if ! printf '%s\n' "$inputs_json" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
||||
echo "--inputs must be a JSON object." >&2
|
||||
exit 64
|
||||
fi
|
||||
inputs_json="$(printf '%s\n' "$inputs_json" | jq -c 'with_entries(.value |= tostring)')"
|
||||
payload="$(jq -n --arg ref "$ref" --argjson inputs "$inputs_json" --argjson return_run_info "$return_run_info" \
|
||||
'{ref:$ref, inputs:$inputs} + (if $return_run_info then {return_run_info:true} else {} end)')"
|
||||
else
|
||||
payload="$(jq -n --arg ref "$ref" --argjson return_run_info "$return_run_info" \
|
||||
'{ref:$ref} + (if $return_run_info then {return_run_info:true} else {} end)')"
|
||||
fi
|
||||
|
||||
workflow_path="$(python3 - "$workflow" <<'PY'
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
print(urllib.parse.quote(sys.argv[1], safe=""))
|
||||
PY
|
||||
)"
|
||||
|
||||
url="${base_url%/}/api/v1/repos/${repo_slug}/actions/workflows/${workflow_path}/dispatches"
|
||||
response_file="$(mktemp)"
|
||||
trap 'rm -f "$response_file" "${decrypted_config:-}"' EXIT
|
||||
|
||||
curl_status=0
|
||||
http_code="$(
|
||||
curl -sS -o "$response_file" -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${pat}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary "$payload" \
|
||||
"$url"
|
||||
)" || curl_status="$?"
|
||||
|
||||
if [[ "$curl_status" != "0" || "$http_code" != 2* ]]; then
|
||||
if [[ "$host_fallback" == "auto" && "$pat_source" != "FORGEJO_PAT" && ( "$http_code" == "401" || "$http_code" == "403" ) ]]; then
|
||||
if host_dispatch "returned HTTP ${http_code} with ${pat_source}"; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
cat "$response_file" >&2 || true
|
||||
if [[ "$curl_status" != "0" ]]; then
|
||||
exit "$curl_status"
|
||||
fi
|
||||
exit 22
|
||||
fi
|
||||
|
||||
cat "$response_file"
|
||||
if [[ "$return_run_info" != "true" ]]; then
|
||||
echo "Dispatched ${workflow} on ${ref}."
|
||||
fi
|
||||
41
Scripts/grafana-tofu.sh
Executable file
41
Scripts/grafana-tofu.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(
|
||||
git rev-parse --show-toplevel 2>/dev/null || {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
}
|
||||
)"
|
||||
|
||||
work_dir="${BURROW_GRAFANA_TOFU_DIR:-$ROOT/infra/grafana}"
|
||||
plugin_cache_dir="${BURROW_TOFU_PLUGIN_CACHE_DIR:-$ROOT/.cache/opentofu/plugins}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/grafana-tofu.sh [tofu args...]
|
||||
|
||||
Runs OpenTofu for Burrow Grafana API configuration. This stack creates folders
|
||||
and dashboards only; Nix owns the Grafana service, authentication, data sources,
|
||||
and boot-time provisioning files.
|
||||
|
||||
Examples:
|
||||
Scripts/grafana-tofu.sh init -backend-config=backend.hcl
|
||||
Scripts/grafana-tofu.sh plan -var-file=terraform.tfvars
|
||||
|
||||
Local syntax check:
|
||||
Scripts/grafana-tofu.sh init -backend=false
|
||||
Scripts/grafana-tofu.sh validate
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$plugin_cache_dir"
|
||||
export TF_PLUGIN_CACHE_DIR="$plugin_cache_dir"
|
||||
export TF_IN_AUTOMATION="${TF_IN_AUTOMATION:-true}"
|
||||
|
||||
exec nix shell --inputs-from "$ROOT" nixpkgs#opentofu -c tofu -chdir="$work_dir" "$@"
|
||||
41
Scripts/identity-tofu.sh
Executable file
41
Scripts/identity-tofu.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(
|
||||
git rev-parse --show-toplevel 2>/dev/null || {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
}
|
||||
)"
|
||||
|
||||
work_dir="${BURROW_IDENTITY_TOFU_DIR:-$ROOT/infra/identity}"
|
||||
plugin_cache_dir="${BURROW_TOFU_PLUGIN_CACHE_DIR:-$ROOT/.cache/opentofu/plugins}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/identity-tofu.sh [tofu args...]
|
||||
|
||||
Runs OpenTofu for Burrow Google KMS and Workload Identity Federation resources.
|
||||
Cloud provider credentials must come from the normal provider environment or a
|
||||
WIF flow; do not put credentials in backend config.
|
||||
|
||||
Examples:
|
||||
Scripts/identity-tofu.sh init -backend-config=backend.hcl
|
||||
Scripts/identity-tofu.sh plan -var-file=terraform.tfvars
|
||||
|
||||
Local syntax check:
|
||||
Scripts/identity-tofu.sh init -backend=false
|
||||
Scripts/identity-tofu.sh validate
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$plugin_cache_dir"
|
||||
export TF_PLUGIN_CACHE_DIR="$plugin_cache_dir"
|
||||
export TF_IN_AUTOMATION="${TF_IN_AUTOMATION:-true}"
|
||||
|
||||
exec nix shell --inputs-from "$ROOT" nixpkgs#opentofu -c tofu -chdir="$work_dir" "$@"
|
||||
41
Scripts/openbao-tofu.sh
Executable file
41
Scripts/openbao-tofu.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(
|
||||
git rev-parse --show-toplevel 2>/dev/null || {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
}
|
||||
)"
|
||||
|
||||
work_dir="${BURROW_OPENBAO_TOFU_DIR:-$ROOT/infra/openbao}"
|
||||
plugin_cache_dir="${BURROW_TOFU_PLUGIN_CACHE_DIR:-$ROOT/.cache/opentofu/plugins}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/openbao-tofu.sh [tofu args...]
|
||||
|
||||
Runs OpenTofu for Burrow OpenBao cloud seal and OpenBao API configuration. The
|
||||
Vault/OpenBao provider reads VAULT_TOKEN from the environment when managing
|
||||
OpenBao mounts, policies, or auth backends.
|
||||
|
||||
Examples:
|
||||
Scripts/openbao-tofu.sh init -backend-config=backend.hcl
|
||||
Scripts/openbao-tofu.sh plan -var-file=terraform.tfvars
|
||||
|
||||
Local syntax check:
|
||||
Scripts/openbao-tofu.sh init -backend=false
|
||||
Scripts/openbao-tofu.sh validate
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$plugin_cache_dir"
|
||||
export TF_PLUGIN_CACHE_DIR="$plugin_cache_dir"
|
||||
export TF_IN_AUTOMATION="${TF_IN_AUTOMATION:-true}"
|
||||
|
||||
exec nix shell --inputs-from "$ROOT" nixpkgs#opentofu -c tofu -chdir="$work_dir" "$@"
|
||||
78
Scripts/package/bootstrap-native-daemon.sh
Executable file
78
Scripts/package/bootstrap-native-daemon.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
apply=0
|
||||
prefer_packagekit=1
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/package/bootstrap-native-daemon.sh [--apply] [--no-packagekit]
|
||||
|
||||
Detects the host package manager and prints, or with --apply runs, the native
|
||||
install command for the Burrow daemon package. This is the host-side bootstrap
|
||||
equivalent the Flatpak GUI can point users at when PackageKit is unavailable.
|
||||
|
||||
This assumes the Burrow native repository for the detected package manager is
|
||||
already configured. Repository setup should be explicit because it establishes
|
||||
package trust roots.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apply)
|
||||
apply=1
|
||||
;;
|
||||
--no-packagekit)
|
||||
prefer_packagekit=0
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
run_or_print() {
|
||||
if [[ "$apply" == "1" ]]; then
|
||||
"$@"
|
||||
else
|
||||
printf '%q ' "$@"
|
||||
printf '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$prefer_packagekit" == "1" ]] && command -v pkcon >/dev/null 2>&1; then
|
||||
run_or_print pkcon install burrow
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
run_or_print sudo apt-get install burrow
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
run_or_print sudo dnf install burrow
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
run_or_print sudo zypper install burrow
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
run_or_print sudo pacman -S burrow
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
run_or_print sudo apk add burrow
|
||||
elif command -v xbps-install >/dev/null 2>&1; then
|
||||
run_or_print sudo xbps-install -S burrow
|
||||
elif command -v eopkg >/dev/null 2>&1; then
|
||||
run_or_print sudo eopkg install burrow
|
||||
elif command -v emerge >/dev/null 2>&1; then
|
||||
run_or_print sudo emerge --ask net-vpn/burrow
|
||||
elif command -v swupd >/dev/null 2>&1; then
|
||||
run_or_print sudo swupd bundle-add burrow
|
||||
elif command -v nix >/dev/null 2>&1; then
|
||||
run_or_print nix profile install git+https://git.burrow.net/hackclub/burrow
|
||||
else
|
||||
echo "No supported native package manager found. Install Burrow through DEB, RPM, pacman, AUR, Flatpak GUI plus daemon package, or the NixOS flake." >&2
|
||||
exit 69
|
||||
fi
|
||||
248
Scripts/package/build-repositories.sh
Executable file
248
Scripts/package/build-repositories.sh
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(
|
||||
git rev-parse --show-toplevel 2>/dev/null || {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
|
||||
pwd
|
||||
}
|
||||
)"
|
||||
|
||||
input_dir="${BURROW_PACKAGE_INPUT_DIR:-${repo_root}/dist/packages}"
|
||||
output_dir="${BURROW_PACKAGE_REPOSITORY_DIR:-${repo_root}/publish/repositories}"
|
||||
channel="${BURROW_PACKAGE_CHANNEL:-stable}"
|
||||
suite="${BURROW_APT_SUITE:-stable}"
|
||||
component="${BURROW_APT_COMPONENT:-main}"
|
||||
apt_arch="${BURROW_APT_ARCH:-${BURROW_PACKAGE_ARCH:-amd64}}"
|
||||
rpm_arch="${BURROW_RPM_ARCH:-x86_64}"
|
||||
pacman_arch="${BURROW_PACMAN_ARCH:-x86_64}"
|
||||
repo_name="${BURROW_ARCH_REPO_NAME:-burrow}"
|
||||
generic_kms_public_key_pem="${BURROW_PACKAGE_REPO_KMS_PUBLIC_KEY_PEM:-}"
|
||||
generic_kms_key="${BURROW_PACKAGE_REPO_KMS_KEY:-}"
|
||||
apt_kms_public_key_pem="${BURROW_APT_REPO_KMS_PUBLIC_KEY_PEM:-$generic_kms_public_key_pem}"
|
||||
apt_kms_key="${BURROW_APT_REPO_KMS_KEY:-${generic_kms_key:-package-apt-repository}}"
|
||||
rpm_kms_public_key_pem="${BURROW_RPM_REPO_KMS_PUBLIC_KEY_PEM:-$generic_kms_public_key_pem}"
|
||||
rpm_kms_key="${BURROW_RPM_REPO_KMS_KEY:-${generic_kms_key:-package-rpm-repository}}"
|
||||
pacman_kms_public_key_pem="${BURROW_PACMAN_REPO_KMS_PUBLIC_KEY_PEM:-$generic_kms_public_key_pem}"
|
||||
pacman_kms_key="${BURROW_PACMAN_REPO_KMS_KEY:-${generic_kms_key:-package-pacman-repository}}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/package/build-repositories.sh [apt|rpm|arch|keys|all]
|
||||
|
||||
Builds native Linux package repositories from existing package artifacts.
|
||||
|
||||
Inputs:
|
||||
BURROW_PACKAGE_INPUT_DIR directory containing .deb, .rpm, and .pkg.tar.* files
|
||||
BURROW_PACKAGE_REPOSITORY_DIR output directory, default publish/repositories
|
||||
BURROW_APT_REPO_KMS_PUBLIC_KEY_PEM Google KMS public key PEM for APT OpenPGP signatures
|
||||
BURROW_APT_REPO_KMS_KEY Google KMS key name for APT signatures
|
||||
BURROW_RPM_REPO_KMS_PUBLIC_KEY_PEM Google KMS public key PEM for RPM repository signatures
|
||||
BURROW_RPM_REPO_KMS_KEY Google KMS key name for RPM repository signatures
|
||||
BURROW_PACMAN_REPO_KMS_PUBLIC_KEY_PEM Google KMS public key PEM for pacman signatures
|
||||
BURROW_PACMAN_REPO_KMS_KEY Google KMS key name for pacman signatures
|
||||
BURROW_PACKAGE_REPO_KMS_PUBLIC_KEY_PEM fallback public key PEM for all formats
|
||||
BURROW_PACKAGE_REPO_KMS_KEY fallback KMS key name for all formats
|
||||
BURROW_KMS_KEYRING/BURROW_KMS_LOCATION/BURROW_KMS_VERSION/GOOGLE_CLOUD_PROJECT
|
||||
|
||||
The repository metadata is signed with Google KMS-backed OpenPGP detached
|
||||
signatures when each format's KMS public key and key name are configured.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mode="${1:-all}"
|
||||
|
||||
sha256_file() {
|
||||
local file="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" > "${file}.sha256"
|
||||
else
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
fi
|
||||
}
|
||||
|
||||
sign_file() {
|
||||
local file="$1"
|
||||
local output="${2:-${file}.sig}"
|
||||
local armor="${3:-0}"
|
||||
local kms_key="$4"
|
||||
local kms_public_key_pem="$5"
|
||||
if [[ -z "$kms_public_key_pem" || -z "$kms_key" ]]; then
|
||||
echo "warning: KMS OpenPGP signing disabled for ${file}; set the ${file} repository KMS public key and key name" >&2
|
||||
return 0
|
||||
fi
|
||||
args=(
|
||||
"${repo_root}/Scripts/package/google-kms-openpgp.py"
|
||||
detach-sign
|
||||
--public-key-pem "$kms_public_key_pem"
|
||||
--kms-key "$kms_key"
|
||||
--input "$file"
|
||||
--output "$output"
|
||||
)
|
||||
if [[ "$armor" == "1" ]]; then
|
||||
args+=(--armor)
|
||||
fi
|
||||
"${args[@]}"
|
||||
}
|
||||
|
||||
write_public_key() {
|
||||
local name="$1"
|
||||
local kms_key="$2"
|
||||
local kms_public_key_pem="$3"
|
||||
local user_id="$4"
|
||||
local output="${output_dir}/keys/${name}.asc"
|
||||
|
||||
if [[ -z "$kms_public_key_pem" || -z "$kms_key" ]]; then
|
||||
echo "warning: KMS OpenPGP public key export disabled for ${name}; set the repository KMS public key and key name" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$output")"
|
||||
"${repo_root}/Scripts/package/google-kms-openpgp.py" \
|
||||
public-key \
|
||||
--public-key-pem "$kms_public_key_pem" \
|
||||
--kms-key "$kms_key" \
|
||||
--user-id "$user_id" \
|
||||
--output "$output" \
|
||||
--armor
|
||||
}
|
||||
|
||||
write_release_file() {
|
||||
local release_path="$1"
|
||||
local dist_dir="$2"
|
||||
local now
|
||||
now="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
||||
{
|
||||
printf 'Origin: Burrow\n'
|
||||
printf 'Label: Burrow\n'
|
||||
printf 'Suite: %s\n' "$suite"
|
||||
printf 'Codename: %s\n' "$suite"
|
||||
printf 'Date: %s\n' "$now"
|
||||
printf 'Architectures: %s\n' "$apt_arch"
|
||||
printf 'Components: %s\n' "$component"
|
||||
printf 'Description: Burrow native package repository\n'
|
||||
printf 'SHA256:\n'
|
||||
(
|
||||
cd "$dist_dir"
|
||||
find . -type f ! -name Release -print | LC_ALL=C sort | while read -r file; do
|
||||
clean="${file#./}"
|
||||
sum="$(sha256sum "$clean" | awk '{print $1}')"
|
||||
size="$(wc -c < "$clean" | tr -d ' ')"
|
||||
printf ' %s %s %s\n' "$sum" "$size" "$clean"
|
||||
done
|
||||
)
|
||||
} > "$release_path"
|
||||
}
|
||||
|
||||
build_apt() {
|
||||
local root binary_dir pool_dir release_dir release_file
|
||||
root="${output_dir}/apt"
|
||||
pool_dir="${root}/pool/${component}/b/burrow"
|
||||
binary_dir="${root}/dists/${suite}/${component}/binary-${apt_arch}"
|
||||
release_dir="${root}/dists/${suite}"
|
||||
mkdir -p "$pool_dir" "$binary_dir" "$release_dir"
|
||||
|
||||
find "$input_dir" -type f -name '*.deb' -exec cp {} "$pool_dir/" \; 2>/dev/null || true
|
||||
if ! find "$pool_dir" -type f -name '*.deb' -print -quit | grep -q .; then
|
||||
echo "warning: no .deb packages found under ${input_dir}; apt repository contains README only" >&2
|
||||
printf 'Place Burrow .deb artifacts in this repository pool before publishing.\n' > "${root}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v dpkg-scanpackages >/dev/null 2>&1; then
|
||||
echo "error: dpkg-scanpackages is required to build the apt repository" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$root"
|
||||
dpkg-scanpackages --arch "$apt_arch" "pool/${component}" /dev/null > "dists/${suite}/${component}/binary-${apt_arch}/Packages"
|
||||
gzip -9cn "dists/${suite}/${component}/binary-${apt_arch}/Packages" > "dists/${suite}/${component}/binary-${apt_arch}/Packages.gz"
|
||||
)
|
||||
release_file="${release_dir}/Release"
|
||||
write_release_file "$release_file" "$release_dir"
|
||||
sign_file "$release_file" "${release_file}.gpg" 1 "$apt_kms_key" "$apt_kms_public_key_pem"
|
||||
sha256_file "$release_file"
|
||||
}
|
||||
|
||||
build_rpm() {
|
||||
local root repomd
|
||||
root="${output_dir}/rpm/${channel}/${rpm_arch}"
|
||||
mkdir -p "$root"
|
||||
find "$input_dir" -type f -name '*.rpm' -exec cp {} "$root/" \; 2>/dev/null || true
|
||||
if ! find "$root" -maxdepth 1 -type f -name '*.rpm' -print -quit | grep -q .; then
|
||||
echo "warning: no .rpm packages found under ${input_dir}; rpm repository contains README only" >&2
|
||||
printf 'Place Burrow .rpm artifacts in this directory before publishing.\n' > "${root}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v createrepo_c >/dev/null 2>&1; then
|
||||
echo "error: createrepo_c is required to build the rpm repository" >&2
|
||||
exit 127
|
||||
fi
|
||||
createrepo_c "$root"
|
||||
repomd="${root}/repodata/repomd.xml"
|
||||
sign_file "$repomd" "${repomd}.asc" 1 "$rpm_kms_key" "$rpm_kms_public_key_pem"
|
||||
sha256_file "$repomd"
|
||||
}
|
||||
|
||||
build_arch() {
|
||||
local root db
|
||||
root="${output_dir}/arch/${repo_name}/${pacman_arch}"
|
||||
mkdir -p "$root"
|
||||
find "$input_dir" -type f \( -name '*.pkg.tar.zst' -o -name '*.pkg.tar.xz' -o -name '*.pkg.tar.gz' \) -exec cp {} "$root/" \; 2>/dev/null || true
|
||||
if ! find "$root" -maxdepth 1 -type f -name '*.pkg.tar.*' -print -quit | grep -q .; then
|
||||
echo "warning: no Arch packages found under ${input_dir}; pacman repository contains README only" >&2
|
||||
printf 'Place Burrow .pkg.tar.* artifacts in this directory before publishing.\n' > "${root}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v repo-add >/dev/null 2>&1; then
|
||||
echo "error: repo-add is required to build the Arch repository" >&2
|
||||
exit 127
|
||||
fi
|
||||
(
|
||||
cd "$root"
|
||||
repo-add "${repo_name}.db.tar.gz" ./*.pkg.tar.*
|
||||
)
|
||||
db="${root}/${repo_name}.db.tar.gz"
|
||||
sign_file "$db" "${db}.sig" 0 "$pacman_kms_key" "$pacman_kms_public_key_pem"
|
||||
find "$root" -maxdepth 1 -type f -name '*.pkg.tar.*' ! -name '*.sig' -print | while read -r package; do
|
||||
sign_file "$package" "${package}.sig" 0 "$pacman_kms_key" "$pacman_kms_public_key_pem"
|
||||
done
|
||||
sha256_file "$db"
|
||||
}
|
||||
|
||||
build_keys() {
|
||||
write_public_key "burrow-package-apt-repository" "$apt_kms_key" "$apt_kms_public_key_pem" "Burrow APT Package Repository <packages@burrow.net>"
|
||||
write_public_key "burrow-package-rpm-repository" "$rpm_kms_key" "$rpm_kms_public_key_pem" "Burrow RPM Package Repository <packages@burrow.net>"
|
||||
write_public_key "burrow-package-pacman-repository" "$pacman_kms_key" "$pacman_kms_public_key_pem" "Burrow pacman Package Repository <packages@burrow.net>"
|
||||
}
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
case "$mode" in
|
||||
apt)
|
||||
build_apt
|
||||
;;
|
||||
rpm)
|
||||
build_rpm
|
||||
;;
|
||||
arch)
|
||||
build_arch
|
||||
;;
|
||||
keys)
|
||||
build_keys
|
||||
;;
|
||||
all)
|
||||
build_keys
|
||||
build_apt
|
||||
build_rpm
|
||||
build_arch
|
||||
;;
|
||||
*)
|
||||
echo "unknown repository mode: ${mode}" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
313
Scripts/package/google-kms-openpgp.py
Executable file
313
Scripts/package/google-kms-openpgp.py
Executable file
|
|
@ -0,0 +1,313 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Create OpenPGP public keys and detached signatures backed by Google KMS.
|
||||
|
||||
This intentionally implements only the narrow RSA/SHA-256 subset Burrow needs
|
||||
for Linux package repository metadata. The private key lives in Google Cloud KMS;
|
||||
this script builds OpenPGP packets locally and asks KMS to produce the RSA
|
||||
signature value.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RSA_PUBLIC_KEY_ALGO = 1
|
||||
SHA256_HASH_ALGO = 8
|
||||
|
||||
|
||||
def parse_der_length(data: bytes, offset: int) -> tuple[int, int]:
|
||||
first = data[offset]
|
||||
offset += 1
|
||||
if first < 0x80:
|
||||
return first, offset
|
||||
count = first & 0x7F
|
||||
if count == 0 or count > 4:
|
||||
raise ValueError("unsupported DER length")
|
||||
value = int.from_bytes(data[offset : offset + count], "big")
|
||||
return value, offset + count
|
||||
|
||||
|
||||
def parse_der_tlv(data: bytes, offset: int, expected_tag: int | None = None) -> tuple[int, bytes, int]:
|
||||
tag = data[offset]
|
||||
offset += 1
|
||||
length, offset = parse_der_length(data, offset)
|
||||
value = data[offset : offset + length]
|
||||
if len(value) != length:
|
||||
raise ValueError("truncated DER value")
|
||||
if expected_tag is not None and tag != expected_tag:
|
||||
raise ValueError(f"expected DER tag {expected_tag:#x}, got {tag:#x}")
|
||||
return tag, value, offset + length
|
||||
|
||||
|
||||
def pem_to_der(pem: bytes) -> bytes:
|
||||
lines = [
|
||||
line.strip()
|
||||
for line in pem.splitlines()
|
||||
if line and not line.startswith(b"-----")
|
||||
]
|
||||
return base64.b64decode(b"".join(lines))
|
||||
|
||||
|
||||
def parse_rsa_public_key_from_pem(path: Path) -> tuple[int, int]:
|
||||
der = pem_to_der(path.read_bytes())
|
||||
_, spki, end = parse_der_tlv(der, 0, 0x30)
|
||||
if end != len(der):
|
||||
raise ValueError("trailing data after SubjectPublicKeyInfo")
|
||||
_, _alg, offset = parse_der_tlv(spki, 0, 0x30)
|
||||
_, bit_string, offset = parse_der_tlv(spki, offset, 0x03)
|
||||
if offset != len(spki):
|
||||
raise ValueError("trailing data after SPKI bit string")
|
||||
if not bit_string or bit_string[0] != 0:
|
||||
raise ValueError("unsupported SPKI bit string")
|
||||
rsa_der = bit_string[1:]
|
||||
_, rsa_seq, end = parse_der_tlv(rsa_der, 0, 0x30)
|
||||
if end != len(rsa_der):
|
||||
raise ValueError("trailing data after RSA public key")
|
||||
_, n_bytes, offset = parse_der_tlv(rsa_seq, 0, 0x02)
|
||||
_, e_bytes, offset = parse_der_tlv(rsa_seq, offset, 0x02)
|
||||
if offset != len(rsa_seq):
|
||||
raise ValueError("trailing data after RSA integers")
|
||||
return int.from_bytes(n_bytes, "big"), int.from_bytes(e_bytes, "big")
|
||||
|
||||
|
||||
def packet_header(tag: int, length: int) -> bytes:
|
||||
if length < 192:
|
||||
return bytes([0xC0 | tag, length])
|
||||
if length < 8384:
|
||||
length -= 192
|
||||
return bytes([0xC0 | tag, (length >> 8) + 192, length & 0xFF])
|
||||
return bytes([0xC0 | tag, 0xFF]) + struct.pack(">I", length)
|
||||
|
||||
|
||||
def old_packet_header(tag: int, length: int) -> bytes:
|
||||
if length > 0xFFFF:
|
||||
raise ValueError("old-format two-octet packet length exceeded")
|
||||
return bytes([(tag << 2) | 1]) + struct.pack(">H", length)
|
||||
|
||||
|
||||
def mpi(value: int) -> bytes:
|
||||
if value < 0:
|
||||
raise ValueError("MPI value must be non-negative")
|
||||
raw = value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big")
|
||||
return struct.pack(">H", value.bit_length()) + raw.lstrip(b"\x00")
|
||||
|
||||
|
||||
def subpacket(kind: int, body: bytes) -> bytes:
|
||||
payload = bytes([kind]) + body
|
||||
length = len(payload)
|
||||
if length < 192:
|
||||
return bytes([length]) + payload
|
||||
if length < 8384:
|
||||
length -= 192
|
||||
return bytes([(length >> 8) + 192, length & 0xFF]) + payload
|
||||
return b"\xff" + struct.pack(">I", length) + payload
|
||||
|
||||
|
||||
def public_key_body(n: int, e: int, created_at: int) -> bytes:
|
||||
return (
|
||||
b"\x04"
|
||||
+ struct.pack(">I", created_at)
|
||||
+ bytes([RSA_PUBLIC_KEY_ALGO])
|
||||
+ mpi(n)
|
||||
+ mpi(e)
|
||||
)
|
||||
|
||||
|
||||
def fingerprint(public_body: bytes) -> bytes:
|
||||
return hashlib.sha1(old_packet_header(6, len(public_body)) + public_body).digest()
|
||||
|
||||
|
||||
def armor(kind: str, body: bytes) -> str:
|
||||
b64 = base64.b64encode(body).decode("ascii")
|
||||
crc = crc24(body)
|
||||
checksum = base64.b64encode(crc.to_bytes(3, "big")).decode("ascii")
|
||||
lines = [f"-----BEGIN PGP {kind}-----", ""]
|
||||
lines.extend(b64[i : i + 64] for i in range(0, len(b64), 64))
|
||||
lines.append(f"={checksum}")
|
||||
lines.append(f"-----END PGP {kind}-----")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def crc24(data: bytes) -> int:
|
||||
crc = 0xB704CE
|
||||
for octet in data:
|
||||
crc ^= octet << 16
|
||||
for _ in range(8):
|
||||
crc <<= 1
|
||||
if crc & 0x1000000:
|
||||
crc ^= 0x1864CFB
|
||||
return crc & 0xFFFFFF
|
||||
|
||||
|
||||
def kms_sign(payload: bytes, args: argparse.Namespace) -> bytes:
|
||||
with tempfile.TemporaryDirectory(prefix="burrow-kms-openpgp.") as tmp:
|
||||
input_path = Path(tmp) / "payload"
|
||||
signature_path = Path(tmp) / "signature.b64"
|
||||
input_path.write_bytes(payload)
|
||||
command = [
|
||||
"gcloud",
|
||||
"kms",
|
||||
"asymmetric-sign",
|
||||
"--location",
|
||||
args.kms_location,
|
||||
"--keyring",
|
||||
args.kms_keyring,
|
||||
"--key",
|
||||
args.kms_key,
|
||||
"--version",
|
||||
args.kms_version,
|
||||
"--digest-algorithm",
|
||||
"sha256",
|
||||
"--input-file",
|
||||
str(input_path),
|
||||
"--signature-file",
|
||||
str(signature_path),
|
||||
]
|
||||
if args.gcloud_project:
|
||||
command.extend(["--project", args.gcloud_project])
|
||||
subprocess.run(command, check=True)
|
||||
return base64.b64decode(signature_path.read_text(encoding="utf-8").strip())
|
||||
|
||||
|
||||
def signature_packet(
|
||||
signed_payload: bytes,
|
||||
public_body: bytes,
|
||||
sig_type: int,
|
||||
created_at: int,
|
||||
args: argparse.Namespace,
|
||||
) -> bytes:
|
||||
fpr = fingerprint(public_body)
|
||||
key_id = fpr[-8:]
|
||||
hashed = b"".join(
|
||||
[
|
||||
subpacket(2, struct.pack(">I", created_at)),
|
||||
subpacket(33, b"\x04" + fpr),
|
||||
]
|
||||
)
|
||||
unhashed = subpacket(16, key_id)
|
||||
sig_data = (
|
||||
b"\x04"
|
||||
+ bytes([sig_type, RSA_PUBLIC_KEY_ALGO, SHA256_HASH_ALGO])
|
||||
+ struct.pack(">H", len(hashed))
|
||||
+ hashed
|
||||
)
|
||||
trailer = b"\x04\xff" + struct.pack(">I", len(sig_data))
|
||||
to_hash = signed_payload + sig_data + trailer
|
||||
digest = hashlib.sha256(to_hash).digest()
|
||||
signature = kms_sign(to_hash, args)
|
||||
body = (
|
||||
sig_data
|
||||
+ struct.pack(">H", len(unhashed))
|
||||
+ unhashed
|
||||
+ digest[:2]
|
||||
+ mpi(int.from_bytes(signature, "big"))
|
||||
)
|
||||
return packet_header(2, len(body)) + body
|
||||
|
||||
|
||||
def public_key_command(args: argparse.Namespace) -> int:
|
||||
created_at = args.created_at or int(time.time())
|
||||
n, e = parse_rsa_public_key_from_pem(Path(args.public_key_pem))
|
||||
public_body = public_key_body(n, e, created_at)
|
||||
public_packet = packet_header(6, len(public_body)) + public_body
|
||||
uid = args.user_id.encode("utf-8")
|
||||
uid_packet = packet_header(13, len(uid)) + uid
|
||||
uid_signed_payload = (
|
||||
old_packet_header(6, len(public_body))
|
||||
+ public_body
|
||||
+ b"\xb4"
|
||||
+ struct.pack(">I", len(uid))
|
||||
+ uid
|
||||
)
|
||||
cert_packet = signature_packet(
|
||||
uid_signed_payload,
|
||||
public_body,
|
||||
sig_type=0x13,
|
||||
created_at=created_at,
|
||||
args=args,
|
||||
)
|
||||
key = public_packet + uid_packet + cert_packet
|
||||
output = Path(args.output)
|
||||
if args.armor:
|
||||
output.write_text(armor("PUBLIC KEY BLOCK", key), encoding="utf-8")
|
||||
else:
|
||||
output.write_bytes(key)
|
||||
return 0
|
||||
|
||||
|
||||
def detach_sign_command(args: argparse.Namespace) -> int:
|
||||
created_at = args.created_at or int(time.time())
|
||||
n, e = parse_rsa_public_key_from_pem(Path(args.public_key_pem))
|
||||
public_body = public_key_body(n, e, args.key_created_at)
|
||||
payload = Path(args.input).read_bytes()
|
||||
sig_type = 0x01 if args.text else 0x00
|
||||
packet = signature_packet(payload, public_body, sig_type, created_at, args)
|
||||
output = Path(args.output)
|
||||
if args.armor:
|
||||
output.write_text(armor("SIGNATURE", packet), encoding="utf-8")
|
||||
else:
|
||||
output.write_bytes(packet)
|
||||
return 0
|
||||
|
||||
|
||||
def add_kms_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--kms-location", default=os.environ.get("BURROW_KMS_LOCATION", "global"))
|
||||
parser.add_argument("--kms-keyring", default=os.environ.get("BURROW_KMS_KEYRING", "burrow-identity"))
|
||||
parser.add_argument("--kms-key", required=not os.environ.get("BURROW_KMS_KEY"), default=os.environ.get("BURROW_KMS_KEY"))
|
||||
parser.add_argument("--kms-version", default=os.environ.get("BURROW_KMS_VERSION", "1"))
|
||||
parser.add_argument("--gcloud-project", default=os.environ.get("GOOGLE_CLOUD_PROJECT"))
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subcommands = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
public_key = subcommands.add_parser("public-key")
|
||||
public_key.add_argument("--public-key-pem", required=True)
|
||||
public_key.add_argument("--user-id", default="Burrow Package Repository <packages@burrow.net>")
|
||||
public_key.add_argument("--output", required=True)
|
||||
public_key.add_argument("--armor", action="store_true")
|
||||
public_key.add_argument("--created-at", type=int)
|
||||
add_kms_args(public_key)
|
||||
public_key.set_defaults(func=public_key_command)
|
||||
|
||||
detach = subcommands.add_parser("detach-sign")
|
||||
detach.add_argument("--public-key-pem", required=True)
|
||||
detach.add_argument("--key-created-at", type=int, default=1704067200)
|
||||
detach.add_argument("--input", required=True)
|
||||
detach.add_argument("--output", required=True)
|
||||
detach.add_argument("--armor", action="store_true")
|
||||
detach.add_argument("--text", action="store_true")
|
||||
detach.add_argument("--created-at", type=int)
|
||||
add_kms_args(detach)
|
||||
detach.set_defaults(func=detach_sign_command)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return args.func(args)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"gcloud signing failed: {exc}", file=sys.stderr)
|
||||
return exc.returncode or 1
|
||||
except Exception as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -23,7 +23,7 @@ Options:
|
|||
--no-refresh-token Reuse intake/forgejo_nsc_token.txt if it already exists.
|
||||
--token-name <name> Forgejo PAT name prefix (default: forgejo-nsc)
|
||||
--contact-user <name> Forgejo username used for PAT creation (default: contact)
|
||||
--scope-owner <name> Forgejo org/user owner for the default NSC scope (default: burrow)
|
||||
--scope-owner <name> Forgejo org/user owner for the default NSC scope (default: hackclub)
|
||||
--scope-name <name> Forgejo repository name for the default NSC scope (default: burrow)
|
||||
-h, --help Show this help text.
|
||||
EOF
|
||||
|
|
@ -36,7 +36,7 @@ KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-k
|
|||
REFRESH_TOKEN=1
|
||||
TOKEN_NAME_PREFIX="${FORGEJO_PAT_NAME:-forgejo-nsc}"
|
||||
CONTACT_USER="${FORGEJO_CONTACT_USER:-contact}"
|
||||
SCOPE_OWNER="${FORGEJO_SCOPE_OWNER:-burrow}"
|
||||
SCOPE_OWNER="${FORGEJO_SCOPE_OWNER:-hackclub}"
|
||||
SCOPE_NAME="${FORGEJO_SCOPE_NAME:-burrow}"
|
||||
BURROW_FLAKE_TMPDIRS=()
|
||||
|
||||
|
|
|
|||
41
Scripts/releases-tofu.sh
Executable file
41
Scripts/releases-tofu.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(
|
||||
git rev-parse --show-toplevel 2>/dev/null || {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
}
|
||||
)"
|
||||
|
||||
work_dir="${BURROW_RELEASES_TOFU_DIR:-$ROOT/infra/releases}"
|
||||
plugin_cache_dir="${BURROW_TOFU_PLUGIN_CACHE_DIR:-$ROOT/.cache/opentofu/plugins}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/releases-tofu.sh [tofu args...]
|
||||
|
||||
Runs OpenTofu for Burrow Google Cloud Storage release and package repository
|
||||
buckets. Cloud provider credentials must come from ADC or Workload Identity
|
||||
Federation; do not put service-account JSON in this repository.
|
||||
|
||||
Examples:
|
||||
Scripts/releases-tofu.sh init -backend-config=backend.hcl
|
||||
Scripts/releases-tofu.sh plan -var-file=terraform.tfvars
|
||||
|
||||
Local syntax check:
|
||||
Scripts/releases-tofu.sh init -backend=false
|
||||
Scripts/releases-tofu.sh validate
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$plugin_cache_dir"
|
||||
export TF_PLUGIN_CACHE_DIR="$plugin_cache_dir"
|
||||
export TF_IN_AUTOMATION="${TF_IN_AUTOMATION:-true}"
|
||||
|
||||
exec nix shell --inputs-from "$ROOT" nixpkgs#opentofu -c tofu -chdir="$work_dir" "$@"
|
||||
172
Scripts/resolve-age-identity.sh
Executable file
172
Scripts/resolve-age-identity.sh
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
identity_is_valid() {
|
||||
local path="$1"
|
||||
if [[ -z "$path" || ! -f "$path" || ! -s "$path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
chmod 600 "$path" 2>/dev/null || true
|
||||
if command -v age-keygen >/dev/null 2>&1 && age-keygen -y "$path" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if openssh_private_identity_has_markers "$path"; then
|
||||
return 0
|
||||
fi
|
||||
if command -v ssh-keygen >/dev/null 2>&1 && ssh-keygen -y -f "$path" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local header
|
||||
header="$(LC_ALL=C head -c 80 "$path" 2>/dev/null || true)"
|
||||
case "$header" in
|
||||
AGE-SECRET-KEY-*) return 0 ;;
|
||||
esac
|
||||
|
||||
echo "::warning ::Ignoring invalid age identity candidate at ${path}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
openssh_private_identity_has_markers() {
|
||||
local path="$1"
|
||||
local first_line
|
||||
first_line="$(LC_ALL=C sed -n '1p' "$path" 2>/dev/null || true)"
|
||||
if [[ "$first_line" != "-----BEGIN OPENSSH PRIVATE KEY-----" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if LC_ALL=C grep -Fq '\n' "$path" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
LC_ALL=C grep -q '^-----END OPENSSH PRIVATE KEY-----$' "$path" 2>/dev/null
|
||||
}
|
||||
|
||||
candidate_path() {
|
||||
local path="$1"
|
||||
if identity_is_valid "$path"; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
preferred_home() {
|
||||
if [[ -n "${RUNNER_HOME:-}" ]]; then
|
||||
printf '%s\n' "$RUNNER_HOME"
|
||||
elif [[ -n "${FORGEJO_RUNNER_HOME:-}" ]]; then
|
||||
printf '%s\n' "$FORGEJO_RUNNER_HOME"
|
||||
elif [[ -n "${HOME:-}" ]]; then
|
||||
printf '%s\n' "$HOME"
|
||||
elif [[ -n "${FORGEJO_RUNNER_WORKDIR:-}" ]]; then
|
||||
printf '%s\n' "${FORGEJO_RUNNER_WORKDIR}/home"
|
||||
else
|
||||
printf '%s\n' "/tmp/forgejo-runner/home"
|
||||
fi
|
||||
}
|
||||
|
||||
decode_base64() {
|
||||
local value="$1"
|
||||
local path="$2"
|
||||
if command -v base64 >/dev/null 2>&1; then
|
||||
if printf '%s' "$value" | base64 -d > "$path" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if printf '%s' "$value" | base64 -D > "$path" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
materialize_secret() {
|
||||
local raw="$1"
|
||||
local home="$2"
|
||||
local path="${home}/.ssh/age_keystore"
|
||||
install -d -m 700 "${home}/.ssh"
|
||||
printf '%s\n' "$raw" > "$path"
|
||||
perl -0pi -e 's/\r\n?/\n/g' "$path" 2>/dev/null || true
|
||||
chmod 600 "$path"
|
||||
if identity_is_valid "$path"; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$raw" == *"\\n"* ]]; then
|
||||
printf '%b' "$raw" > "$path"
|
||||
perl -0pi -e 's/\r\n?/\n/g' "$path" 2>/dev/null || true
|
||||
chmod 600 "$path"
|
||||
if identity_is_valid "$path"; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if decode_base64 "$raw" "$path"; then
|
||||
perl -0pi -e 's/\r\n?/\n/g' "$path" 2>/dev/null || true
|
||||
chmod 600 "$path"
|
||||
if identity_is_valid "$path"; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$path"
|
||||
echo "::warning ::Injected age identity was present but not parseable; trying other candidates" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_from_existing_files() {
|
||||
local home
|
||||
home="$(preferred_home)"
|
||||
|
||||
local candidate
|
||||
while IFS= read -r candidate; do
|
||||
candidate_path "$candidate" && return 0
|
||||
done <<EOF
|
||||
${AGE_IDENTITY_PATH:-}
|
||||
${BURROW_RUNNER_AGE_IDENTITY_PATH:-}
|
||||
/var/lib/forgejo-runner-agent/age_keystore
|
||||
EOF
|
||||
|
||||
if [[ -n "${BURROW_RUNNER_AGE_IDENTITY_B64:-}" ]]; then
|
||||
local decoded_path
|
||||
decoded_path="${home}/.ssh/age_keystore.from_b64"
|
||||
if decode_base64 "$BURROW_RUNNER_AGE_IDENTITY_B64" "$decoded_path" && candidate_path "$decoded_path"; then
|
||||
return 0
|
||||
fi
|
||||
rm -f "$decoded_path"
|
||||
if materialize_secret "$BURROW_RUNNER_AGE_IDENTITY_B64" "$home"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${BURROW_RUNNER_AGE_IDENTITY:-}" ]]; then
|
||||
materialize_secret "$BURROW_RUNNER_AGE_IDENTITY" "$home" && return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${AGE_FORGE_SSH_KEY:-}" ]]; then
|
||||
materialize_secret "$AGE_FORGE_SSH_KEY" "$home" && return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r candidate; do
|
||||
candidate_path "$candidate" && return 0
|
||||
done <<EOF
|
||||
${RUNNER_HOME:-}/.ssh/age_keystore
|
||||
${FORGEJO_RUNNER_HOME:-}/.ssh/age_keystore
|
||||
${HOME:-}/.ssh/age_keystore
|
||||
${FORGEJO_RUNNER_WORKDIR:-}/home/.ssh/age_keystore
|
||||
${FORGEJO_RUNNER_WORKDIR:-}/age_keystore
|
||||
/tmp/forgejo-runner/home/.ssh/age_keystore
|
||||
/tmp/forgejo-runner/age_keystore
|
||||
EOF
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if resolved="$(resolve_from_existing_files)"; then
|
||||
printf 'AGE_IDENTITY_PATH=%s\n' "$resolved"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "::error ::Missing age identity (checked explicit path, runner home, managed host path, and injected secrets)" >&2
|
||||
exit 1
|
||||
121
Scripts/resolve-forge-ssh-key.sh
Executable file
121
Scripts/resolve-forge-ssh-key.sh
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
preferred_home() {
|
||||
if [[ -n "${RUNNER_HOME:-}" ]]; then
|
||||
printf '%s\n' "$RUNNER_HOME"
|
||||
elif [[ -n "${FORGEJO_RUNNER_HOME:-}" ]]; then
|
||||
printf '%s\n' "$FORGEJO_RUNNER_HOME"
|
||||
elif [[ -n "${HOME:-}" ]]; then
|
||||
printf '%s\n' "$HOME"
|
||||
elif [[ -n "${FORGEJO_RUNNER_WORKDIR:-}" ]]; then
|
||||
printf '%s\n' "${FORGEJO_RUNNER_WORKDIR}/home"
|
||||
else
|
||||
printf '%s\n' "/tmp/forgejo-runner/home"
|
||||
fi
|
||||
}
|
||||
|
||||
is_ssh_private_key() {
|
||||
local candidate="$1"
|
||||
[[ -n "$candidate" && -f "$candidate" && -s "$candidate" ]] || return 1
|
||||
chmod 600 "$candidate" 2>/dev/null || true
|
||||
ssh-keygen -y -f "$candidate" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
candidate_path() {
|
||||
local path="$1"
|
||||
if is_ssh_private_key "$path"; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
decode_base64() {
|
||||
local value="$1"
|
||||
local path="$2"
|
||||
if command -v base64 >/dev/null 2>&1; then
|
||||
if printf '%s' "$value" | base64 -d > "$path" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if printf '%s' "$value" | base64 -D > "$path" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
materialize_secret() {
|
||||
local raw="$1"
|
||||
local home="$2"
|
||||
local path="${home}/.ssh/burrow_forge_ed25519"
|
||||
install -d -m 700 "${home}/.ssh"
|
||||
|
||||
printf '%s\n' "$raw" > "$path"
|
||||
if candidate_path "$path"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$raw" == *"\\n"* ]]; then
|
||||
printf '%b' "$raw" > "$path"
|
||||
if candidate_path "$path"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if decode_base64 "$raw" "$path" && candidate_path "$path"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$path"
|
||||
echo "::warning ::Injected Forge SSH key was present but not parseable; trying file candidates" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_key() {
|
||||
local home
|
||||
home="$(preferred_home)"
|
||||
|
||||
if [[ -n "${BURROW_FORGE_SSH_KEY:-}" ]]; then
|
||||
if candidate_path "$BURROW_FORGE_SSH_KEY"; then
|
||||
return 0
|
||||
fi
|
||||
echo "::error ::BURROW_FORGE_SSH_KEY is set but is not an SSH private key: ${BURROW_FORGE_SSH_KEY}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${AGE_FORGE_SSH_KEY:-}" ]]; then
|
||||
materialize_secret "$AGE_FORGE_SSH_KEY" "$home" && return 0
|
||||
fi
|
||||
|
||||
local candidate
|
||||
while IFS= read -r candidate; do
|
||||
candidate_path "$candidate" && return 0
|
||||
done <<EOF
|
||||
${AGE_IDENTITY_PATH:-}
|
||||
${BURROW_RUNNER_AGE_IDENTITY_PATH:-}
|
||||
${RUNNER_HOME:-}/.ssh/burrow_forge_ed25519
|
||||
${FORGEJO_RUNNER_HOME:-}/.ssh/burrow_forge_ed25519
|
||||
${HOME:-}/.ssh/burrow_forge_ed25519
|
||||
${HOME:-}/.ssh/agent_at_burrow_net_ed25519
|
||||
${HOME:-}/.ssh/burrow_forgejo_ed25519
|
||||
/var/lib/forgejo-runner-agent/age_keystore
|
||||
${RUNNER_HOME:-}/.ssh/age_keystore
|
||||
${FORGEJO_RUNNER_HOME:-}/.ssh/age_keystore
|
||||
${HOME:-}/.ssh/age_keystore
|
||||
${FORGEJO_RUNNER_WORKDIR:-}/home/.ssh/age_keystore
|
||||
${FORGEJO_RUNNER_WORKDIR:-}/age_keystore
|
||||
/tmp/forgejo-runner/home/.ssh/age_keystore
|
||||
/tmp/forgejo-runner/age_keystore
|
||||
EOF
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if resolved="$(resolve_key)"; then
|
||||
printf 'BURROW_FORGE_SSH_KEY=%s\n' "$resolved"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "::error ::Forge SSH key not found. Set BURROW_FORGE_SSH_KEY or AGE_FORGE_SSH_KEY." >&2
|
||||
exit 1
|
||||
261
Scripts/seal-release-secrets.sh
Executable file
261
Scripts/seal-release-secrets.sh
Executable file
|
|
@ -0,0 +1,261 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/seal-release-secrets.sh [--source-dir <dir>] [--only <secret>]
|
||||
|
||||
Encrypt Burrow release inputs into the agenix secrets consumed by CI.
|
||||
|
||||
Expected files in --source-dir (default: intake/release):
|
||||
appstore-key-id.txt
|
||||
appstore-key-issuer-id.txt
|
||||
appstore-key.p8
|
||||
distribution-cert.p12
|
||||
distribution-cert-password.txt
|
||||
sparkle-eddsa.key
|
||||
|
||||
Pre-composed appstore-connect.env, distribution-signing.env, and sparkle.env
|
||||
files are also accepted.
|
||||
|
||||
Provisioning profiles are synced from App Store Connect credentials in CI.
|
||||
The temporary keychain password is always the distribution certificate password.
|
||||
|
||||
Secrets:
|
||||
all
|
||||
appstore-connect
|
||||
distribution-signing
|
||||
sparkle
|
||||
EOF
|
||||
}
|
||||
|
||||
SOURCE_DIR="${BURROW_RELEASE_SECRET_SOURCE_DIR:-${REPO_ROOT}/intake/release}"
|
||||
ONLY="all"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source-dir)
|
||||
SOURCE_DIR="${2:?missing value for --source-dir}"
|
||||
shift 2
|
||||
;;
|
||||
--only)
|
||||
ONLY="${2:?missing value for --only}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$ONLY" in
|
||||
all|appstore-connect|app-store|appstore|distribution-signing|apple-signing|sparkle) ;;
|
||||
*)
|
||||
echo "unknown release secret selector: $ONLY" >&2
|
||||
usage >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd age
|
||||
require_cmd nix
|
||||
require_cmd python3
|
||||
|
||||
if [[ ! -d "$SOURCE_DIR" ]]; then
|
||||
echo "release secret source directory does not exist: $SOURCE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
seal_secret() {
|
||||
local target="$1"
|
||||
local source_path="$2"
|
||||
local required="${3:-required}"
|
||||
local recipients_file="${tmpdir}/$(basename "${target}").recipients"
|
||||
|
||||
if [[ ! -s "$source_path" ]]; then
|
||||
if [[ "$required" == "optional" ]]; then
|
||||
return 0
|
||||
fi
|
||||
echo "required release secret source missing or empty: $source_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${REPO_ROOT}/$(dirname "$target")"
|
||||
nix eval --impure --json --expr "let s = import ${REPO_ROOT}/secrets.nix; in s.\"${target}\".publicKeys" \
|
||||
| python3 -c 'import json, sys; [print(item) for item in json.load(sys.stdin)]' \
|
||||
> "$recipients_file"
|
||||
|
||||
age -R "$recipients_file" -o "${REPO_ROOT}/${target}" "$source_path"
|
||||
chmod 600 "${REPO_ROOT}/${target}"
|
||||
echo "Sealed ${target}"
|
||||
}
|
||||
|
||||
base64_file() {
|
||||
python3 -c 'import base64, pathlib, sys; print(base64.b64encode(pathlib.Path(sys.argv[1]).read_bytes()).decode())' "$1"
|
||||
}
|
||||
|
||||
read_secret_text() {
|
||||
python3 -c 'import pathlib, sys; sys.stdout.write(pathlib.Path(sys.argv[1]).read_text().strip())' "$1"
|
||||
}
|
||||
|
||||
base64_text() {
|
||||
python3 -c 'import base64, sys; print(base64.b64encode(sys.stdin.buffer.read()).decode())'
|
||||
}
|
||||
|
||||
reject_placeholder() {
|
||||
local path="$1"
|
||||
if grep -Eq '^(TODO|PASTE_|REPLACE_|#)' "$path"; then
|
||||
echo "intake file still contains placeholder text: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
reject_placeholder_text() {
|
||||
local path="$1"
|
||||
if grep -Iq . "$path"; then
|
||||
reject_placeholder "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
first_existing() {
|
||||
local path
|
||||
for path in "$@"; do
|
||||
if [[ -s "$path" ]]; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
compose_appstore_connect_env() {
|
||||
local direct="${SOURCE_DIR}/appstore-connect.env"
|
||||
if [[ -s "$direct" ]]; then
|
||||
printf '%s\n' "$direct"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local key_id_file="${SOURCE_DIR}/appstore-key-id.txt"
|
||||
local issuer_id_file="${SOURCE_DIR}/appstore-key-issuer-id.txt"
|
||||
local key_file="${SOURCE_DIR}/appstore-key.p8"
|
||||
if [[ ! -s "$key_file" ]]; then
|
||||
echo "required App Store Connect source missing or empty: appstore-key.p8" >&2
|
||||
exit 1
|
||||
fi
|
||||
for path in "$key_id_file" "$issuer_id_file" "$key_file"; do
|
||||
if [[ ! -s "$path" ]]; then
|
||||
echo "required App Store Connect source missing or empty: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
reject_placeholder_text "$path"
|
||||
done
|
||||
|
||||
local out="${tmpdir}/appstore-connect.env"
|
||||
{
|
||||
printf 'ASC_API_KEY_ID=%s\n' "$(read_secret_text "$key_id_file")"
|
||||
printf 'ASC_API_ISSUER_ID=%s\n' "$(read_secret_text "$issuer_id_file")"
|
||||
printf 'ASC_API_KEY_BASE64=%s\n' "$(base64_file "$key_file")"
|
||||
} > "$out"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
compose_distribution_signing_env() {
|
||||
local direct="${SOURCE_DIR}/distribution-signing.env"
|
||||
if [[ -s "$direct" ]]; then
|
||||
printf '%s\n' "$direct"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local cert_file
|
||||
cert_file="${SOURCE_DIR}/distribution-cert.p12"
|
||||
local password_file="${SOURCE_DIR}/distribution-cert-password.txt"
|
||||
if [[ ! -s "$cert_file" ]]; then
|
||||
echo "required distribution signing source missing or empty: distribution-cert.p12" >&2
|
||||
exit 1
|
||||
fi
|
||||
for path in "$password_file"; do
|
||||
if [[ ! -s "$path" ]]; then
|
||||
echo "required distribution signing source missing or empty: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
reject_placeholder_text "$path"
|
||||
done
|
||||
|
||||
local out="${tmpdir}/distribution-signing.env"
|
||||
{
|
||||
printf 'APPLE_SIGNING_CERTIFICATE_BASE64=%s\n' "$(base64_file "$cert_file")"
|
||||
printf 'APPLE_SIGNING_CERTIFICATE_PASSWORD_BASE64=%s\n' "$(read_secret_text "$password_file" | base64_text)"
|
||||
} > "$out"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
compose_sparkle_env() {
|
||||
local direct="${SOURCE_DIR}/sparkle.env"
|
||||
if [[ -s "$direct" ]]; then
|
||||
printf '%s\n' "$direct"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local key_file
|
||||
key_file="${SOURCE_DIR}/sparkle-eddsa.key"
|
||||
if [[ ! -s "$key_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
reject_placeholder_text "$key_file"
|
||||
|
||||
local out="${tmpdir}/sparkle.env"
|
||||
printf 'SPARKLE_EDDSA_KEY_BASE64=%s\n' "$(base64_file "$key_file")" > "$out"
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
case "$ONLY" in
|
||||
all)
|
||||
appstore_env="$(compose_appstore_connect_env)"
|
||||
seal_secret "secrets/apple/appstore-connect.env.age" "$appstore_env"
|
||||
distribution_env="$(compose_distribution_signing_env)"
|
||||
seal_secret "secrets/apple/distribution-signing.env.age" "$distribution_env"
|
||||
if sparkle_env="$(compose_sparkle_env)"; then
|
||||
seal_secret "secrets/apple/sparkle.env.age" "$sparkle_env"
|
||||
fi
|
||||
;;
|
||||
appstore-connect|app-store|appstore)
|
||||
appstore_env="$(compose_appstore_connect_env)"
|
||||
seal_secret "secrets/apple/appstore-connect.env.age" "$appstore_env"
|
||||
;;
|
||||
distribution-signing|apple-signing)
|
||||
distribution_env="$(compose_distribution_signing_env)"
|
||||
seal_secret "secrets/apple/distribution-signing.env.age" "$distribution_env"
|
||||
;;
|
||||
sparkle)
|
||||
if sparkle_env="$(compose_sparkle_env)"; then
|
||||
seal_secret "secrets/apple/sparkle.env.age" "$sparkle_env"
|
||||
else
|
||||
echo "required Sparkle source missing or empty: sparkle-eddsa.key" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Release secrets are sealed. Commit the .age files and deploy/use CI with an authorized age identity."
|
||||
107
Scripts/sparkle/sign-appcast-kms.py
Executable file
107
Scripts/sparkle/sign-appcast-kms.py
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sign Sparkle appcast enclosures with a Google Cloud KMS Ed25519 key."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SPARKLE_NS = "http://www.andymatuschak.org/xml-namespaces/sparkle"
|
||||
DEFAULT_PROJECT = "project-88c23ce9-918a-470a-b33"
|
||||
DEFAULT_LOCATION = "global"
|
||||
DEFAULT_KEYRING = "burrow-identity"
|
||||
DEFAULT_KEY = "sparkle-ed25519"
|
||||
DEFAULT_VERSION = "1"
|
||||
|
||||
|
||||
def run(command: list[str]) -> None:
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
|
||||
def kms_sign(args: argparse.Namespace, payload: Path) -> bytes:
|
||||
with tempfile.TemporaryDirectory(prefix="burrow-sparkle-kms.") as tmp:
|
||||
signature_path = Path(tmp) / "signature.bin"
|
||||
command = [
|
||||
args.gcloud,
|
||||
"kms",
|
||||
"asymmetric-sign",
|
||||
"--location",
|
||||
args.kms_location,
|
||||
"--keyring",
|
||||
args.kms_keyring,
|
||||
"--key",
|
||||
args.kms_key,
|
||||
"--version",
|
||||
args.kms_version,
|
||||
"--input-file",
|
||||
str(payload),
|
||||
"--signature-file",
|
||||
str(signature_path),
|
||||
]
|
||||
if args.gcloud_project:
|
||||
command.extend(["--project", args.gcloud_project])
|
||||
run(command)
|
||||
return signature_path.read_bytes()
|
||||
|
||||
|
||||
def enclosure_path(enclosure: ET.Element, artifact_dir: Path) -> Path:
|
||||
url = enclosure.attrib.get("url")
|
||||
if not url:
|
||||
raise ValueError("Sparkle enclosure is missing a url attribute")
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
name = Path(parsed.path).name
|
||||
if not name:
|
||||
raise ValueError(f"Sparkle enclosure URL has no file name: {url}")
|
||||
path = artifact_dir / name
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Sparkle enclosure artifact not found: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def sign_appcast(args: argparse.Namespace) -> None:
|
||||
appcast = Path(args.appcast)
|
||||
artifact_dir = Path(args.artifact_dir) if args.artifact_dir else appcast.parent
|
||||
ET.register_namespace("sparkle", SPARKLE_NS)
|
||||
tree = ET.parse(appcast)
|
||||
root = tree.getroot()
|
||||
enclosures = root.findall(".//enclosure")
|
||||
if not enclosures:
|
||||
raise ValueError(f"no Sparkle enclosures found in {appcast}")
|
||||
|
||||
for enclosure in enclosures:
|
||||
artifact = enclosure_path(enclosure, artifact_dir)
|
||||
signature = kms_sign(args, artifact)
|
||||
enclosure.attrib[f"{{{SPARKLE_NS}}}edSignature"] = base64.b64encode(signature).decode("ascii")
|
||||
|
||||
tree.write(appcast, encoding="utf-8", xml_declaration=True)
|
||||
print(f"signed {len(enclosures)} enclosure(s) in {appcast}")
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Sign Sparkle appcast enclosures with Google Cloud KMS.")
|
||||
parser.add_argument("--appcast", required=True)
|
||||
parser.add_argument("--artifact-dir")
|
||||
parser.add_argument("--gcloud-project", default=os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("BURROW_GOOGLE_PROJECT_ID") or DEFAULT_PROJECT)
|
||||
parser.add_argument("--kms-location", default=os.environ.get("BURROW_KMS_LOCATION", DEFAULT_LOCATION))
|
||||
parser.add_argument("--kms-keyring", default=os.environ.get("BURROW_KMS_KEYRING", DEFAULT_KEYRING))
|
||||
parser.add_argument("--kms-key", default=os.environ.get("BURROW_SPARKLE_KMS_KEY", DEFAULT_KEY))
|
||||
parser.add_argument("--kms-version", default=os.environ.get("BURROW_SPARKLE_KMS_VERSION", DEFAULT_VERSION))
|
||||
parser.add_argument("--gcloud", default=os.environ.get("GCLOUD", "gcloud"))
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
sign_appcast(parse_args(argv))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
146
Scripts/version.sh
Executable file
146
Scripts/version.sh
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$PATH:/opt/homebrew/bin:/usr/local/bin:/etc/profiles/per-user/$USER/bin"
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
|
||||
TAG_PREFIX="builds/"
|
||||
DEFAULT_VERSION_REMOTE="origin"
|
||||
if git remote get-url forgejo >/dev/null 2>&1; then
|
||||
DEFAULT_VERSION_REMOTE="forgejo"
|
||||
fi
|
||||
VERSION_REMOTE="${BURROW_VERSION_REMOTE:-$DEFAULT_VERSION_REMOTE}"
|
||||
VERSION_REQUIRE_REMOTE="${BURROW_VERSION_REQUIRE_REMOTE:-false}"
|
||||
COMMAND="${1:-}"
|
||||
|
||||
truthy() {
|
||||
case "$1" in
|
||||
1|true|TRUE|True|yes|YES|Yes|y|Y|on|ON|On) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
list_local_build_tags() {
|
||||
git tag -l "${TAG_PREFIX}[0-9]*"
|
||||
}
|
||||
|
||||
latest_local_build_tag() {
|
||||
list_local_build_tags | sort -n -t'/' -k2 | tail -n 1
|
||||
}
|
||||
|
||||
latest_remote_build_record() {
|
||||
local raw rc=0
|
||||
raw="$(git ls-remote --tags --refs "$VERSION_REMOTE" "refs/tags/${TAG_PREFIX}[0-9]*" 2>/dev/null)" || rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
return $rc
|
||||
fi
|
||||
if [[ -z "$raw" ]]; then
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$raw" | awk -F'\t' -v prefix="refs/tags/${TAG_PREFIX}" '
|
||||
$2 ~ ("^" prefix "[0-9]+$") {
|
||||
build = $2
|
||||
sub("^refs/tags/", "", build)
|
||||
print build "\t" $1
|
||||
}
|
||||
' | sort -n -t'/' -k2 | tail -n 1
|
||||
}
|
||||
|
||||
resolve_build_state() {
|
||||
local remote_record local_latest remote_status=0
|
||||
BUILD_STATE_SOURCE="local"
|
||||
LATEST_BUILD=""
|
||||
LATEST_BUILD_COMMIT=""
|
||||
|
||||
remote_record="$(latest_remote_build_record)" || remote_status=$?
|
||||
if [[ $remote_status -eq 0 ]]; then
|
||||
BUILD_STATE_SOURCE="remote"
|
||||
if [[ -n "$remote_record" ]]; then
|
||||
LATEST_BUILD="$(printf '%s\n' "$remote_record" | cut -f1)"
|
||||
LATEST_BUILD_COMMIT="$(printf '%s\n' "$remote_record" | cut -f2)"
|
||||
fi
|
||||
else
|
||||
if truthy "$VERSION_REQUIRE_REMOTE"; then
|
||||
echo "error: unable to resolve authoritative build tags from ${VERSION_REMOTE}" >&2
|
||||
exit "$remote_status"
|
||||
fi
|
||||
echo "warning: unable to resolve authoritative build tags from ${VERSION_REMOTE}; falling back to local tags" >&2
|
||||
local_latest="$(latest_local_build_tag)"
|
||||
if [[ -n "$local_latest" ]]; then
|
||||
LATEST_BUILD="$local_latest"
|
||||
LATEST_BUILD_COMMIT="$(git rev-parse -q --verify "refs/tags/${LATEST_BUILD}^{commit}" 2>/dev/null || true)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$LATEST_BUILD" ]]; then
|
||||
LATEST_BUILD_NUMBER="0"
|
||||
else
|
||||
LATEST_BUILD_NUMBER="${LATEST_BUILD#$TAG_PREFIX}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_build_state
|
||||
|
||||
HEAD_COMMIT="$(git rev-parse HEAD)"
|
||||
|
||||
ANCESTRY_STATE="ok"
|
||||
if [[ -n "${LATEST_BUILD_NUMBER}" && "${LATEST_BUILD_NUMBER}" != "0" ]]; then
|
||||
if [[ -n "${LATEST_BUILD_COMMIT}" ]] && git cat-file -e "${LATEST_BUILD_COMMIT}^{commit}" 2>/dev/null; then
|
||||
if ! git merge-base --is-ancestor "${LATEST_BUILD_COMMIT}" HEAD; then
|
||||
ANCESTRY_STATE="not-descended"
|
||||
if [[ "$COMMAND" != "status" ]]; then
|
||||
echo "error: HEAD is not descended from build ${LATEST_BUILD_NUMBER}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
ANCESTRY_STATE="missing-tag-commit"
|
||||
echo "notice: skipping build ancestry check (missing ${LATEST_BUILD} commit, likely shallow checkout)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
BUILD_NUMBER="$LATEST_BUILD_NUMBER"
|
||||
|
||||
if [[ "$COMMAND" == "increment" || "$COMMAND" == "pre-increment" ]]; then
|
||||
if [[ -n "${BURROW_BUILD_NUMBER_OVERRIDE:-}" ]]; then
|
||||
if [[ ! "${BURROW_BUILD_NUMBER_OVERRIDE}" =~ ^[0-9]+$ ]]; then
|
||||
echo "error: BURROW_BUILD_NUMBER_OVERRIDE must be an integer" >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( BURROW_BUILD_NUMBER_OVERRIDE <= LATEST_BUILD_NUMBER )); then
|
||||
echo "error: BURROW_BUILD_NUMBER_OVERRIDE must be greater than ${LATEST_BUILD_NUMBER}" >&2
|
||||
exit 1
|
||||
fi
|
||||
NEW_BUILD_NUMBER="${BURROW_BUILD_NUMBER_OVERRIDE}"
|
||||
else
|
||||
NEW_BUILD_NUMBER=$((LATEST_BUILD_NUMBER + 1))
|
||||
fi
|
||||
NEW_TAG="$TAG_PREFIX$NEW_BUILD_NUMBER"
|
||||
BUILD_NUMBER="$NEW_BUILD_NUMBER"
|
||||
fi
|
||||
|
||||
if [[ "$COMMAND" == "increment" ]]; then
|
||||
git tag "$NEW_TAG"
|
||||
git push --quiet "$VERSION_REMOTE" "$NEW_TAG"
|
||||
fi
|
||||
|
||||
CONFIG_PATH="Apple/Configuration/Version.xcconfig"
|
||||
if [[ "$COMMAND" != "status" && -f "$CONFIG_PATH" ]]; then
|
||||
current_config_version="$(sed -n 's/^[[:space:]]*CURRENT_PROJECT_VERSION[[:space:]]*=[[:space:]]*//p' "$CONFIG_PATH" 2>/dev/null | tail -n 1 || true)"
|
||||
if [[ "$current_config_version" != "$BUILD_NUMBER" ]]; then
|
||||
echo "CURRENT_PROJECT_VERSION = $BUILD_NUMBER" > "$CONFIG_PATH"
|
||||
fi
|
||||
git update-index --assume-unchanged "$CONFIG_PATH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [[ "$COMMAND" == "status" ]]; then
|
||||
if [[ "${ANCESTRY_STATE}" == "ok" && "${LATEST_BUILD_NUMBER}" != "0" && -n "${LATEST_BUILD_COMMIT}" && "${HEAD_COMMIT}" == "${LATEST_BUILD_COMMIT}" ]]; then
|
||||
echo "clean"
|
||||
else
|
||||
echo "dirty"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$BUILD_NUMBER"
|
||||
14
bazel/android/BUILD.bazel
Normal file
14
bazel/android/BUILD.bazel
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
|
||||
load(":android_targets.bzl", "android_stub_stamp")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
sh_binary(
|
||||
name = "runner",
|
||||
srcs = ["runner.sh"],
|
||||
)
|
||||
|
||||
android_stub_stamp(
|
||||
name = "check_stub_stamp",
|
||||
command = "check-stub",
|
||||
)
|
||||
12
bazel/android/android_targets.bzl
Normal file
12
bazel/android/android_targets.bzl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
def android_stub_stamp(name, command):
|
||||
native.genrule(
|
||||
name = name,
|
||||
outs = [name + ".stamp"],
|
||||
cmd = "$(location :runner) %s && : > \"$@\"" % command,
|
||||
srcs = [
|
||||
"//:android_project_files",
|
||||
"//:mobile_rust_core_files",
|
||||
],
|
||||
tools = [":runner"],
|
||||
tags = ["no-sandbox"],
|
||||
)
|
||||
73
bazel/android/runner.sh
Executable file
73
bazel/android/runner.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
command="${1:-}"
|
||||
|
||||
resolve_repo_root() {
|
||||
if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" && -f "${BUILD_WORKSPACE_DIRECTORY}/MODULE.bazel" ]]; then
|
||||
printf '%s\n' "$BUILD_WORKSPACE_DIRECTORY"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local dir="$PWD"
|
||||
while [[ "$dir" != "/" ]]; do
|
||||
if [[ -f "$dir/MODULE.bazel" && -d "$dir/Android" ]]; then
|
||||
printf '%s\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
|
||||
echo "unable to resolve Burrow repository root" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
repo_root="$(resolve_repo_root)"
|
||||
cd "$repo_root"
|
||||
|
||||
cargo_check_locked() {
|
||||
local -a cargo_path_prefixes
|
||||
local home_dir user_name
|
||||
cargo_path_prefixes=()
|
||||
home_dir="${HOME:-}"
|
||||
if [[ -n "$home_dir" ]]; then
|
||||
cargo_path_prefixes+=("$home_dir/.cargo/bin")
|
||||
fi
|
||||
user_name="$(id -un 2>/dev/null || true)"
|
||||
if [[ -n "$user_name" ]]; then
|
||||
cargo_path_prefixes+=(
|
||||
"/Users/${user_name}/.cargo/bin"
|
||||
"/home/${user_name}/.cargo/bin"
|
||||
"/etc/profiles/per-user/${user_name}/bin"
|
||||
)
|
||||
fi
|
||||
cargo_path_prefixes+=("/opt/homebrew/bin" "/usr/local/bin" "/nix/var/nix/profiles/default/bin" "/run/current-system/sw/bin")
|
||||
export PATH="$(IFS=:; printf '%s' "${cargo_path_prefixes[*]}"):$PATH"
|
||||
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
cargo check --locked -p burrow-mobile-ffi
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${BURROW_ANDROID_BAZEL_USE_NIX:-0}" == "1" ]] && command -v nix >/dev/null 2>&1; then
|
||||
nix develop .#ci -c cargo check --locked -p burrow-mobile-ffi
|
||||
return
|
||||
fi
|
||||
|
||||
echo "cargo or nix is required to check the Android Rust core stub" >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
case "$command" in
|
||||
check-stub)
|
||||
test -f Android/settings.gradle.kts
|
||||
test -f Android/app/src/main/java/net/burrow/android/MainActivity.kt
|
||||
test -f crates/burrow-core/src/lib.rs
|
||||
test -f crates/burrow-mobile-ffi/src/lib.rs
|
||||
cargo_check_locked
|
||||
;;
|
||||
*)
|
||||
echo "unknown Android runner command: ${command}" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
25
bazel/apple/BUILD.bazel
Normal file
25
bazel/apple/BUILD.bazel
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
|
||||
load(":apple_targets.bzl", "apple_bazel_stamp")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
sh_binary(
|
||||
name = "runner",
|
||||
srcs = ["runner.sh"],
|
||||
target_compatible_with = ["@platforms//os:macos"],
|
||||
)
|
||||
|
||||
apple_bazel_stamp(
|
||||
name = "release_ios_stamp",
|
||||
command = "release-ios",
|
||||
)
|
||||
|
||||
apple_bazel_stamp(
|
||||
name = "release_macos_stamp",
|
||||
command = "release-macos",
|
||||
)
|
||||
|
||||
apple_bazel_stamp(
|
||||
name = "release_all_stamp",
|
||||
command = "release-all",
|
||||
)
|
||||
13
bazel/apple/apple_targets.bzl
Normal file
13
bazel/apple/apple_targets.bzl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
def apple_bazel_stamp(name, command):
|
||||
native.genrule(
|
||||
name = name,
|
||||
outs = [name + ".stamp"],
|
||||
cmd = "$(location :runner) %s && : > \"$@\"" % command,
|
||||
srcs = ["//:apple_project_files"],
|
||||
tools = [":runner"],
|
||||
# The Xcode script stages release artifacts under dist/builds as a
|
||||
# deliberate CI side effect; keep the wrapper uncached so the staging
|
||||
# step always runs in fresh and reused workspaces.
|
||||
tags = ["no-cache", "no-remote-cache", "no-sandbox"],
|
||||
target_compatible_with = ["@platforms//os:macos"],
|
||||
)
|
||||
42
bazel/apple/runner.sh
Executable file
42
bazel/apple/runner.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
command="${1:-}"
|
||||
|
||||
resolve_repo_root() {
|
||||
if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" && -f "${BUILD_WORKSPACE_DIRECTORY}/MODULE.bazel" ]]; then
|
||||
printf '%s\n' "$BUILD_WORKSPACE_DIRECTORY"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local dir="$PWD"
|
||||
while [[ "$dir" != "/" ]]; do
|
||||
if [[ -f "$dir/MODULE.bazel" && -d "$dir/Apple" ]]; then
|
||||
printf '%s\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
|
||||
echo "unable to resolve Burrow repository root" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
repo_root="$(resolve_repo_root)"
|
||||
cd "$repo_root"
|
||||
|
||||
case "$command" in
|
||||
release-ios)
|
||||
Scripts/ci/package-apple-artifacts.sh ios
|
||||
;;
|
||||
release-macos)
|
||||
Scripts/ci/package-apple-artifacts.sh macos
|
||||
;;
|
||||
release-all)
|
||||
Scripts/ci/package-apple-artifacts.sh all
|
||||
;;
|
||||
*)
|
||||
echo "unknown apple runner command: ${command}" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
515
burrow-gtk/src/components/networks_screen.rs
Normal file
515
burrow-gtk/src/components/networks_screen.rs
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
use super::*;
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::time::Duration;
|
||||
|
||||
pub struct NetworksScreen {
|
||||
list_box: gtk::ListBox,
|
||||
network_status: gtk::Label,
|
||||
wireguard_text: gtk::TextView,
|
||||
tailnet_email_entry: gtk::Entry,
|
||||
tailnet_authority_entry: gtk::Entry,
|
||||
tailnet_account_entry: gtk::Entry,
|
||||
tailnet_identity_entry: gtk::Entry,
|
||||
tailnet_hostname_entry: gtk::Entry,
|
||||
tailnet_name_entry: gtk::Entry,
|
||||
custom_authority_switch: gtk::Switch,
|
||||
tailnet_status: gtk::Label,
|
||||
tailnet_session_id: Option<String>,
|
||||
tailnet_running: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NetworksScreenMsg {
|
||||
Refresh,
|
||||
AddWireGuard,
|
||||
DiscoverTailnet,
|
||||
ProbeTailnet,
|
||||
StartTailnetLogin,
|
||||
PollTailnetLogin,
|
||||
CancelTailnetLogin,
|
||||
AddTailnet,
|
||||
}
|
||||
|
||||
#[relm4::component(pub, async)]
|
||||
impl AsyncComponent for NetworksScreen {
|
||||
type Init = ();
|
||||
type Input = NetworksScreenMsg;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
gtk::ScrolledWindow {
|
||||
set_vexpand: true,
|
||||
|
||||
adw::Clamp {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 12,
|
||||
set_margin_all: 12,
|
||||
|
||||
adw::PreferencesGroup {
|
||||
set_title: "Stored Networks",
|
||||
set_description: Some("Networks saved in the Burrow daemon"),
|
||||
|
||||
#[name(network_status)]
|
||||
gtk::Label {
|
||||
set_xalign: 0.0,
|
||||
set_wrap: true,
|
||||
set_label: "Loading networks",
|
||||
},
|
||||
|
||||
#[name(list_box)]
|
||||
gtk::ListBox {
|
||||
add_css_class: "boxed-list",
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Refresh",
|
||||
connect_clicked => NetworksScreenMsg::Refresh
|
||||
},
|
||||
},
|
||||
|
||||
adw::PreferencesGroup {
|
||||
set_title: "Add WireGuard",
|
||||
set_description: Some("Import a WireGuard configuration into the daemon"),
|
||||
|
||||
gtk::ScrolledWindow {
|
||||
set_min_content_height: 150,
|
||||
set_vexpand: false,
|
||||
|
||||
#[name(wireguard_text)]
|
||||
gtk::TextView {
|
||||
set_monospace: true,
|
||||
set_wrap_mode: gtk::WrapMode::WordChar,
|
||||
}
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Add WireGuard Network",
|
||||
connect_clicked => NetworksScreenMsg::AddWireGuard
|
||||
},
|
||||
},
|
||||
|
||||
adw::PreferencesGroup {
|
||||
set_title: "Tailnet",
|
||||
set_description: Some("Discover, probe, sign in, and save Tailnet authorities through the daemon"),
|
||||
|
||||
#[name(tailnet_email_entry)]
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Email address"),
|
||||
},
|
||||
|
||||
#[name(custom_authority_switch)]
|
||||
gtk::Switch {
|
||||
set_halign: Align::Start,
|
||||
set_active: false,
|
||||
},
|
||||
|
||||
#[name(tailnet_authority_entry)]
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Tailnet server URL"),
|
||||
},
|
||||
|
||||
#[name(tailnet_account_entry)]
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Account name"),
|
||||
},
|
||||
|
||||
#[name(tailnet_identity_entry)]
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Identity name"),
|
||||
},
|
||||
|
||||
#[name(tailnet_hostname_entry)]
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Device hostname"),
|
||||
},
|
||||
|
||||
#[name(tailnet_name_entry)]
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Tailnet name"),
|
||||
},
|
||||
|
||||
#[name(tailnet_status)]
|
||||
gtk::Label {
|
||||
set_xalign: 0.0,
|
||||
set_wrap: true,
|
||||
set_selectable: true,
|
||||
set_label: "Use discovery for the managed Tailscale authority or enter a custom Tailnet server.",
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 8,
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Discover",
|
||||
connect_clicked => NetworksScreenMsg::DiscoverTailnet
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Probe",
|
||||
connect_clicked => NetworksScreenMsg::ProbeTailnet
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Sign In",
|
||||
connect_clicked => NetworksScreenMsg::StartTailnetLogin
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Cancel",
|
||||
connect_clicked => NetworksScreenMsg::CancelTailnetLogin
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Save Tailnet Network",
|
||||
connect_clicked => NetworksScreenMsg::AddTailnet
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn init(
|
||||
_: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let widgets = view_output!();
|
||||
widgets
|
||||
.tailnet_authority_entry
|
||||
.set_text(daemon_api::default_tailnet_authority());
|
||||
widgets.tailnet_account_entry.set_text("default");
|
||||
widgets.tailnet_identity_entry.set_text("linux");
|
||||
|
||||
let mut model = NetworksScreen {
|
||||
list_box: widgets.list_box.clone(),
|
||||
network_status: widgets.network_status.clone(),
|
||||
wireguard_text: widgets.wireguard_text.clone(),
|
||||
tailnet_email_entry: widgets.tailnet_email_entry.clone(),
|
||||
tailnet_authority_entry: widgets.tailnet_authority_entry.clone(),
|
||||
tailnet_account_entry: widgets.tailnet_account_entry.clone(),
|
||||
tailnet_identity_entry: widgets.tailnet_identity_entry.clone(),
|
||||
tailnet_hostname_entry: widgets.tailnet_hostname_entry.clone(),
|
||||
tailnet_name_entry: widgets.tailnet_name_entry.clone(),
|
||||
custom_authority_switch: widgets.custom_authority_switch.clone(),
|
||||
tailnet_status: widgets.tailnet_status.clone(),
|
||||
tailnet_session_id: None,
|
||||
tailnet_running: false,
|
||||
};
|
||||
|
||||
model.refresh_networks().await;
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
msg: Self::Input,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
match msg {
|
||||
NetworksScreenMsg::Refresh => self.refresh_networks().await,
|
||||
NetworksScreenMsg::AddWireGuard => self.add_wireguard().await,
|
||||
NetworksScreenMsg::DiscoverTailnet => self.discover_tailnet().await,
|
||||
NetworksScreenMsg::ProbeTailnet => self.probe_tailnet().await,
|
||||
NetworksScreenMsg::StartTailnetLogin => self.start_tailnet_login(sender).await,
|
||||
NetworksScreenMsg::PollTailnetLogin => self.poll_tailnet_login(sender).await,
|
||||
NetworksScreenMsg::CancelTailnetLogin => self.cancel_tailnet_login().await,
|
||||
NetworksScreenMsg::AddTailnet => self.add_tailnet().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworksScreen {
|
||||
async fn refresh_networks(&mut self) {
|
||||
match daemon_api::list_networks().await {
|
||||
Ok(networks) => {
|
||||
self.render_networks(&networks);
|
||||
self.network_status.set_label(if networks.is_empty() {
|
||||
"No daemon networks saved yet"
|
||||
} else {
|
||||
"Daemon networks are up to date"
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
self.clear_networks();
|
||||
self.network_status
|
||||
.set_label(&format!("Unable to read daemon networks: {error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_wireguard(&mut self) {
|
||||
let config = text_view_text(&self.wireguard_text);
|
||||
if config.trim().is_empty() {
|
||||
self.network_status
|
||||
.set_label("Paste a WireGuard configuration before adding a network");
|
||||
return;
|
||||
}
|
||||
|
||||
match daemon_api::add_wireguard(config).await {
|
||||
Ok(id) => {
|
||||
self.network_status
|
||||
.set_label(&format!("Added WireGuard network #{id}"));
|
||||
self.wireguard_text.buffer().set_text("");
|
||||
self.refresh_networks().await;
|
||||
}
|
||||
Err(error) => self
|
||||
.network_status
|
||||
.set_label(&format!("Unable to add WireGuard network: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn discover_tailnet(&mut self) {
|
||||
let email = self.tailnet_email_entry.text().to_string();
|
||||
let Ok(email) = daemon_api::require_value(&email, "Email address") else {
|
||||
self.tailnet_status
|
||||
.set_label("Enter an email address before discovery");
|
||||
return;
|
||||
};
|
||||
|
||||
self.tailnet_status.set_label("Discovering Tailnet server");
|
||||
match daemon_api::discover_tailnet(email).await {
|
||||
Ok(discovery) => {
|
||||
self.tailnet_authority_entry.set_text(&discovery.authority);
|
||||
self.custom_authority_switch.set_active(!discovery.managed);
|
||||
let issuer = discovery
|
||||
.oidc_issuer
|
||||
.map(|issuer| format!(" OIDC issuer: {issuer}."))
|
||||
.unwrap_or_default();
|
||||
let kind = if discovery.managed {
|
||||
"managed authority"
|
||||
} else {
|
||||
"custom authority"
|
||||
};
|
||||
self.tailnet_status.set_label(&format!(
|
||||
"Discovered {kind}: {}.{issuer}",
|
||||
discovery.authority
|
||||
));
|
||||
}
|
||||
Err(error) => self
|
||||
.tailnet_status
|
||||
.set_label(&format!("Tailnet discovery failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe_tailnet(&mut self) {
|
||||
let authority = self.tailnet_authority();
|
||||
let Ok(authority) = daemon_api::require_value(&authority, "Tailnet server URL") else {
|
||||
self.tailnet_status
|
||||
.set_label("Enter a Tailnet server URL before probing");
|
||||
return;
|
||||
};
|
||||
|
||||
self.tailnet_status.set_label("Checking Tailnet server");
|
||||
match daemon_api::probe_tailnet(authority).await {
|
||||
Ok(probe) => {
|
||||
let detail = probe
|
||||
.detail
|
||||
.unwrap_or_else(|| format!("HTTP {}", probe.status_code));
|
||||
self.tailnet_status
|
||||
.set_label(&format!("{}: {detail}", probe.summary));
|
||||
}
|
||||
Err(error) => self
|
||||
.tailnet_status
|
||||
.set_label(&format!("Tailnet probe failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_tailnet_login(&mut self, sender: AsyncComponentSender<Self>) {
|
||||
let authority = self.tailnet_authority();
|
||||
let Ok(authority) = daemon_api::require_value(&authority, "Tailnet server URL") else {
|
||||
self.tailnet_status
|
||||
.set_label("Enter a Tailnet server URL before sign-in");
|
||||
return;
|
||||
};
|
||||
let account = daemon_api::normalized(&self.tailnet_account_entry.text(), "default");
|
||||
let identity = daemon_api::normalized(&self.tailnet_identity_entry.text(), "linux");
|
||||
let hostname = daemon_api::normalized_optional(&self.tailnet_hostname_entry.text());
|
||||
|
||||
self.tailnet_status.set_label("Starting Tailnet sign-in");
|
||||
match daemon_api::start_tailnet_login(authority, account, identity, hostname).await {
|
||||
Ok(status) => {
|
||||
self.apply_login_status(&status);
|
||||
if let Some(auth_url) = status.auth_url.as_deref() {
|
||||
if let Err(error) = open_auth_url(auth_url) {
|
||||
self.tailnet_status.set_label(&format!(
|
||||
"{} Open this URL manually: {auth_url}. Browser launch failed: {error}",
|
||||
self.tailnet_status.text()
|
||||
));
|
||||
}
|
||||
}
|
||||
if !status.running {
|
||||
sender.input(NetworksScreenMsg::PollTailnetLogin);
|
||||
}
|
||||
}
|
||||
Err(error) => self
|
||||
.tailnet_status
|
||||
.set_label(&format!("Tailnet sign-in failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn poll_tailnet_login(&mut self, sender: AsyncComponentSender<Self>) {
|
||||
let Some(session_id) = self.tailnet_session_id.clone() else {
|
||||
return;
|
||||
};
|
||||
if self.tailnet_running {
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
match daemon_api::tailnet_login_status(session_id).await {
|
||||
Ok(status) => {
|
||||
self.apply_login_status(&status);
|
||||
if !status.running {
|
||||
sender.input(NetworksScreenMsg::PollTailnetLogin);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
self.tailnet_status
|
||||
.set_label(&format!("Tailnet sign-in status failed: {error}"));
|
||||
self.tailnet_session_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cancel_tailnet_login(&mut self) {
|
||||
let Some(session_id) = self.tailnet_session_id.clone() else {
|
||||
self.tailnet_status
|
||||
.set_label("No Tailnet sign-in is active");
|
||||
return;
|
||||
};
|
||||
|
||||
match daemon_api::cancel_tailnet_login(session_id).await {
|
||||
Ok(()) => {
|
||||
self.tailnet_session_id = None;
|
||||
self.tailnet_running = false;
|
||||
self.tailnet_status.set_label("Tailnet sign-in cancelled");
|
||||
}
|
||||
Err(error) => self
|
||||
.tailnet_status
|
||||
.set_label(&format!("Unable to cancel Tailnet sign-in: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_tailnet(&mut self) {
|
||||
let authority = self.tailnet_authority();
|
||||
let Ok(authority) = daemon_api::require_value(&authority, "Tailnet server URL") else {
|
||||
self.tailnet_status
|
||||
.set_label("Enter a Tailnet server URL before saving");
|
||||
return;
|
||||
};
|
||||
if self.tailnet_session_id.is_some() && !self.tailnet_running {
|
||||
self.tailnet_status
|
||||
.set_label("Finish browser sign-in before saving this Tailnet network");
|
||||
return;
|
||||
}
|
||||
|
||||
let account = daemon_api::normalized(&self.tailnet_account_entry.text(), "default");
|
||||
let identity = daemon_api::normalized(&self.tailnet_identity_entry.text(), "linux");
|
||||
let hostname = daemon_api::normalized_optional(&self.tailnet_hostname_entry.text());
|
||||
let tailnet = daemon_api::normalized_optional(&self.tailnet_name_entry.text());
|
||||
|
||||
match daemon_api::add_tailnet(authority, account, identity, hostname, tailnet).await {
|
||||
Ok(id) => {
|
||||
self.tailnet_status
|
||||
.set_label(&format!("Saved Tailnet network #{id}"));
|
||||
self.refresh_networks().await;
|
||||
}
|
||||
Err(error) => self
|
||||
.tailnet_status
|
||||
.set_label(&format!("Unable to save Tailnet network: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_login_status(&mut self, status: &daemon_api::TailnetLoginStatus) {
|
||||
self.tailnet_session_id = Some(status.session_id.clone());
|
||||
self.tailnet_running = status.running;
|
||||
|
||||
let mut parts = Vec::new();
|
||||
if status.running {
|
||||
parts.push("Signed in".to_owned());
|
||||
} else if status.needs_login {
|
||||
parts.push("Browser sign-in required".to_owned());
|
||||
} else {
|
||||
parts.push("Checking sign-in".to_owned());
|
||||
}
|
||||
if !status.backend_state.is_empty() {
|
||||
parts.push(format!("State: {}", status.backend_state));
|
||||
}
|
||||
if let Some(tailnet_name) = &status.tailnet_name {
|
||||
parts.push(format!("Tailnet: {tailnet_name}"));
|
||||
}
|
||||
if let Some(self_dns_name) = &status.self_dns_name {
|
||||
parts.push(self_dns_name.clone());
|
||||
}
|
||||
if !status.tailnet_ips.is_empty() {
|
||||
parts.push(status.tailnet_ips.join(", "));
|
||||
}
|
||||
if !status.health.is_empty() {
|
||||
parts.push(status.health.join(" / "));
|
||||
}
|
||||
if let Some(auth_url) = &status.auth_url {
|
||||
parts.push(format!("Auth URL: {auth_url}"));
|
||||
}
|
||||
|
||||
self.tailnet_status.set_label(&parts.join("\n"));
|
||||
}
|
||||
|
||||
fn tailnet_authority(&self) -> String {
|
||||
if self.custom_authority_switch.is_active() {
|
||||
self.tailnet_authority_entry.text().to_string()
|
||||
} else {
|
||||
let authority = self.tailnet_authority_entry.text();
|
||||
if authority.trim().is_empty() {
|
||||
daemon_api::default_tailnet_authority().to_owned()
|
||||
} else {
|
||||
authority.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_networks(&self) {
|
||||
while let Some(child) = self.list_box.first_child() {
|
||||
self.list_box.remove(&child);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_networks(&self, networks: &[daemon_api::NetworkSummary]) {
|
||||
self.clear_networks();
|
||||
if networks.is_empty() {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title("No networks")
|
||||
.subtitle("Save a WireGuard or Tailnet network to use it here.")
|
||||
.build();
|
||||
self.list_box.append(&row);
|
||||
return;
|
||||
}
|
||||
|
||||
for network in networks {
|
||||
let title = format!("{} (#{})", network.title, network.id);
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(&title)
|
||||
.subtitle(&network.detail)
|
||||
.build();
|
||||
self.list_box.append(&row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_view_text(text_view: >k::TextView) -> String {
|
||||
let buffer = text_view.buffer();
|
||||
buffer
|
||||
.text(&buffer.start_iter(), &buffer.end_iter(), true)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn open_auth_url(url: &str) -> Result<()> {
|
||||
gtk::gio::AppInfo::launch_default_for_uri(url, None::<>k::gio::AppLaunchContext>)
|
||||
.with_context(|| format!("failed to open {url}"))
|
||||
}
|
||||
|
|
@ -4,12 +4,10 @@ use std::process::Command;
|
|||
#[derive(Debug)]
|
||||
pub struct DaemonGroup {
|
||||
system_setup: SystemSetup,
|
||||
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
already_running: bool,
|
||||
}
|
||||
|
||||
pub struct DaemonGroupInit {
|
||||
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
pub system_setup: SystemSetup,
|
||||
}
|
||||
|
||||
|
|
@ -51,8 +49,7 @@ impl AsyncComponent for DaemonGroup {
|
|||
// Should be impossible to panic here
|
||||
let model = DaemonGroup {
|
||||
system_setup: init.system_setup,
|
||||
daemon_client: init.daemon_client.clone(),
|
||||
already_running: init.daemon_client.lock().await.is_some(),
|
||||
already_running: daemon_api::daemon_available().await,
|
||||
};
|
||||
|
||||
let widgets = view_output!();
|
||||
|
|
@ -104,7 +101,7 @@ mv $TEMP /tmp/burrow-detached-daemon"#,
|
|||
.unwrap();
|
||||
}
|
||||
DaemonGroupMsg::DaemonStateChange => {
|
||||
self.already_running = self.daemon_client.lock().await.is_some();
|
||||
self.already_running = daemon_api::daemon_available().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ use super::*;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct DiagGroup {
|
||||
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
|
||||
system_setup: SystemSetup,
|
||||
service_installed: StatusTernary,
|
||||
socket_installed: StatusTernary,
|
||||
|
|
@ -12,14 +10,13 @@ pub struct DiagGroup {
|
|||
}
|
||||
|
||||
pub struct DiagGroupInit {
|
||||
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
pub system_setup: SystemSetup,
|
||||
}
|
||||
|
||||
impl DiagGroup {
|
||||
async fn new(daemon_client: Arc<Mutex<Option<DaemonClient>>>) -> Result<Self> {
|
||||
async fn new() -> Result<Self> {
|
||||
let system_setup = SystemSetup::new();
|
||||
let daemon_running = daemon_client.lock().await.is_some();
|
||||
let daemon_running = daemon_api::daemon_available().await;
|
||||
|
||||
Ok(Self {
|
||||
service_installed: system_setup.is_service_installed()?,
|
||||
|
|
@ -27,7 +24,6 @@ impl DiagGroup {
|
|||
socket_enabled: system_setup.is_socket_enabled()?,
|
||||
daemon_running,
|
||||
system_setup,
|
||||
daemon_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +91,8 @@ impl AsyncComponent for DiagGroup {
|
|||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
// Should be impossible to panic here
|
||||
let model = DiagGroup::new(init.daemon_client).await.unwrap();
|
||||
let mut model = DiagGroup::new().await.unwrap();
|
||||
model.system_setup = init.system_setup;
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
|
|
@ -111,7 +108,7 @@ impl AsyncComponent for DiagGroup {
|
|||
match msg {
|
||||
DiagGroupMsg::Refresh => {
|
||||
// Should be impossible to panic here
|
||||
*self = Self::new(Arc::clone(&self.daemon_client)).await.unwrap();
|
||||
*self = Self::new().await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ pub struct SettingsScreen {
|
|||
daemon_group: AsyncController<settings::DaemonGroup>,
|
||||
}
|
||||
|
||||
pub struct SettingsScreenInit {
|
||||
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum SettingsScreenMsg {
|
||||
DaemonStateChange,
|
||||
|
|
@ -17,7 +13,7 @@ pub enum SettingsScreenMsg {
|
|||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for SettingsScreen {
|
||||
type Init = SettingsScreenInit;
|
||||
type Init = ();
|
||||
type Input = SettingsScreenMsg;
|
||||
type Output = ();
|
||||
|
||||
|
|
@ -27,26 +23,20 @@ impl SimpleComponent for SettingsScreen {
|
|||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
_init: Self::Init,
|
||||
root: &Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let system_setup = SystemSetup::new();
|
||||
|
||||
let diag_group = settings::DiagGroup::builder()
|
||||
.launch(settings::DiagGroupInit {
|
||||
system_setup,
|
||||
daemon_client: Arc::clone(&init.daemon_client),
|
||||
})
|
||||
.launch(settings::DiagGroupInit { system_setup })
|
||||
.forward(sender.input_sender(), |_| {
|
||||
SettingsScreenMsg::DaemonStateChange
|
||||
});
|
||||
|
||||
let daemon_group = settings::DaemonGroup::builder()
|
||||
.launch(settings::DaemonGroupInit {
|
||||
system_setup,
|
||||
daemon_client: Arc::clone(&init.daemon_client),
|
||||
})
|
||||
.launch(settings::DaemonGroupInit { system_setup })
|
||||
.forward(sender.input_sender(), |_| {
|
||||
SettingsScreenMsg::DaemonStateChange
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,27 +1,22 @@
|
|||
use super::*;
|
||||
|
||||
pub struct SwitchScreen {
|
||||
daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
switch: gtk::Switch,
|
||||
switch_screen: gtk::Box,
|
||||
disconnected_banner: adw::Banner,
|
||||
}
|
||||
|
||||
pub struct SwitchScreenInit {
|
||||
pub daemon_client: Arc<Mutex<Option<DaemonClient>>>,
|
||||
status_label: gtk::Label,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum SwitchScreenMsg {
|
||||
DaemonReconnect,
|
||||
DaemonDisconnect,
|
||||
Refresh,
|
||||
Start,
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[relm4::component(pub, async)]
|
||||
impl AsyncComponent for SwitchScreen {
|
||||
type Init = SwitchScreenInit;
|
||||
type Init = ();
|
||||
type Input = SwitchScreenMsg;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
|
|
@ -39,7 +34,7 @@ impl AsyncComponent for SwitchScreen {
|
|||
|
||||
#[name(setup_banner)]
|
||||
adw::Banner {
|
||||
set_title: "Burrow is not running!",
|
||||
set_title: "Burrow daemon is not reachable",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -52,7 +47,12 @@ impl AsyncComponent for SwitchScreen {
|
|||
set_vexpand: true,
|
||||
|
||||
gtk::Label {
|
||||
set_label: "Burrow Switch",
|
||||
set_label: "Burrow Tunnel",
|
||||
},
|
||||
|
||||
#[name(status_label)]
|
||||
gtk::Label {
|
||||
set_label: "Checking daemon status",
|
||||
},
|
||||
|
||||
#[name(switch)]
|
||||
|
|
@ -68,91 +68,76 @@ impl AsyncComponent for SwitchScreen {
|
|||
}
|
||||
|
||||
async fn init(
|
||||
init: Self::Init,
|
||||
_: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
) -> AsyncComponentParts<Self> {
|
||||
let mut initial_switch_status = false;
|
||||
let mut initial_daemon_server_down = false;
|
||||
|
||||
if let Some(daemon_client) = init.daemon_client.lock().await.as_mut() {
|
||||
if let Ok(res) = daemon_client
|
||||
.send_command(DaemonCommand::ServerInfo)
|
||||
.await
|
||||
.as_ref()
|
||||
{
|
||||
initial_switch_status = match res.result.as_ref() {
|
||||
Ok(DaemonResponseData::None) => false,
|
||||
Ok(DaemonResponseData::ServerInfo(_)) => true,
|
||||
_ => false,
|
||||
};
|
||||
} else {
|
||||
initial_daemon_server_down = true;
|
||||
}
|
||||
} else {
|
||||
initial_daemon_server_down = true;
|
||||
}
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
widgets.switch.set_active(initial_switch_status);
|
||||
|
||||
if initial_daemon_server_down {
|
||||
*init.daemon_client.lock().await = None;
|
||||
widgets.switch.set_active(false);
|
||||
widgets.switch_screen.set_sensitive(false);
|
||||
widgets.setup_banner.set_revealed(true);
|
||||
}
|
||||
|
||||
let model = SwitchScreen {
|
||||
daemon_client: init.daemon_client,
|
||||
let mut model = SwitchScreen {
|
||||
switch: widgets.switch.clone(),
|
||||
switch_screen: widgets.switch_screen.clone(),
|
||||
disconnected_banner: widgets.setup_banner.clone(),
|
||||
status_label: widgets.status_label.clone(),
|
||||
};
|
||||
|
||||
model.refresh().await;
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&mut self,
|
||||
msg: Self::Input,
|
||||
_: AsyncComponentSender<Self>,
|
||||
_sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
let mut disconnected_daemon_client = false;
|
||||
|
||||
if let Some(daemon_client) = self.daemon_client.lock().await.as_mut() {
|
||||
match msg {
|
||||
Self::Input::Start => {
|
||||
if let Err(_e) = daemon_client
|
||||
.send_command(DaemonCommand::Start(Default::default()))
|
||||
.await
|
||||
{
|
||||
disconnected_daemon_client = true;
|
||||
SwitchScreenMsg::Refresh => self.refresh().await,
|
||||
SwitchScreenMsg::Start => {
|
||||
match daemon_api::start_tunnel().await {
|
||||
Ok(()) => self.status_label.set_label("Tunnel running"),
|
||||
Err(error) => self
|
||||
.status_label
|
||||
.set_label(&format!("Start failed: {error}")),
|
||||
}
|
||||
self.refresh().await;
|
||||
}
|
||||
Self::Input::Stop => {
|
||||
if let Err(_e) = daemon_client.send_command(DaemonCommand::Stop).await {
|
||||
disconnected_daemon_client = true;
|
||||
SwitchScreenMsg::Stop => {
|
||||
match daemon_api::stop_tunnel().await {
|
||||
Ok(()) => self.status_label.set_label("Tunnel stopped"),
|
||||
Err(error) => self
|
||||
.status_label
|
||||
.set_label(&format!("Stop failed: {error}")),
|
||||
}
|
||||
self.refresh().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SwitchScreen {
|
||||
async fn refresh(&mut self) {
|
||||
match daemon_api::tunnel_state().await {
|
||||
Ok(daemon_api::TunnelState::Running) => {
|
||||
self.disconnected_banner.set_revealed(false);
|
||||
self.switch_screen.set_sensitive(true);
|
||||
self.status_label.set_label("Tunnel running");
|
||||
self.switch.set_active(true);
|
||||
}
|
||||
Ok(daemon_api::TunnelState::Stopped) => {
|
||||
self.disconnected_banner.set_revealed(false);
|
||||
self.switch_screen.set_sensitive(true);
|
||||
self.status_label.set_label("Tunnel stopped");
|
||||
self.switch.set_active(false);
|
||||
}
|
||||
Err(error) => {
|
||||
self.disconnected_banner.set_revealed(true);
|
||||
self.switch_screen.set_sensitive(false);
|
||||
self.status_label
|
||||
.set_label(&format!("Daemon unavailable: {error}"));
|
||||
self.switch.set_active(false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
disconnected_daemon_client = true;
|
||||
}
|
||||
|
||||
if msg == Self::Input::DaemonReconnect {
|
||||
self.disconnected_banner.set_revealed(false);
|
||||
self.switch_screen.set_sensitive(true);
|
||||
}
|
||||
|
||||
if disconnected_daemon_client || msg == Self::Input::DaemonDisconnect {
|
||||
*self.daemon_client.lock().await = None;
|
||||
self.switch.set_active(false);
|
||||
self.switch_screen.set_sensitive(false);
|
||||
self.disconnected_banner.set_revealed(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ use crate::daemon::rpc::{grpc_defs::Empty, BurrowClient};
|
|||
#[command(
|
||||
about = "Burrow is a tool for burrowing through firewalls, built by teenagers at Hack Club.",
|
||||
long_about = "Burrow is a 🚀 blazingly fast 🚀 tool designed to penetrate unnecessarily restrictive firewalls, providing teenagers worldwide with secure, less-filtered, and safe access to the internet!
|
||||
It's being built by teenagers from Hack Club, in public! Check it out: https://github.com/hackclub/burrow
|
||||
Spotted a bug? Please open an issue! https://github.com/hackclub/burrow/issues/new"
|
||||
It's being built by teenagers from Hack Club, in public! Check it out: https://git.burrow.net/hackclub/burrow
|
||||
Spotted a bug? Please open an issue on the Burrow forge."
|
||||
)]
|
||||
|
||||
struct Cli {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
groups = {
|
||||
users = "burrow-users";
|
||||
admins = "burrow-admins";
|
||||
automation = "burrow-automation";
|
||||
linear = {
|
||||
owners = "linear-owners";
|
||||
admins = "linear-admins";
|
||||
|
|
@ -75,6 +76,30 @@
|
|||
];
|
||||
};
|
||||
|
||||
release = {
|
||||
displayName = "Burrow Release";
|
||||
canonicalEmail = "release@burrow.net";
|
||||
isAdmin = false;
|
||||
forgeAuthorized = false;
|
||||
bootstrapAuthentik = false;
|
||||
roles = [
|
||||
"automation"
|
||||
"release"
|
||||
];
|
||||
};
|
||||
|
||||
namespace = {
|
||||
displayName = "Burrow Namespace";
|
||||
canonicalEmail = "namespace@burrow.net";
|
||||
isAdmin = false;
|
||||
forgeAuthorized = false;
|
||||
bootstrapAuthentik = false;
|
||||
roles = [
|
||||
"automation"
|
||||
"namespace-runners"
|
||||
];
|
||||
};
|
||||
|
||||
ui-test = {
|
||||
displayName = "Burrow UI Test";
|
||||
canonicalEmail = "ui-test@burrow.net";
|
||||
|
|
|
|||
9
crates/burrow-core/Cargo.toml
Normal file
9
crates/burrow-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "burrow-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Cross-platform Burrow protocol and application core."
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "staticlib"]
|
||||
61
crates/burrow-core/src/lib.rs
Normal file
61
crates/burrow-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#![deny(missing_debug_implementations)]
|
||||
|
||||
/// Version of the Rust core linked into platform shells.
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
/// Platform family reported by the current Rust target.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PlatformFamily {
|
||||
Android,
|
||||
Apple,
|
||||
Linux,
|
||||
Windows,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl PlatformFamily {
|
||||
pub fn current() -> Self {
|
||||
if cfg!(target_os = "android") {
|
||||
Self::Android
|
||||
} else if cfg!(target_vendor = "apple") {
|
||||
Self::Apple
|
||||
} else if cfg!(target_os = "linux") {
|
||||
Self::Linux
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Other
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Android => "android",
|
||||
Self::Apple => "apple",
|
||||
Self::Linux => "linux",
|
||||
Self::Windows => "windows",
|
||||
Self::Other => "other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable core identity for stubs and smoke tests.
|
||||
pub fn identity() -> String {
|
||||
format!(
|
||||
"burrow-core/{} ({})",
|
||||
version(),
|
||||
PlatformFamily::current().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn identity_includes_project_name() {
|
||||
assert!(identity().starts_with("burrow-core/"));
|
||||
}
|
||||
}
|
||||
13
crates/burrow-mobile-ffi/Cargo.toml
Normal file
13
crates/burrow-mobile-ffi/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "burrow-mobile-ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Mobile FFI boundary for the Burrow Rust core."
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
burrow-core = { path = "../burrow-core" }
|
||||
jni = "0.21"
|
||||
27
crates/burrow-mobile-ffi/src/lib.rs
Normal file
27
crates/burrow-mobile-ffi/src/lib.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#![deny(missing_debug_implementations)]
|
||||
|
||||
use std::ffi::c_char;
|
||||
|
||||
use jni::objects::JClass;
|
||||
use jni::sys::jstring;
|
||||
use jni::JNIEnv;
|
||||
|
||||
static CORE_VERSION: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes();
|
||||
|
||||
/// C ABI entrypoint for platform shells that do not use JNI.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn burrow_core_version() -> *const c_char {
|
||||
CORE_VERSION.as_ptr().cast()
|
||||
}
|
||||
|
||||
/// JNI entrypoint used by the Android Kotlin stub.
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_net_burrow_android_BurrowCore_version(
|
||||
env: JNIEnv<'_>,
|
||||
_class: JClass<'_>,
|
||||
) -> jstring {
|
||||
match env.new_string(burrow_core::identity()) {
|
||||
Ok(value) => value.into_raw(),
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue