mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
* Route CI LFS through Gitea * Add fresh Gitea LFS verification mode * Expose fresh LFS verification failures * Avoid duplicate Gitea LFS auth headers
387 lines
15 KiB
YAML
387 lines
15 KiB
YAML
name: iOS TestFlight
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 5 * * *' # 9 PM Pacific (UTC-8) / 10 PM PDT (UTC-7)
|
|
workflow_dispatch:
|
|
inputs:
|
|
skip_upload:
|
|
description: 'Skip TestFlight upload (build and archive only)'
|
|
required: false
|
|
default: 'false'
|
|
type: boolean
|
|
|
|
permissions:
|
|
contents: read
|
|
actions: write
|
|
|
|
env:
|
|
# Runner-specific build directory to allow parallel builds on multiple runners
|
|
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}
|
|
# Runner-specific keychain to avoid conflicts when multiple runners sign simultaneously
|
|
KEYCHAIN_NAME: ios-build-${{ github.run_id }}.keychain
|
|
|
|
jobs:
|
|
check-changes:
|
|
# Skip nightly builds when nothing has changed since the last successful
|
|
# scheduled run. Manual workflow_dispatch always builds.
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
should_build: ${{ steps.check.outputs.should_build }}
|
|
steps:
|
|
- name: Check for relevant changes since last successful run
|
|
id: check
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
if (context.eventName === 'workflow_dispatch') {
|
|
core.setOutput('should_build', 'true');
|
|
console.log('Manual trigger — building');
|
|
return;
|
|
}
|
|
|
|
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
workflow_id: 'ios_testflight.yml',
|
|
status: 'success',
|
|
event: 'schedule',
|
|
per_page: 1,
|
|
});
|
|
|
|
if (runs.length === 0) {
|
|
core.setOutput('should_build', 'true');
|
|
console.log('No previous successful scheduled run — building');
|
|
return;
|
|
}
|
|
|
|
const lastSha = runs[0].head_sha;
|
|
console.log(`Last successful scheduled run: ${lastSha}`);
|
|
console.log(`Current SHA: ${context.sha}`);
|
|
|
|
if (lastSha === context.sha) {
|
|
core.setOutput('should_build', 'false');
|
|
console.log('No changes since last successful run — skipping');
|
|
return;
|
|
}
|
|
|
|
// Paths that can affect the iOS Unity build.
|
|
const relevantPrefixes = [
|
|
'src/main/csharp/', // Unity project
|
|
'src/main/protobuf/', // Generates C# code
|
|
'src/main/resources/', // Game data & maps
|
|
'scripts/', // Build scripts
|
|
'ci/', // CI scripts & workflows
|
|
'.github/workflows/ios_testflight.yml',
|
|
'MODULE.bazel',
|
|
'MODULE.bazel.lock',
|
|
'.bazelrc',
|
|
];
|
|
|
|
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
basehead: `${lastSha}...${context.sha}`,
|
|
per_page: 1, // We only need the file list, not patches
|
|
});
|
|
|
|
// If the diff is too large, build to be safe
|
|
if (comparison.files === undefined) {
|
|
core.setOutput('should_build', 'true');
|
|
console.log('Could not retrieve file list — building to be safe');
|
|
return;
|
|
}
|
|
|
|
const relevant = comparison.files.filter(f =>
|
|
relevantPrefixes.some(p => f.filename.startsWith(p))
|
|
);
|
|
|
|
if (relevant.length > 0) {
|
|
core.setOutput('should_build', 'true');
|
|
console.log(`${relevant.length} relevant file(s) changed:`);
|
|
relevant.slice(0, 20).forEach(f => console.log(` ${f.filename}`));
|
|
if (relevant.length > 20) console.log(` ... and ${relevant.length - 20} more`);
|
|
} else {
|
|
core.setOutput('should_build', 'false');
|
|
console.log(`${comparison.files.length} file(s) changed, none in relevant paths — skipping`);
|
|
}
|
|
|
|
build-unity:
|
|
needs: check-changes
|
|
if: needs.check-changes.outputs.should_build == 'true'
|
|
runs-on: [self-hosted, macOS, testflight]
|
|
|
|
steps:
|
|
- name: Prune stale PR refs
|
|
run: |
|
|
# Self-hosted runners persist .git between runs. When a PR is updated,
|
|
# old local refs (refs/remotes/pull/*/merge) may point to commits whose
|
|
# objects were never fetched or have been pruned. Remove these stale refs
|
|
# before checkout to prevent "missing object" errors.
|
|
if [ -d ".git" ]; then
|
|
echo "Pruning stale PR refs..."
|
|
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
|
xargs -r git update-ref -d 2>/dev/null || true
|
|
fi
|
|
|
|
- name: Ensure Git LFS available for checkout
|
|
run: |
|
|
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
export PATH="${COMMON_PATHS}:${PATH}"
|
|
for path in /opt/homebrew/bin /usr/local/bin; do
|
|
if [ -d "$path" ]; then
|
|
echo "$path" >> "$GITHUB_PATH"
|
|
fi
|
|
done
|
|
if ! command -v git-lfs >/dev/null 2>&1; then
|
|
brew install git-lfs
|
|
fi
|
|
git-lfs --version
|
|
|
|
- uses: actions/checkout@v7
|
|
env:
|
|
GIT_LFS_SKIP_SMUDGE: 1
|
|
with:
|
|
persist-credentials: false
|
|
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
|
clean: false # Library/ persists between runs on self-hosted runners
|
|
fetch-depth: 0
|
|
|
|
- name: Clean stale files
|
|
run: |
|
|
git clean -ffd
|
|
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
|
|
|
- name: Fetch LFS files
|
|
env:
|
|
GITEA_LFS_TOKEN: ${{ secrets.GITEA_LFS_TOKEN }}
|
|
GITEA_LFS_USERNAME: ${{ secrets.GITEA_LFS_USERNAME }}
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: ./ci/github_actions/fetch_lfs.sh
|
|
|
|
- name: Ensure Unity version installed
|
|
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
|
|
|
- name: Ensure Bazel installed
|
|
uses: ./.github/actions/setup-bazel
|
|
|
|
- name: Sync Bazel Xcode config
|
|
run: ./scripts/sync_bazel_xcode.sh
|
|
|
|
- name: Build iOS Unity Project
|
|
id: build
|
|
run: |
|
|
./ci/github_actions/build_unity_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS"
|
|
|
|
- name: Upload Addressables to CDN
|
|
if: success() && github.event.inputs.skip_upload != 'true'
|
|
env:
|
|
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
|
run: ./ci/github_actions/upload_addressables.sh iOS
|
|
|
|
- name: Purge CDN cache for iOS addressables
|
|
if: success() && github.event.inputs.skip_upload != 'true'
|
|
env:
|
|
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
|
run: |
|
|
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
|
|
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
|
|
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
|
-H "Authorization: Bearer $DO_CDN_PAT" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"files\": [\"addressables/iOS/$UNITY_MAJOR_MINOR/*\"]}" \
|
|
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
|
|
|
- name: Archive Build Log
|
|
if: success() || failure()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: editor_ios.log
|
|
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios.log
|
|
retention-days: 3
|
|
|
|
- name: Package generated iOS project
|
|
if: success()
|
|
run: |
|
|
tar -czf "$RUNNER_TEMP/eagle0-ios-project.tar.gz" -C "$EAGLE0_BUILD_DIR" eagle0iOS
|
|
|
|
- name: Upload generated iOS project
|
|
if: success()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: eagle0-ios-project-${{ github.run_id }}
|
|
path: ${{ runner.temp }}/eagle0-ios-project.tar.gz
|
|
retention-days: 1
|
|
|
|
- name: Cleanup build directory
|
|
if: always()
|
|
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
|
|
|
archive-and-upload:
|
|
needs: build-unity
|
|
runs-on: [self-hosted, macOS, testflight]
|
|
|
|
steps:
|
|
- name: Ensure Git LFS available for checkout
|
|
run: |
|
|
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
export PATH="${COMMON_PATHS}:${PATH}"
|
|
for path in /opt/homebrew/bin /usr/local/bin; do
|
|
if [ -d "$path" ]; then
|
|
echo "$path" >> "$GITHUB_PATH"
|
|
fi
|
|
done
|
|
if ! command -v git-lfs >/dev/null 2>&1; then
|
|
brew install git-lfs
|
|
fi
|
|
git-lfs --version
|
|
|
|
- uses: actions/checkout@v7
|
|
env:
|
|
GIT_LFS_SKIP_SMUDGE: 1
|
|
with:
|
|
persist-credentials: false
|
|
lfs: false
|
|
|
|
- name: Clean build directory
|
|
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
|
|
|
- name: Download generated iOS project
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: eagle0-ios-project-${{ github.run_id }}
|
|
path: ${{ runner.temp }}
|
|
|
|
- name: Extract generated iOS project
|
|
run: |
|
|
mkdir -p "$EAGLE0_BUILD_DIR"
|
|
tar -xzf "$RUNNER_TEMP/eagle0-ios-project.tar.gz" -C "$EAGLE0_BUILD_DIR"
|
|
|
|
- name: Delete generated iOS project artifact
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
const artifactName = 'eagle0-ios-project-${{ github.run_id }}';
|
|
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
run_id: context.runId,
|
|
});
|
|
|
|
const artifact = artifacts.find(a => a.name === artifactName);
|
|
if (artifact === undefined) {
|
|
console.log(`Artifact not found: ${artifactName}`);
|
|
return;
|
|
}
|
|
|
|
await github.rest.actions.deleteArtifact({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
artifact_id: artifact.id,
|
|
});
|
|
console.log(`Deleted artifact: ${artifactName} (${artifact.id})`);
|
|
|
|
- name: Install Signing Certificate
|
|
env:
|
|
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
|
|
IOS_CERTIFICATE_PWD: ${{ secrets.IOS_CERTIFICATE_PWD }}
|
|
run: |
|
|
# Generate random keychain password
|
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
|
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV
|
|
|
|
# Decode certificate
|
|
echo "$IOS_CERTIFICATE" | base64 --decode > certificate.p12
|
|
|
|
# Delete any existing keychain from previous runs
|
|
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
|
|
|
# Create temporary keychain
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
|
security default-keychain -s "$KEYCHAIN_NAME"
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
|
security set-keychain-settings -t 3600 -u "$KEYCHAIN_NAME"
|
|
|
|
# Import certificate
|
|
security import certificate.p12 -k "$KEYCHAIN_NAME" -P "$IOS_CERTIFICATE_PWD" -T /usr/bin/codesign -T /usr/bin/security
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
|
|
|
# Add keychain to search list
|
|
security list-keychains -d user -s "$KEYCHAIN_NAME" login.keychain
|
|
|
|
rm certificate.p12
|
|
|
|
- name: Install Provisioning Profile
|
|
env:
|
|
IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
|
|
run: |
|
|
# Decode provisioning profile
|
|
echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision
|
|
|
|
# Extract UUID from provisioning profile
|
|
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" /dev/stdin <<< $(security cms -D -i profile.mobileprovision))
|
|
echo "PROFILE_UUID=$PROFILE_UUID" >> $GITHUB_ENV
|
|
|
|
# Install to standard location
|
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
|
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE_UUID.mobileprovision
|
|
|
|
rm profile.mobileprovision
|
|
echo "Installed provisioning profile: $PROFILE_UUID"
|
|
|
|
- name: Archive and Export IPA
|
|
env:
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
|
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
|
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
|
run: |
|
|
# Write API key to file for xcodebuild
|
|
API_KEY_PATH=$(mktemp)
|
|
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
|
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
|
|
|
chmod +x ./ci/github_actions/archive_ios.sh
|
|
./ci/github_actions/archive_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS" "$EAGLE0_BUILD_DIR/archive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
|
|
|
rm -f "$API_KEY_PATH"
|
|
|
|
- name: Upload to TestFlight
|
|
if: ${{ github.event.inputs.skip_upload != 'true' }}
|
|
env:
|
|
# App Store Connect API Key (replaces deprecated altool with Apple ID)
|
|
# Create at: https://appstoreconnect.apple.com/access/api
|
|
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
|
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
|
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
# Write API key to file (xcodebuild needs a file path)
|
|
API_KEY_PATH=$(mktemp)
|
|
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
|
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
|
|
|
chmod +x ./ci/github_actions/upload_testflight.sh
|
|
./ci/github_actions/upload_testflight.sh "$EAGLE0_BUILD_DIR/archive/eagle0.xcarchive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
|
|
|
# Cleanup
|
|
rm -f "$API_KEY_PATH"
|
|
|
|
- name: Upload IPA artifact
|
|
# Only keep artifact if we skipped TestFlight upload (for debugging)
|
|
if: success() && github.event.inputs.skip_upload == 'true'
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: eagle0-ios-${{ github.run_id }}
|
|
path: ${{ env.EAGLE0_BUILD_DIR }}/archive/eagle0.ipa
|
|
retention-days: 1
|
|
|
|
- name: Cleanup Keychain
|
|
if: always()
|
|
run: |
|
|
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
|
- name: Cleanup build directory
|
|
if: always()
|
|
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|