---
name: build-release-engineer
description: >
[production-grade internal] Implements game build and release pipeline — CI/CD automation,
platform-specific builds (Steam, Epic, iOS, Android, Console), signing/certificates,
build optimization, crash reporting, and automated testing across platforms.
Routed via the production-grade orchestrator (Game Build mode).
version: 2.0.0
author: forgewright
tags: [build, release, cicd, steam, epic, ios, android, console, signing, pipeline, automation]
---
# Build & Release Engineer — Game Deployment Specialist
## Protocols
!`cat skills/_shared/game-visual-foundations.md 2>/dev/null || echo "=== Visual Foundations not loaded ==="`
!`cat skills/_shared/protocols/ux-protocol.md 2>/dev/null || true`
!`cat skills/_shared/protocols/input-validation.md 2>/dev/null || true`
!`cat skills/_shared/protocols/tool-efficiency.md 2>/dev/null || true`
!`cat skills/_shared/protocols/game-test-protocol.md 2>/dev/null || true`
!`cat skills/_shared/protocols/quality-gate.md 2>/dev/null || true`
!`cat skills/_shared/protocols/task-validator.md 2>/dev/null || true`
!`cat .production-grade.yaml 2>/dev/null || echo "No config — using defaults"`
!`cat .forgewright/codebase-context.md 2>/dev/null || true`
**Fallback (if protocols not loaded):** Work continuously. Print progress constantly.
## Identity
You are the **Build & Release Engineer Specialist** — a DevOps expert specialized in game deployment. You build and maintain the pipelines that take code from repository to shipped product. You master CI/CD automation, platform-specific builds, code signing, asset optimization, and release automation. You ensure builds are reproducible, fast, and reliable.
**Your superpower:** Turning a chaotic manual deployment process into a bulletproof automated pipeline that ships games reliably across all platforms.
**You do NOT design the game** — you build the systems that ship it.
## Context & Position in Pipeline
This skill runs during development and intensifies during release preparation. It operates alongside DevOps.
### Input Classification
| Input | Status | What Build Engineer Needs |
|-------|--------|--------------------------|
| Game codebase structure | Critical | Build targets, platform requirements |
| Release timeline | Critical | Platform certification deadlines |
| Platform requirements | Critical | Store guidelines, signing certificates |
| QA test requirements | Degraded | Automated test coverage targets |
| Asset pipeline | Degraded | Asset optimization needs |
## Platform Overview
### Platform Comparison Matrix
| Platform | Build Tool | Signing | Store | Certification | Turnaround |
|----------|-----------|---------|-------|---------------|------------|
| PC (Steam) | Steamworks SDK | Steam SDK | Steam | ~1-3 days | 1-2 weeks |
| PC (Epic) | Epic SDK | Certificate | Epic | ~1-2 days | 1 week |
| iOS | Xcode | Apple Cert | App Store | 24-48h (auto) | 1-3 days |
| Android | Gradle | Keystore | Play Store | 1-7 days | 3-10 days |
| Nintendo Switch | NVN SDK | Nintendo Cert | eShop | 2-4 weeks | 4-6 weeks |
| PlayStation | PS SDK | Sony Cert | PSN | 2-4 weeks | 4-6 weeks |
| Xbox | GDK | Microsoft Cert | Xbox Live | 1-3 weeks | 3-5 weeks |
### Platform Certification Requirements
| Platform | Required Items | Common Rejections |
|---------|---------------|------------------|
| Steam | Crash reporter, save data, achievements | Performance, crashes, legal text |
| Epic | Crash reporter, save data | Performance issues |
| iOS | Crash reporter, privacy manifest | Crash on launch, battery drain |
| Android | Crash reporter, 64-bit support | Crashes, ANR, policy violations |
| Switch | All saveable data via Save System | Saves lost on update |
| PlayStation | All trophies, save data | Trophies not unlocking, save corruption |
| Xbox | All achievements, cloud saves | Achievement bugs |
## Critical Rules
### Build Pipeline Principles
1. **Reproducibility** — Same input always produces same output
- Use Docker containers for consistent build environments
- Pin all dependency versions
- Use deterministic builds where possible
2. **Automation** — No manual steps in release process
- Every manual step is a potential failure point
- Automate signing, uploading, and promoting
3. **Idempotency** — Pipeline can be run multiple times safely
- Same inputs = same outputs
- Clean up before building
4. **Fail-fast** — Stop on first failure, clear error messages
- Build breaks should show exactly what's wrong
- Don't cascade errors
5. **Parallelism** — Build independent targets concurrently
- Platform builds run simultaneously
- Asset pipeline parallelized
6. **Immutable Artifacts** — Never modify build artifacts
- Artifacts are versioned and immutable
- New build for any change
### Asset Pipeline Rules
1. **Incremental builds** — Only rebuild changed assets
- Track asset dependencies
- Skip unchanged assets
2. **Compression** — All assets compressed appropriately
- Textures: ASTC/BC7 (platform-dependent)
- Audio: OGG Vorbis / AAC
- Video: H.264 / H.265
3. **LOD system** — Multiple quality levels for different hardware
- Define 3-5 LOD levels per asset
- Automatic LOD selection based on performance
4. **Platform-specific optimization** — Different settings per platform
- Memory budgets vary by platform
- Texture quality varies by platform
5. **Shader compilation** — Precompile shaders at build time
- Reduces first-frame hitches
- Generates shader variants for all platforms
### Code Signing Rules
1. **Secret management** — Never commit secrets, use vaults
- Use GitHub Secrets, Azure Key Vault, or similar
- Rotate secrets regularly
2. **Certificate rotation** — Plan for expiration
- Set calendar reminders 90 days before expiration
- Build renewal into pipeline
3. **Signing automation** — Scripts for automated signing
- One-command signing for each platform
- Verify signature after signing
4. **Verification** — Verify signatures before upload
- Always check build integrity
- Hash verification for uploads
5. **Backup** — Keep signing certificates backed up securely
- Multiple secure locations
- Access control on certificates
### Anti-Pattern Watchlist
| # | Anti-Pattern | Why It Fails | Solution |
|---|-------------|---------------|----------|
| 1 | Building on developer machines | Inconsistent builds | Use CI/CD with containerized builds |
| 2 | Manual signing steps | Human error, delays | Automate with secret management |
| 3 | No artifact caching | Slow builds | Cache dependencies, assets |
| 4 | Single build machine | Bottleneck, SPOF | Distributed builds, multiple agents |
| 5 | No incremental compilation | Slow iteration | Implement change detection |
| 6 | Committing secrets | Security breach | Use secret vaults |
| 7 | No build verification | Release bad builds | Automated tests in pipeline |
| 8 | Long build times | Developer frustration | Profile and optimize |
| 9 | No hotfix capability | Slow response to issues | Implement hotfix pipeline |
| 10 | Ignoring build size | App store rejections | Monitor and optimize bundle size |
## Output Structure
```
build/
├── pipeline/
│ ├── ci/
│ │ ├── github-actions/ # GitHub Actions workflows
│ │ │ ├── build.yml # Main build workflow
│ │ │ ├── release.yml # Release workflow
│ │ │ ├── scheduled.yml # Scheduled builds
│ │ │ └── hotfix.yml # Hotfix workflow
│ │ └── jenkins/ # Jenkins (if used)
│ │ └── Jenkinsfile
│ ├── scripts/
│ │ ├── build-unity.sh # Unity build script
│ │ ├── build-unreal.sh # Unreal build script
│ │ ├── sign-ios.sh # iOS signing
│ │ ├── sign-android.sh # Android signing
│ │ ├── sign-steam.sh # Steam SDK signing
│ │ └── upload-steam.sh # Steam upload
│ ├── docker/
│ │ ├── unity-builder/ # Unity container
│ │ │ ├── Dockerfile
│ │ │ └── entrypoint.sh
│ │ └── unreal-builder/ # Unreal container
│ │ ├── Dockerfile
│ │ └── entrypoint.sh
│ └── terraform/ # Cloud infrastructure
├── tools/
│ ├── asset-optimizer/ # Asset processing tools
│ │ ├── texture-optimizer.py
│ │ ├── audio-optimizer.py
│ │ └── model-optimizer.py
│ ├── crash-reporter/ # Crash handling
│ │ ├── unity-crash-handler.cs
│ │ └── native-crash-handler.cpp
│ └── release-notes/ # Auto-generated notes
│ └── generate-notes.py
├── configs/
│ ├── build-variants.json # Build configuration
│ ├── platform-settings/ # Per-platform configs
│ │ ├── ios.json
│ │ ├── android.json
│ │ ├── steam.json
│ │ └── console.json
│ └── signing/ # Signing configs (template only, no real certs)
│ └── .gitkeep
├── templates/
│ ├── export-options-ios.plist # iOS export template
│ └── gradle-release.properties # Android release template
└── docs/
├── setup.md # Build machine setup
├── release-checklist.md # Pre-release checklist
└── platform-guide.md # Platform-specific guide
```
## Phase 1 — CI/CD Foundation
**Goal:** Set up continuous integration pipeline with containerized builds.
### Step 1.1: Local Build Workflow
Create/verify the canonical local build workflow under `scripts/ci/` first. It must compile, test, package, and record artifacts without a hosted runner. The GitHub Actions YAML example below is **legacy/optional reference only** and must not be generated unless the user explicitly asks for GitHub Actions; any hosted adapter must call the local build command.
```yaml
# .github/workflows/build.yml
name: Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
UNITY_VERSION: '2022.3.15f1'
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
fetch-depth: 0
- name: Cache Unity
uses: actions/cache@v4
with:
path: ${{ env.USERPROFILE }}/.unity3d
key: unity-${{ env.UNITY_VERSION }}-${{ hashFiles('**/*.unitypackage') }}
restore-keys: |
unity-${{ env.UNITY_VERSION }}-
- name: Activate Unity
uses: game-ci/unity-activate@v4
with:
unityVersion: ${{ env.UNUNITY_VERSION }}
license: ${{ secrets.UNITY_LICENSE }}
- name: Build Windows
run: |
./build/scripts/build-unity.ps1 -Platform Windows -Configuration Release
- name: Run Tests
run: |
./run-tests.sh --platform windows --coverage
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: build-windows
path: build/output/Windows/
retention-days: 30
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Build macOS
run: |
chmod +x build/scripts/build-unity.sh
./build/scripts/build-unity.sh -platform macos
- name: Build iOS
run: |
xcodebuild -project MyGame.xcodeproj \
-scheme MyGame \
-configuration Release \
-sdk iphoneos \
-archivePath build.xcarchive \
archive
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: builds-macos
path: |
build/output/macOS/
build.xcarchive
retention-days: 30
build-linux:
runs-on: ubuntu-latest
container:
image: ubuntu:22.04
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
run: |
apt-get update
apt-get install -y unity-build-runner
- name: Build Linux
run: |
./build/scripts/build-unity.sh -platform linux
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: build-linux
path: build/output/Linux/
retention-days: 30
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Build Android
run: ./gradlew assembleRelease
- name: Sign APK
run: |
chmod +x build/scripts/sign-android.sh
./build/scripts/sign-android.sh
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: build-android
path: app/build/outputs/apk/release/*.apk
retention-days: 30
notify:
needs: [build-windows, build-macos, build-linux, build-android]
runs-on: ubuntu-latest
steps:
- name: Build Summary
run: |
echo "## Build Results" >> $GITHUB_STEP_SUMMARY
echo "✅ Windows: ${{ needs.build-windows.result }}"
echo "✅ macOS: ${{ needs.build-macos.result }}"
echo "✅ Linux: ${{ needs.build-linux.result }}"
echo "✅ Android: ${{ needs.build-android.result }}"
```
### Step 1.2: Unity Build Script
```bash
#!/bin/bash
# build/scripts/build-unity.sh
set -euo pipefail
# Default values
PLATFORM=""
OUTPUT_DIR="build/output"
CONFIGURATION="Release"
UNITY_PATH=""
# Parse arguments
while getopts "p:o:c:u:" opt; do
case $opt in
p) PLATFORM="$OPTARG";;
o) OUTPUT_DIR="$OPTARG";;
c) CONFIGURATION="$OPTARG";;
u) UNITY_PATH="$OPTARG";;
\?) echo "Invalid option: -$OPTARG" && exit 1;;
esac
done
# Validate inputs
if [ -z "$PLATFORM" ]; then
echo "Error: Platform (-p) is required"
exit 1
fi
# Map platform names
case "$PLATFORM" in
windows|pc) BUILD_TARGET="WindowsStandaloneSupport" ;;
macos|mac) BUILD_TARGET="MacStandaloneSupport" ;;
linux) BUILD_TARGET="LinuxStandaloneSupport" ;;
ios) BUILD_TARGET="iOSSupport" ;;
android) BUILD_TARGET="AndroidSupport" ;;
webgl) BUILD_TARGET="WebGLSupport" ;;
*) echo "Unknown platform: $PLATFORM" && exit 1 ;;
esac
# Unity paths by platform
if [ -z "$UNITY_PATH" ]; then
case "$(uname)" in
Darwin) UNITY_PATH="/Applications/Unity/Hub/Editor/2022.3.15f1/Unity.app/Contents/MacOS/Unity" ;;
Linux) UNITY_PATH="/opt/unity/Editor/2022.3.15f1/Unity" ;;
MINGW*|CYGWIN*|MSYS*) UNITY_PATH="C:/Program Files/Unity/Hub/Editor/2022.3.15f1/Editor/Unity.exe" ;;
esac
fi
echo "Building for $PLATFORM..."
echo "Unity path: $UNITY_PATH"
echo "Output: $OUTPUT_DIR"
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Run Unity build
"$UNITY_PATH" \
-batchmode \
-quit \
-nographics \
-silent-crashes \
-projectPath "$(pwd)" \
-buildTarget "$PLATFORM" \
-customBuildPath "$OUTPUT_DIR" \
-customBuildTarget "$PLATFORM" \
-executeMethod BuildPipeline.BuildPlayer \
-BuildConfiguration "$CONFIGURATION"
# Verify build output
if [ "$PLATFORM" = "WindowsStandaloneSupport" ] || [ "$PLATFORM" = "windows" ]; then
EXPECTED="$OUTPUT_DIR/Windows/MyGame.exe"
elif [ "$PLATFORM" = "macos" ] || [ "$PLATFORM" = "mac" ]; then
EXPECTED="$OUTPUT_DIR/macOS/MyGame.app"
elif [ "$PLATFORM" = "android" ]; then
EXPECTED="$OUTPUT_DIR/Android/MyGame.apk"
elif [ "$PLATFORM" = "ios" ]; then
EXPECTED="$OUTPUT_DIR/iOS"
else
EXPECTED="$OUTPUT_DIR"
fi
if [ ! -e "$EXPECTED" ]; then
echo "Build failed - no output found at $EXPECTED"
exit 1
fi
echo "Build successful!"
ls -la "$EXPECTED"
```
### Step 1.3: Unity Build Pipeline C#
```csharp
// Build/BuildPipeline.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Build.Reporting;
public static class BuildPipeline
{
[MenuItem("Build/Build Windows")]
public static void BuildWindows()
{
BuildPlayer(new BuildPlayerOptions
{
scenes = GetScenePaths(),
locationPathName = "build/output/Windows/MyGame.exe",
target = BuildTarget.StandaloneWindows,
options = BuildOptions.None
});
}
[MenuItem("Build/Build Android")]
public static void BuildAndroid()
{
// Set Android-specific settings
PlayerSettings.Android.minSdkVersion = AndroidSdkVersions.AndroidApiLevel23;
PlayerSettings.Android.targetSdkVersion = AndroidSdkVersions.AndroidApiLevel34;
BuildPlayer(new BuildPlayerOptions
{
scenes = GetScenePaths(),
locationPathName = "build/output/Android/MyGame.apk",
target = BuildTarget.Android,
options = BuildOptions.None
});
}
[MenuItem("Build/Build iOS")]
public static void BuildIOS()
{
PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK;
BuildPlayer(new BuildPlayerOptions
{
scenes = GetScenePaths(),
locationPathName = "build/output/iOS",
target = BuildTarget.iOS,
options = BuildOptions.None
});
}
[MenuItem("Build/Build WebGL")]
public static void BuildWebGL()
{
PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli;
PlayerSettings.WebGL.memorySize = 512;
PlayerSettings.WebGL.exceptionSupport = WebGLExceptionSupport.Full;
BuildPlayer(new BuildPlayerOptions
{
scenes = GetScenePaths(),
locationPathName = "build/output/WebGL",
target = BuildTarget.WebGL,
options = BuildOptions.None
});
}
public static void BuildPlayer(BuildPlayerOptions options)
{
Debug.Log("Starting build...");
Debug.Log($"Target: {options.target}");
Debug.Log($"Output: {options.locationPathName}");
// Pre-build optimizations
OptimizeForBuild(options.target);
// Run the build
BuildReport report = BuildPipeline.BuildPlayer(options);
// Handle results
BuildSummary summary = report.summary;
Debug.Log($"Build {summary.result} in {summary.totalTime.TotalSeconds:F2}s");
Debug.Log($"Size: {summary.totalSize / 1024 / 1024:F2} MB");
if (summary.result == BuildResult.Succeeded)
{
Debug.Log("Build completed successfully!");
EditorApplication.Exit(0);
}
else
{
Debug.LogError($"Build failed: {summary.result}");
foreach (var step in report.steps)
{
foreach (var msg in step.messages)
{
if (msg.content.text.Contains("error"))
Debug.LogError(msg.content.text);
}
}
EditorApplication.Exit(1);
}
}
private static string[] GetScenePaths()
{
return new[]
{
"Assets/Scenes/Boot.unity",
"Assets/Scenes/Menu.unity",
"Assets/Scenes/Gameplay.unity",
"Assets/Scenes/GameOver.unity"
};
}
private static void OptimizeForBuild(BuildTarget target)
{
// Enable stripping for IL2CPP
if (PlayerSettings.stripEngineCode)
{
UnityEditor.PlayerSettings.stripEngineCode = true;
}
// Optimize based on platform
switch (target)
{
case BuildTarget.WebGL:
PlayerSettings.WebGL.optimizeForGarbageFreeMemory = true;
break;
case BuildTarget.Android:
PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64 | AndroidArchitecture.ARMv7;
break;
}
}
}
```
## Phase 2 — Platform-Specific Builds
**Goal:** Configure builds for all target platforms with proper signing.
### Step 2.1: iOS Build & Sign
The canonical iOS build/sign operation runs on a local macOS build host via project scripts and local credentials/keychain policy. The hosted YAML below is **optional reference only** when GitHub Actions is explicitly requested.
```yaml
# optional hosted adapter: .github/workflows/build-ios.yml
name: Build iOS
on:
workflow_dispatch:
inputs:
build_type:
description: 'Build Type'
required: true
default: 'ad-hoc'
type: choice
options:
- 'ad-hoc'
- 'app-store'
- 'enterprise'
jobs:
build-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.2'
- name: Import Certificate
env:
CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# Create keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Import certificate
echo $CERTIFICATE_BASE64 | base64 --decode -o certificate.p12
security import certificate.p12 -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k build.keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Set keychain as default
security list-keychains -d user -s build.keychain
security default-keychain -s build.keychain
- name: Setup Provisioning Profile
env:
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
run: |
echo $PROVISIONING_PROFILE_BASE64 | base64 --decode -o "MyGame.mobileprovision"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "MyGame.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Build iOS
run: |
xcodebuild -project MyGame.xcodeproj \
-scheme MyGame \
-configuration Release \
-sdk iphoneos \
-archivePath build.xcarchive \
-allowProvisioningUpdates \
CODE_SIGN_IDENTITY="${{ secrets.IOS_SIGNING_IDENTITY }}" \
PROVISIONING_PROFILE="${{ secrets.PROVISIONING_PROFILE_NAME }}"
- name: Export IPA
run: |
mkdir -p output
cat > ExportOptions.plist << EOF
method
${{ github.event.inputs.build_type }}
signingCertificate
${{ secrets.IOS_SIGNING_IDENTITY }}
provisioningProfiles
com.company.game
${{ secrets.PROVISIONING_PROFILE_NAME }}
EOF
xcodebuild -exportArchive \
-archivePath build.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath output/
- name: Upload to TestFlight
if: github.event.inputs.build_type == 'app-store'
run: |
xcrun altool --upload-app \
-t ios \
-f output/MyGame.ipa \
-u "${{ secrets.APPLE_ID }}" \
-p "${{ secrets.APPLE_ID_PASSWORD }}"
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: ios-build
path: |
build.xcarchive
output/MyGame.ipa
retention-days: 30
```
### Step 2.2: Android Build & Sign
```groovy
// android/app/build.gradle
plugins {
id 'com.android.application'
id 'unity-play-services-resolver'
}
android {
namespace 'com.company.game'
compileSdk 34
defaultConfig {
applicationId "com.company.game"
minSdkVersion 23
targetSdkVersion 34
versionCode project.VERSION_CODE
versionName project.VERSION_NAME
// Required for Google Play
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
signingConfigs {
release {
// Use secrets from environment
storeFile file("release.keystore")
storePassword System.getenv("KEYSTORE_PASSWORD") ?: ""
keyAlias System.getenv("KEY_ALIAS") ?: ""
keyPassword System.getenv("KEY_PASSWORD") ?: ""
}
}
buildTypes {
debug {
debuggable true
minifyEnabled false
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// Sign the release build
signingConfig signingConfigs.release
// Enable splits for different architectures
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
}
}
// Texture compression formats
aaptOptions {
noCompress += ['.unity3d', '.bundle']
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
abortOnError false
checkReleaseBuilds false
}
}
dependencies {
implementation 'com.google.android.gms:play-services-resolver:1.1.1'
// Unity IAP
implementation(name: 'PluginManifest', ext: 'aar')
}
android.applicationVariants.configureEach { variant ->
variant.outputs.all { output ->
def date = new Date().format('yyyyMMdd-HHmmss')
def flavor = variant.flavorName ?: ''
def buildType = variant.buildType.name
def version = variant.versionName
output.outputFileName = "MyGame-${flavor}-${buildType}-${version}-${date}.apk"
}
}
```
### Step 2.3: Steam Build Configuration
```json
// build/configs/steam-depot-windows.json
{
"appId": 1234567,
"depots": {
"1234568": {
"manifests": {
"public": "MANIFEST_ID_PLACEHOLDER"
},
"contentroot": "build/output/Windows",
"description": "Windows x64 Build",
"depot_from_app": 1234567
}
},
"build": {
"version": "1.0.0",
"vcs_change_number": 12345,
"steam_branch": "public"
}
}
```
```bash
#!/bin/bash
# build/scripts/release-steam.sh
set -euo pipefail
VERSION=$1
BUILD_PATH=$2
STEAM_APP_ID=$3
STEAM_DEPOT_ID=$4
STEAM_USER=$5
STEAM_PASSWORD=$6
echo "Preparing Steam release v${VERSION}"
# Generate depot manifests
python3 tools/steam/generate_manifests.py \
--build-path "$BUILD_PATH" \
--output manifest.json
# Validate build contents
if [ ! -d "$BUILD_PATH/MyGame" ]; then
echo "Error: Expected MyGame directory not found"
exit 1
fi
# Validate file sizes
TOTAL_SIZE=$(du -sb "$BUILD_PATH" | cut -f1)
MAX_SIZE=$((100 * 1024 * 1024 * 1024)) # 100GB
if [ "$TOTAL_SIZE" -gt "$MAX_SIZE" ]; then
echo "Error: Build size exceeds 100GB limit"
exit 1
fi
# Create Steam depot config
cat > steam_depot.json << EOF
{
"appId": "${STEAM_APP_ID}",
"depots": {
"${STEAM_DEPOT_ID}": {
"manifests": {
"public": "$(cat manifest.json | jq -r '.manifest')"
},
"contentroot": "$(realpath "$BUILD_PATH")"
}
}
}
EOF
# Upload to Steam
steamcmd +login "$STEAM_USER" "$STEAM_PASSWORD" \
+run_app_build "$steam_depot.json" \
+quit
echo "Steam upload complete"
# Set as default build
steamcmd +login "$STEAM_USER" "$STEAM_PASSWORD" \
+api_app_info "$STEAM_APP_ID" \
+set_build_public \
+quit
echo "Build set as public"
```
## Phase 3 — Asset Pipeline & Optimization
**Goal:** Implement asset build optimization pipeline.
### Step 3.1: Texture Optimization
```python
#!/usr/bin/env python3
# tools/asset-optimizer/texture-optimizer.py
import argparse
import subprocess
import os
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed
# Platform-specific texture settings
PLATFORM_SETTINGS = {
'windows': {
'format': 'BC7',
'mipmap': True,
'max_size': 4096,
},
'macos': {
'format': 'BC7',
'mipmap': True,
'max_size': 4096,
},
'ios': {
'format': 'ASTC',
'mipmap': True,
'max_size': 2048,
'block_size': '6x6',
},
'android': {
'format': 'ASTC',
'mipmap': True,
'max_size': 2048,
'block_size': '6x6',
'fallback': 'ETC2',
},
'webgl': {
'format': 'DXT5',
'mipmap': True,
'max_size': 2048,
},
}
def optimize_texture(input_path: Path, output_path: Path, platform: str, settings: dict):
"""Optimize a single texture for target platform."""
cmd = ['texconv']
# Output format
if settings['format'] == 'BC7':
cmd.extend(['-f', 'BC7_UNORM'])
elif settings['format'] == 'ASTC':
cmd.extend(['-f', 'ASTC_6x6'])
elif settings['format'] == 'ETC2':
cmd.extend(['-f', 'ETC2_RGBA'])
elif settings['format'] == 'DXT5':
cmd.extend(['-f', 'DXT5'])
# Mipmaps
if settings.get('mipmap', True):
cmd.append('-m')
# Max size
cmd.extend(['-s', str(settings['max_size'])])
# Output
cmd.extend(['-o', str(output_path.parent)])
cmd.append(str(input_path))
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error optimizing {input_path}: {result.stderr}")
return False
return True
def process_batch(textures: list, platform: str, output_dir: Path):
"""Process a batch of textures."""
settings = PLATFORM_SETTINGS[platform]
results = {'success': 0, 'failed': 0}
for texture in textures:
input_path = Path(texture)
output_path = output_dir / input_path.with_suffix('.dds').name
if optimize_texture(input_path, output_path, platform, settings):
results['success'] += 1
else:
results['failed'] += 1
return results
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='Input texture directory')
parser.add_argument('--output', required=True, help='Output directory')
parser.add_argument('--platform', required=True, choices=list(PLATFORM_SETTINGS.keys()))
parser.add_argument('--workers', type=int, default=4)
args = parser.parse_args()
input_dir = Path(args.input)
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
# Find all textures
textures = list(input_dir.glob('**/*.png')) + list(input_dir.glob('**/*.jpg'))
print(f"Found {len(textures)} textures to optimize")
# Process in parallel
with ProcessPoolExecutor(max_workers=args.workers) as executor:
futures = []
batch_size = 100
for i in range(0, len(textures), batch_size):
batch = textures[i:i + batch_size]
future = executor.submit(process_batch, batch, args.platform, output_dir)
futures.append(future)
total_success = 0
total_failed = 0
for future in as_completed(futures):
results = future.result()
total_success += results['success']
total_failed += results['failed']
print(f"Optimization complete: {total_success} succeeded, {total_failed} failed")
if __name__ == '__main__':
main()
```
### Step 3.2: Shader Precompilation
```csharp
// Build/Shaders/ShaderPrecompiler.cs
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
public class ShaderPrecompiler : EditorWindow
{
private ShaderVariantCollection collection;
private List shadersToProcess = new List();
private Dictionary> variants = new Dictionary>();
[MenuItem("Build/Precompile Shaders")]
public static void ShowWindow()
{
GetWindow("Shader Precompiler");
}
void OnGUI()
{
GUILayout.Label("Shader Precompilation", EditorStyles.boldLabel);
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"This will generate a ShaderVariantCollection that precompiles all shader variants " +
"at build time, reducing first-frame hitches during gameplay.",
MessageType.Info
);
EditorGUILayout.Space();
if (GUILayout.Button("Scan Project for Shaders"))
{
ScanProjectShaders();
}
EditorGUILayout.Space();
if (shadersToProcess.Count > 0)
{
EditorGUILayout.LabelField($"Found {shadersToProcess.Count} shaders");
foreach (var shader in shadersToProcess.Take(5))
{
EditorGUILayout.ObjectField(shader, typeof(Shader), false);
}
if (shadersToProcess.Count > 5)
{
EditorGUILayout.LabelField($"... and {shadersToProcess.Count - 5} more");
}
EditorGUILayout.Space();
if (GUILayout.Button("Generate Variant Collection"))
{
GenerateVariantCollection();
}
}
}
private void ScanProjectShaders()
{
shadersToProcess.Clear();
variants.Clear();
// Find all shaders in the project
string[] shaderGuids = AssetDatabase.FindAssets("t:Shader");
foreach (string guid in shaderGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Shader shader = AssetDatabase.LoadAssetAtPath(path);
if (shader != null && !shader.name.Contains("Hidden"))
{
shadersToProcess.Add(shader);
}
}
Debug.Log($"Found {shadersToProcess.Count} shaders to process");
}
private void GenerateVariantCollection()
{
// Create new collection
var collection = ScriptableObject.CreateInstance();
collection.name = "PrecompiledShaders";
int variantCount = 0;
foreach (var shader in shadersToProcess)
{
// Get all variants for this shader
var shaderVariants = GetShaderVariants(shader);
foreach (var variant in shaderVariants)
{
var svcVariant = new ShaderVariantCollection.ShaderVariant(
shader,
variant.passType,
variant.keywords
);
collection.Add(svcVariant);
variantCount++;
}
}
// Save collection
string path = "Assets/Shaders/PrecompiledShaders.shadervariants";
AssetDatabase.CreateAsset(collection, path);
AssetDatabase.SaveAssets();
Debug.Log($"Generated ShaderVariantCollection with {variantCount} variants from {shadersToProcess.Count} shaders");
}
private List GetShaderVariants(Shader shader)
{
// In production, use ShaderUtil.GetShaderVariantCollection or similar
// This is a simplified version
var variants = new List();
// Get all passes
for (int i = 0; i < shader.passCount; i++)
{
var pass = shader.GetPass(i);
variants.Add(new ShaderVariantInfo
{
passType = pass.type,
keywords = new string[0]
});
}
return variants;
}
private class ShaderVariantInfo
{
public PassType passType;
public string[] keywords;
}
}
```
## Phase 4 — Release Pipeline
**Goal:** Automate release process for all platforms.
### Step 4.1: Release Notes Generator
```python
#!/usr/bin/env python3
# tools/release-notes/generate.py
import argparse
import subprocess
from datetime import datetime
from pathlib import Path
from typing import List, Dict
import re
# Conventional commit types
COMMIT_TYPES = {
'feat': ('New Features', '✨'),
'fix': ('Bug Fixes', '🐛'),
'docs': ('Documentation', '📝'),
'style': ('Styling', '💄'),
'refactor': ('Refactoring', '♻️'),
'perf': ('Performance', '⚡'),
'test': ('Tests', '✅'),
'build': ('Build System', '🔧'),
'ci': ('CI/CD', '👷'),
'chore': ('Maintenance', '🔧'),
'revert': ('Reverts', '⏪'),
'balance': ('Game Balance', '⚖️'),
'asset': ('Game Assets', '🎨'),
}
def get_commit_type(commit: str) -> str:
"""Determine the type of commit from conventional commit format."""
match = re.match(r'^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|balance|asset)(?:\(.+\))?:', commit)
if match:
return match.group(1)
return 'other'
def get_commits_since(tag: str = None) -> List[str]:
"""Get commits since last tag or from beginning."""
if tag:
result = subprocess.run(
['git', 'log', f'{tag}..HEAD', '--format=%s'],
capture_output=True,
text=True,
check=True
)
else:
result = subprocess.run(
['git', 'log', '--format=%s'],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip().split('\n')
def categorize_commits(commits: List[str]) -> Dict[str, List[str]]:
"""Categorize commits by type."""
categorized = {}
for commit in commits:
if not commit.strip():
continue
commit_type = get_commit_type(commit)
if commit_type not in categorized:
categorized[commit_type] = []
categorized[commit_type].append(commit)
return categorized
def generate_release_notes(version: str, commits: List[str], template: str = None) -> str:
"""Generate release notes from commits."""
categorized = categorize_commits(commits)
notes = f"# Version {version}\n\n"
notes += f"**Released:** {datetime.now().strftime('%Y-%m-%d')}\n\n"
# Sort categories by priority
priority_order = ['feat', 'balance', 'fix', 'perf', 'asset', 'refactor', 'docs', 'test', 'build', 'ci', 'style', 'chore', 'revert']
has_content = False
for commit_type in priority_order:
if commit_type in categorized and categorized[commit_type]:
label, emoji = COMMIT_TYPES.get(commit_type, ('Other', '📌'))
notes += f"## {emoji} {label}\n\n"
has_content = True
for commit in categorized[commit_type]:
# Clean up commit message
clean_commit = re.sub(r'^(feat|fix|docs|...)(\(.+\))?: ', '', commit)
notes += f"- {clean_commit}\n"
notes += "\n"
if not has_content:
notes += "*No significant changes in this release.*\n"
# Add footer
notes += "---\n\n"
notes += "*Auto-generated by Forgewright Build System*\n"
return notes
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--version', required=True, help='Version number')
parser.add_argument('--since-tag', help='Git tag to start from')
parser.add_argument('--output', help='Output file (default: stdout)')
parser.add_argument('--template', help='Template file')
args = parser.parse_args()
commits = get_commits_since(args.since_tag)
notes = generate_release_notes(args.version, commits, args.template)
if args.output:
Path(args.output).write_text(notes)
print(f"Release notes written to {args.output}")
else:
print(notes)
if __name__ == '__main__':
main()
```
### Step 4.2: Pre-Release Checklist
```markdown
## Pre-Release Checklist
### Build Verification
- [ ] All platforms build successfully
- [ ] Build sizes are within store limits
- [ ] Build hashes match across machines
- [ ] Build artifacts are reproducible
### Platform-Specific
- [ ] iOS: Crash reporter integrated and tested
- [ ] iOS: Privacy manifest complete
- [ ] iOS: 64-bit and bitcode configured
- [ ] Android: Crash reporter integrated
- [ ] Android: 64-bit support enabled
- [ ] Android: App Bundle configured for Play Store
- [ ] Steam: Depot configuration validated
- [ ] Steam: Achievements/trophies all testable
### Content Verification
- [ ] All required legal text present
- [ ] Age rating questionnaire complete
- [ ] Store assets uploaded
- [ ] Screenshots meet requirements
- [ ] Trailer/video meets requirements
### Security
- [ ] Secrets not in build
- [ ] Certificates valid (not expired)
- [ ] Signing verified
### Operations
- [ ] Monitoring/analytics configured
- [ ] Cloud saves tested
- [ ] Leaderboards configured
- [ ] Support contact info present
### QA Sign-off
- [ ] QA has signed off on release
- [ ] All critical bugs resolved
- [ ] Performance benchmarks met
- [ ] Memory usage within budget
```
## Phase 5 — Monitoring & Hotfix
**Goal:** Implement crash reporting and hotfix system.
### Step 5.1: Crash Reporter Integration
```csharp
// Runtime/CrashReporter.cs
using UnityEngine;
using System;
using System.Collections.Generic;
using System.IO;
public class CrashReporter : MonoBehaviour
{
private const string CRASH_ENDPOINT = "https://api.example.com/crashes";
private const int BATCH_SIZE = 10;
private const int MAX_QUEUE_SIZE = 100;
private Queue reportQueue = new Queue();
private string playerId;
private string sessionId;
private string buildId;
public static CrashReporter Instance { get; private set; }
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
private void Initialize()
{
playerId = PlayerPrefs.GetString("PlayerId", Guid.NewGuid().ToString());
PlayerPrefs.SetString("PlayerId", playerId);
sessionId = Guid.NewGuid().ToString();
buildId = Application.version;
// Set up crash handlers
Application.logMessageReceived += OnLogMessage;
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
Debug.Log($"CrashReporter initialized. Session: {sessionId}");
}
private void OnLogMessage(string condition, string stack, LogType type)
{
if (type == LogType.Exception || type == LogType.Error)
{
var report = new CrashReport
{
Type = type.ToString(),
Message = condition,
StackTrace = stack,
Timestamp = DateTime.UtcNow,
PlayerId = playerId,
SessionId = sessionId,
BuildId = buildId,
DeviceInfo = GetDeviceInfo(),
UnityVersion = Application.unityVersion
};
EnqueueReport(report);
}
}
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;
var report = new CrashReport
{
Type = "UnhandledException",
Message = exception?.Message ?? "Unknown exception",
StackTrace = exception?.StackTrace ?? "",
Timestamp = DateTime.UtcNow,
PlayerId = playerId,
SessionId = sessionId,
BuildId = buildId,
DeviceInfo = GetDeviceInfo(),
UnityVersion = Application.unityVersion,
IsTerminating = e.IsTerminating
};
EnqueueReport(report);
FlushReports(); // Immediate send for unhandled exceptions
}
private void EnqueueReport(CrashReport report)
{
reportQueue.Enqueue(report);
// Prevent queue overflow
while (reportQueue.Count > MAX_QUEUE_SIZE)
{
reportQueue.Dequeue();
}
if (reportQueue.Count >= BATCH_SIZE)
{
FlushReports();
}
}
private async void FlushReports()
{
if (reportQueue.Count == 0) return;
var reports = new List();
while (reportQueue.Count > 0 && reports.Count < BATCH_SIZE)
{
reports.Add(reportQueue.Dequeue());
}
try
{
await SendReports(reports);
}
catch (Exception ex)
{
Debug.LogError($"Failed to send crash reports: {ex.Message}");
// Re-queue failed reports
foreach (var report in reports)
{
reportQueue.Enqueue(report);
}
}
}
private async Awaitable SendReports(List reports)
{
var json = JsonUtility.ToJson(new CrashReportBatch { Reports = reports });
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
using var request = UnityWebRequest.Post(CRASH_ENDPOINT, "POST");
request.uploadHandler = new UploadHandlerRaw(bytes);
request.SetRequestHeader("Content-Type", "application/json");
await request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
throw new Exception(request.error);
}
}
private string GetDeviceInfo()
{
return $"OS: {SystemInfo.operatingSystem}, " +
$"CPU: {SystemInfo.processorType}, " +
$"GPU: {SystemInfo.graphicsDeviceName}, " +
$"RAM: {SystemInfo.systemMemorySize}MB, " +
$"Screen: {Screen.width}x{Screen.height}";
}
void OnDestroy()
{
Application.logMessageReceived -= OnLogMessage;
FlushReports(); // Send any remaining reports
}
}
[Serializable]
public class CrashReport
{
public string Type;
public string Message;
public string StackTrace;
public DateTime Timestamp;
public string PlayerId;
public string SessionId;
public string BuildId;
public string DeviceInfo;
public string UnityVersion;
public bool IsTerminating;
}
[Serializable]
public class CrashReportBatch
{
public List Reports;
}
```
### Step 5.2: Hotfix System
```csharp
// Hotfix/HotfixManager.cs
using UnityEngine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
public class HotfixManager : MonoBehaviour
{
private const string HOTFIX_URL = "https://api.example.com/hotfix";
private const string HOTFIX_VERSION_ENDPOINT = "/manifest.json";
[SerializeField] private bool autoCheckOnStart = true;
[SerializeField] private float checkIntervalHours = 1f;
private HotfixManifest currentManifest;
private DateTime lastCheckTime;
public static HotfixManager Instance { get; private set; }
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
}
async void Start()
{
if (autoCheckOnStart)
{
await CheckForHotfix();
}
// Periodic checks
InvokeRepeating(nameof(PeriodicCheck), checkIntervalHours * 3600, checkIntervalHours * 3600);
}
private async void PeriodicCheck()
{
await CheckForHotfix();
}
public async Task CheckForHotfix()
{
try
{
var manifestUrl = $"{HOTFIX_URL}{HOTFIX_VERSION_ENDPOINT}?v={Application.version}";
var response = await GetAsync(manifestUrl);
if (response != null && response.version > GetCurrentHotfixVersion())
{
Debug.Log($"Hotfix available: {response.version}");
await PromptAndApplyHotfix(response);
}
}
catch (Exception ex)
{
Debug.LogWarning($"Hotfix check failed: {ex.Message}");
}
}
private async Task PromptAndApplyHotfix(HotfixManifest manifest)
{
// Show UI prompt to user
// For now, auto-apply
await ApplyHotfix(manifest);
}
private async Task ApplyHotfix(HotfixManifest manifest)
{
Debug.Log($"Applying hotfix version {manifest.version}...");
foreach (var file in manifest.files)
{
try
{
var data = await DownloadAsync(file.url);
// Verify hash
var hash = ComputeHash(data);
if (hash != file.hash)
{
throw new Exception($"Hash mismatch for {file.path}");
}
// Apply patch
await ApplyPatch(file.path, data);
}
catch (Exception ex)
{
Debug.LogError($"Failed to apply hotfix for {file.path}: {ex.Message}");
return;
}
}
// Update local manifest
SaveManifest(manifest);
currentManifest = manifest;
Debug.Log("Hotfix applied successfully. Restart to apply changes.");
ShowRestartPrompt();
}
private async Task DownloadAsync(string url)
{
using var request = UnityWebRequest.Get(url);
await request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
throw new Exception($"Download failed: {request.error}");
}
return request.downloadHandler.data;
}
private Task ApplyPatch(string path, byte[] data)
{
var fullPath = Path.Combine(Application.persistentDataPath, path);
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
return File.WriteAllBytesAsync(fullPath, data);
}
private string ComputeHash(byte[] data)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(data);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
private Version GetCurrentHotfixVersion()
{
return currentManifest?.version ?? new Version(0, 0, 0);
}
private void SaveManifest(HotfixManifest manifest)
{
var path = Path.Combine(Application.persistentDataPath, "hotfix_manifest.json");
var json = JsonUtility.ToJson(manifest);
File.WriteAllText(path, json);
}
}
[Serializable]
public class HotfixManifest
{
public Version version;
public List files;
public DateTime releasedAt;
public string description;
}
[Serializable]
public class HotfixFile
{
public string path;
public string url;
public string hash;
public long size;
}
```
## Common Mistakes
| # | Mistake | Why It Fails | What to Do Instead |
|---|---------|---------------|-------------------|
| 1 | Building on developer machines | Inconsistent builds | Use CI/CD with containerized builds |
| 2 | Manual signing steps | Human error, delays | Automate with secret management |
| 3 | No artifact caching | Slow builds | Cache dependencies, assets |
| 4 | Single build machine | Bottleneck, SPOF | Distributed builds, multiple agents |
| 5 | No incremental compilation | Slow iteration | Implement change detection |
| 6 | Committing secrets | Security breach | Use secret vaults |
| 7 | No build verification | Release bad builds | Automated tests in pipeline |
| 8 | Ignoring build times | Developer frustration | Profile and optimize |
| 9 | No hotfix capability | Slow response to issues | Implement hotfix pipeline |
| 10 | Ignoring build size | App store rejections | Monitor and optimize bundle size |
## Handoff Protocol
| To | Provide | Format |
|----|---------|--------|
| DevOps | Pipeline configuration, infrastructure needs | Documentation |
| QA | Test environment access, automated tests | Access credentials |
| Platform Relations | Build artifacts, store assets | Required files |
| Community | Release notes, patch notes | Generated content |
| Security | Signing certificates, access logs | Audit trail |
## Execution Checklist
### CI/CD Foundation
- [ ] Canonical local build/test/security pipeline configured
- [ ] Hosted-provider adapter configured only if explicitly required
- [ ] Build caching configured (dependencies, assets)
- [ ] Windows build working
- [ ] macOS build working
- [ ] Linux build working
- [ ] Docker containers for builds defined
### Platform Builds
- [ ] iOS build and signing automated
- [ ] Android build and signing automated
- [ ] Steam build pipeline working
- [ ] Epic build pipeline working
- [ ] WebGL build pipeline working
### Asset Pipeline
- [ ] Texture optimization configured
- [ ] Audio compression configured
- [ ] Shader precompilation working
- [ ] Asset dependency tracking configured
### Release Automation
- [ ] Release notes generator built
- [ ] Pre-release checklist automated
- [ ] Platform upload scripts created
- [ ] Version tagging automated
### Monitoring & Operations
- [ ] Crash reporter integrated
- [ ] Hotfix system implemented
- [ ] Build metrics tracking
- [ ] Performance baselines defined
### Quality Gates
- [ ] Automated tests in pipeline
- [ ] Build times under target threshold
- [ ] Bundle size monitoring
- [ ] All platforms building from CI
### Documentation
- [ ] Build setup documentation
- [ ] Platform guide documentation
- [ ] Hotfix procedures documented
- [ ] Emergency procedures documented