Initial Upload
Some checks failed
CI / Lint & Typecheck (push) Has been cancelled
CI / Test (routes) (push) Has been cancelled
CI / Test (security) (push) Has been cancelled
CI / Test (services) (push) Has been cancelled
CI / Test (unit) (push) Has been cancelled
CI / Test (integration) (push) Has been cancelled
CI / Test Coverage (push) Has been cancelled
CI / Build (push) Has been cancelled
Some checks failed
CI / Lint & Typecheck (push) Has been cancelled
CI / Test (routes) (push) Has been cancelled
CI / Test (security) (push) Has been cancelled
CI / Test (services) (push) Has been cancelled
CI / Test (unit) (push) Has been cancelled
CI / Test (integration) (push) Has been cancelled
CI / Test Coverage (push) Has been cancelled
CI / Build (push) Has been cancelled
This commit is contained in:
266
.github/workflows/ci.yml
vendored
Normal file
266
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-and-typecheck:
|
||||
name: Lint & Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.group }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
group: [unit, services, routes, security]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build dependencies (shared, test-utils)
|
||||
run: pnpm turbo run build --filter=@tracearr/shared --filter=@tracearr/test-utils
|
||||
|
||||
- name: Run ${{ matrix.group }} tests
|
||||
run: pnpm --filter @tracearr/server test:${{ matrix.group }}
|
||||
|
||||
# Integration tests require real database - run separately from unit tests
|
||||
test-integration:
|
||||
name: Test (integration)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TEST_DATABASE_URL: postgresql://test:test@localhost:5433/tracearr_test
|
||||
TEST_REDIS_URL: redis://localhost:6380
|
||||
|
||||
services:
|
||||
timescale:
|
||||
image: timescale/timescaledb:latest-pg15
|
||||
ports:
|
||||
- 5433:5432
|
||||
env:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
POSTGRES_DB: tracearr_test
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U test -d tracearr_test"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6380:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build dependencies (shared, test-utils)
|
||||
run: pnpm turbo run build --filter=@tracearr/shared --filter=@tracearr/test-utils
|
||||
|
||||
- name: Run integration tests
|
||||
run: pnpm --filter @tracearr/server test:integration
|
||||
|
||||
# Separate job for coverage reporting (runs all tests together for accurate coverage)
|
||||
# Runs in parallel with test matrix - if tests pass there, coverage will too
|
||||
test-coverage:
|
||||
name: Test Coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build dependencies (shared, test-utils)
|
||||
run: pnpm turbo run build --filter=@tracearr/shared --filter=@tracearr/test-utils
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: pnpm test:coverage
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: coverage-report
|
||||
path: apps/server/coverage/
|
||||
retention-days: 7
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Report coverage to PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
with:
|
||||
working-directory: apps/server
|
||||
vite-config-path: vitest.config.ts
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: [lint-and-typecheck, test, test-integration, test-coverage]
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
454
.github/workflows/mobile-build.yml
vendored
Normal file
454
.github/workflows/mobile-build.yml
vendored
Normal file
@@ -0,0 +1,454 @@
|
||||
name: Mobile Build (Local)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Git tag to build (e.g., v1.2.3-beta.1)'
|
||||
required: true
|
||||
type: string
|
||||
platform:
|
||||
description: 'Platform to build'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- android
|
||||
- ios
|
||||
- both
|
||||
default: 'both'
|
||||
distribution:
|
||||
description: 'Distribution method'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- internal
|
||||
- store
|
||||
default: 'internal'
|
||||
profile:
|
||||
description: 'Build profile'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- preview
|
||||
- production
|
||||
default: 'preview'
|
||||
|
||||
# Prevent concurrent builds that could cause version conflicts
|
||||
concurrency:
|
||||
group: mobile-build
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
jobs:
|
||||
# Atomically prepare and reserve version numbers before builds start
|
||||
prepare-versions:
|
||||
name: Prepare Versions
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile
|
||||
outputs:
|
||||
marketing-version: ${{ steps.versions.outputs.marketing-version }}
|
||||
android-version-code: ${{ steps.versions.outputs.android-version-code }}
|
||||
ios-build-number: ${{ steps.versions.outputs.ios-build-number }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: .
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Reserve version numbers
|
||||
id: versions
|
||||
env:
|
||||
EAS_PROJECT_ID: "2e0b9595-ac62-493f-9a10-4f8758bb4b2d"
|
||||
APP_IDENTIFIER: "com.tracearr.app"
|
||||
run: |
|
||||
# Extract marketing version from git tag
|
||||
VERSION="${{ inputs.tag }}"
|
||||
VERSION="${VERSION#v}"
|
||||
VERSION="${VERSION%%-*}"
|
||||
echo "Marketing version: $VERSION"
|
||||
echo "marketing-version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get current versions from EAS remote
|
||||
echo "Getting current versions from EAS remote..."
|
||||
REMOTE_VERSIONS=$(eas build:version:get --platform all --json --non-interactive 2>/dev/null || echo '{}')
|
||||
echo "Remote versions: $REMOTE_VERSIONS"
|
||||
|
||||
# Parse current versions (default to 0 if not set)
|
||||
CURRENT_ANDROID_VC=$(echo "$REMOTE_VERSIONS" | jq -r '.versionCode // "0"')
|
||||
CURRENT_IOS_BN=$(echo "$REMOTE_VERSIONS" | jq -r '.buildNumber // "0"')
|
||||
|
||||
# Increment versions
|
||||
NEXT_ANDROID_VC=$((CURRENT_ANDROID_VC + 1))
|
||||
NEXT_IOS_BN=$((CURRENT_IOS_BN + 1))
|
||||
|
||||
echo "Android versionCode: $CURRENT_ANDROID_VC -> $NEXT_ANDROID_VC"
|
||||
echo "iOS buildNumber: $CURRENT_IOS_BN -> $NEXT_IOS_BN"
|
||||
|
||||
# Reserve versions on EAS remote via GraphQL API
|
||||
echo "Reserving Android version $NEXT_ANDROID_VC on EAS..."
|
||||
ANDROID_RESULT=$(curl -sf -X POST https://api.expo.dev/graphql \
|
||||
-H "Authorization: Bearer $EXPO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"query\":\"mutation { appVersion { createAppVersion(appVersionInput: { appId: \\\"$EAS_PROJECT_ID\\\", platform: ANDROID, applicationIdentifier: \\\"$APP_IDENTIFIER\\\", storeVersion: \\\"$VERSION\\\", buildVersion: \\\"$NEXT_ANDROID_VC\\\" }) { id } } }\"}")
|
||||
echo "Android result: $ANDROID_RESULT"
|
||||
|
||||
echo "Reserving iOS version $NEXT_IOS_BN on EAS..."
|
||||
IOS_RESULT=$(curl -sf -X POST https://api.expo.dev/graphql \
|
||||
-H "Authorization: Bearer $EXPO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"query\":\"mutation { appVersion { createAppVersion(appVersionInput: { appId: \\\"$EAS_PROJECT_ID\\\", platform: IOS, applicationIdentifier: \\\"$APP_IDENTIFIER\\\", storeVersion: \\\"$VERSION\\\", buildVersion: \\\"$NEXT_IOS_BN\\\" }) { id } } }\"}")
|
||||
echo "iOS result: $IOS_RESULT"
|
||||
|
||||
# Output for build jobs
|
||||
echo "android-version-code=$NEXT_ANDROID_VC" >> $GITHUB_OUTPUT
|
||||
echo "ios-build-number=$NEXT_IOS_BN" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Versions reserved successfully"
|
||||
|
||||
build-android:
|
||||
name: Build Android
|
||||
needs: [prepare-versions]
|
||||
if: inputs.platform == 'android' || inputs.platform == 'both'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile
|
||||
outputs:
|
||||
artifact-path: ${{ steps.build.outputs.artifact-path }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Free disk space
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: .
|
||||
|
||||
- name: Build shared package
|
||||
run: pnpm --filter @tracearr/shared build
|
||||
working-directory: .
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Set versions from prepare job
|
||||
run: |
|
||||
VERSION="${{ needs.prepare-versions.outputs.marketing-version }}"
|
||||
VERSION_CODE="${{ needs.prepare-versions.outputs.android-version-code }}"
|
||||
|
||||
echo "Setting version: $VERSION, versionCode: $VERSION_CODE"
|
||||
|
||||
jq --arg v "$VERSION" \
|
||||
--argjson vc "$VERSION_CODE" \
|
||||
'.expo.version = $v | .expo.android.versionCode = $vc' \
|
||||
app.json > tmp.json && mv tmp.json app.json
|
||||
|
||||
- name: Build Android locally
|
||||
id: build
|
||||
env:
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
run: |
|
||||
BUILD_TYPE="${{ inputs.distribution == 'internal' && 'apk' || 'app-bundle' }}"
|
||||
OUTPUT_EXT="${{ inputs.distribution == 'internal' && 'apk' || 'aab' }}"
|
||||
OUTPUT_FILE="tracearr-${{ inputs.tag }}-android.${OUTPUT_EXT}"
|
||||
|
||||
echo "Building $BUILD_TYPE for ${{ inputs.profile }} profile..."
|
||||
|
||||
eas build \
|
||||
--platform android \
|
||||
--profile ${{ inputs.profile }} \
|
||||
--local \
|
||||
--output "./${OUTPUT_FILE}" \
|
||||
--non-interactive
|
||||
|
||||
echo "artifact-path=apps/mobile/${OUTPUT_FILE}" >> $GITHUB_OUTPUT
|
||||
echo "Build complete: ${OUTPUT_FILE}"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-build
|
||||
path: ${{ steps.build.outputs.artifact-path }}
|
||||
retention-days: 30
|
||||
|
||||
build-ios:
|
||||
name: Build iOS
|
||||
needs: [prepare-versions]
|
||||
if: inputs.platform == 'ios' || inputs.platform == 'both'
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 90
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile
|
||||
outputs:
|
||||
artifact-path: ${{ steps.build.outputs.artifact-path }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: .
|
||||
|
||||
- name: Build shared package
|
||||
run: pnpm --filter @tracearr/shared build
|
||||
working-directory: .
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Set versions from prepare job
|
||||
run: |
|
||||
VERSION="${{ needs.prepare-versions.outputs.marketing-version }}"
|
||||
BUILD_NUMBER="${{ needs.prepare-versions.outputs.ios-build-number }}"
|
||||
|
||||
echo "Setting version: $VERSION, buildNumber: $BUILD_NUMBER"
|
||||
|
||||
jq --arg v "$VERSION" \
|
||||
--arg bn "$BUILD_NUMBER" \
|
||||
'.expo.version = $v | .expo.ios.buildNumber = $bn' \
|
||||
app.json > tmp.json && mv tmp.json app.json
|
||||
|
||||
- name: Build iOS locally
|
||||
id: build
|
||||
run: |
|
||||
OUTPUT_FILE="tracearr-${{ inputs.tag }}-ios.ipa"
|
||||
|
||||
echo "Building iOS for ${{ inputs.profile }} profile..."
|
||||
|
||||
eas build \
|
||||
--platform ios \
|
||||
--profile ${{ inputs.profile }} \
|
||||
--local \
|
||||
--output "./${OUTPUT_FILE}" \
|
||||
--non-interactive
|
||||
|
||||
echo "artifact-path=apps/mobile/${OUTPUT_FILE}" >> $GITHUB_OUTPUT
|
||||
echo "Build complete: ${OUTPUT_FILE}"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ios-build
|
||||
path: ${{ steps.build.outputs.artifact-path }}
|
||||
retention-days: 30
|
||||
|
||||
upload-and-submit:
|
||||
name: Upload and Submit
|
||||
needs: [prepare-versions, build-android, build-ios]
|
||||
if: always() && (needs.build-android.result == 'success' || needs.build-ios.result == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: .
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Download Android artifact
|
||||
if: needs.build-android.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-build
|
||||
path: apps/mobile/
|
||||
|
||||
- name: Download iOS artifact
|
||||
if: needs.build-ios.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ios-build
|
||||
path: apps/mobile/
|
||||
|
||||
- name: Upload Android to EAS
|
||||
if: needs.build-android.result == 'success'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
APK_FILE=$(ls *.apk 2>/dev/null || ls *.aab 2>/dev/null)
|
||||
echo "Uploading $APK_FILE to EAS..."
|
||||
eas upload --platform android --build-path "./${APK_FILE}" --non-interactive
|
||||
|
||||
- name: Upload iOS to EAS
|
||||
if: needs.build-ios.result == 'success'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
IPA_FILE=$(ls *.ipa)
|
||||
echo "Uploading $IPA_FILE to EAS..."
|
||||
eas upload --platform ios --build-path "./${IPA_FILE}" --non-interactive
|
||||
|
||||
- name: Decode Google Service Account
|
||||
if: inputs.distribution == 'store' && needs.build-android.result == 'success'
|
||||
run: |
|
||||
mkdir -p credentials
|
||||
echo "${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}" | base64 -d > credentials/google-service-account.json
|
||||
|
||||
- name: Submit Android to Play Store
|
||||
if: inputs.distribution == 'store' && needs.build-android.result == 'success'
|
||||
run: |
|
||||
AAB_FILE=$(ls *.aab)
|
||||
echo "Submitting $AAB_FILE to Play Store..."
|
||||
eas submit --platform android --path "./${AAB_FILE}" --profile ${{ inputs.profile }} --non-interactive
|
||||
|
||||
- name: Submit iOS to App Store
|
||||
if: inputs.distribution == 'store' && needs.build-ios.result == 'success'
|
||||
env:
|
||||
EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
run: |
|
||||
IPA_FILE=$(ls *.ipa)
|
||||
echo "Submitting $IPA_FILE to App Store..."
|
||||
eas submit --platform ios --path "./${IPA_FILE}" --profile ${{ inputs.profile }} --non-interactive
|
||||
|
||||
summary:
|
||||
name: Build Summary
|
||||
needs: [prepare-versions, build-android, build-ios, upload-and-submit]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Generate summary
|
||||
run: |
|
||||
{
|
||||
echo "## Mobile Build Summary"
|
||||
echo ""
|
||||
echo "**Tag**: ${{ inputs.tag }}"
|
||||
echo "**Platform**: ${{ inputs.platform }}"
|
||||
echo "**Distribution**: ${{ inputs.distribution }}"
|
||||
echo "**Profile**: ${{ inputs.profile }}"
|
||||
echo ""
|
||||
echo "### Version Info"
|
||||
echo "- **Marketing Version**: ${{ needs.prepare-versions.outputs.marketing-version }}"
|
||||
echo "- **Android versionCode**: ${{ needs.prepare-versions.outputs.android-version-code }}"
|
||||
echo "- **iOS buildNumber**: ${{ needs.prepare-versions.outputs.ios-build-number }}"
|
||||
echo ""
|
||||
echo "### Build Results"
|
||||
echo ""
|
||||
|
||||
if [[ "${{ inputs.platform }}" == "android" || "${{ inputs.platform }}" == "both" ]]; then
|
||||
if [[ "${{ needs.build-android.result }}" == "success" ]]; then
|
||||
echo "[OK] Android: Built successfully"
|
||||
else
|
||||
echo "[FAIL] Android: ${{ needs.build-android.result }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${{ inputs.platform }}" == "ios" || "${{ inputs.platform }}" == "both" ]]; then
|
||||
if [[ "${{ needs.build-ios.result }}" == "success" ]]; then
|
||||
echo "[OK] iOS: Built successfully"
|
||||
else
|
||||
echo "[FAIL] iOS: ${{ needs.build-ios.result }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "### Upload and Submit"
|
||||
if [[ "${{ needs.upload-and-submit.result }}" == "success" ]]; then
|
||||
echo "[OK] Uploaded to EAS"
|
||||
if [[ "${{ inputs.distribution }}" == "store" ]]; then
|
||||
echo "[OK] Submitted to stores"
|
||||
else
|
||||
echo "[INFO] Internal distribution - check Expo dashboard for download links"
|
||||
fi
|
||||
else
|
||||
echo "[WARN] Upload/Submit: ${{ needs.upload-and-submit.result }}"
|
||||
fi
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
314
.github/workflows/mobile-release.yml
vendored
Normal file
314
.github/workflows/mobile-release.yml
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
name: Mobile Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to deploy (e.g., v1.2.3-beta.1)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: mobile-release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Check if this is a prerelease (alpha, beta, rc)
|
||||
check-release-type:
|
||||
name: Check Release Type
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
prerelease: ${{ steps.check.outputs.prerelease }}
|
||||
previous_tag: ${{ steps.prev_tag.outputs.tag }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Check if prerelease
|
||||
id: check
|
||||
run: |
|
||||
TAG="${{ inputs.tag }}"
|
||||
if [[ "$TAG" == *-* ]]; then
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
echo "This is a prerelease: $TAG"
|
||||
else
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
echo "This is a stable release: $TAG"
|
||||
fi
|
||||
|
||||
- name: Get previous tag
|
||||
id: prev_tag
|
||||
run: |
|
||||
TAG="${{ inputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
||||
echo "tag=$PREV_TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "Previous tag: $PREV_TAG"
|
||||
|
||||
# Check if mobile code changed since last tag
|
||||
check-mobile-changes:
|
||||
name: Check Mobile Changes
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type]
|
||||
if: needs.check-release-type.outputs.prerelease == 'true'
|
||||
outputs:
|
||||
mobile_changed: ${{ steps.check.outputs.changed }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Check for mobile changes
|
||||
id: check
|
||||
run: |
|
||||
PREV_TAG="${{ needs.check-release-type.outputs.previous_tag }}"
|
||||
CURRENT_TAG="${{ inputs.tag }}"
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "No previous tag found, assuming mobile changed"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if any files in apps/mobile or shared packages changed
|
||||
CHANGES=$(git diff --name-only "$PREV_TAG" "$CURRENT_TAG" -- apps/mobile/ packages/shared/ | wc -l)
|
||||
|
||||
if [ "$CHANGES" -gt 0 ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Mobile changes detected: $CHANGES files"
|
||||
git diff --name-only "$PREV_TAG" "$CURRENT_TAG" -- apps/mobile/ packages/shared/
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No mobile changes since $PREV_TAG"
|
||||
fi
|
||||
|
||||
# Deploy mobile app using fingerprint-based detection
|
||||
# This action automatically:
|
||||
# 1. Calculates the native fingerprint
|
||||
# 2. Checks if a compatible build already exists
|
||||
# 3. If no build exists → triggers eas build
|
||||
# 4. If build exists → reuses it
|
||||
# 5. Pushes OTA update to the channel
|
||||
deploy:
|
||||
name: Deploy Mobile
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type, check-mobile-changes]
|
||||
if: |
|
||||
needs.check-release-type.outputs.prerelease == 'true' &&
|
||||
needs.check-mobile-changes.outputs.mobile_changed == 'true'
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: .
|
||||
|
||||
- name: Build shared package
|
||||
run: pnpm --filter @tracearr/shared build
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Validate secrets
|
||||
run: |
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "::error::EXPO_TOKEN secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Sync version from git tag
|
||||
run: |
|
||||
# Extract version from tag (v1.2.3-alpha.4 -> 1.2.3)
|
||||
VERSION="${{ inputs.tag }}"
|
||||
VERSION="${VERSION#v}" # Remove 'v' prefix
|
||||
VERSION="${VERSION%%-*}" # Remove prerelease suffix (-alpha.4, -beta.1, etc.)
|
||||
echo "Setting app version to: $VERSION"
|
||||
# Update app.json with the version
|
||||
jq --arg v "$VERSION" '.expo.version = $v' app.json > tmp.json && mv tmp.json app.json
|
||||
# Verify the change
|
||||
ACTUAL=$(jq -r '.expo.version' app.json)
|
||||
if [ "$ACTUAL" != "$VERSION" ]; then
|
||||
echo "::error::Version sync failed. Expected $VERSION, got $ACTUAL"
|
||||
exit 1
|
||||
fi
|
||||
echo "Version synced: $ACTUAL"
|
||||
|
||||
- name: Decode Google Service Account
|
||||
run: |
|
||||
mkdir -p credentials
|
||||
echo "${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}" | base64 -d > credentials/google-service-account.json
|
||||
|
||||
- name: Deploy with fingerprint
|
||||
uses: expo/expo-github-action/continuous-deploy-fingerprint@main
|
||||
id: deploy
|
||||
with:
|
||||
profile: preview
|
||||
branch: preview
|
||||
working-directory: apps/mobile
|
||||
auto-submit-builds: true
|
||||
env:
|
||||
EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
|
||||
- name: Output deployment info
|
||||
run: |
|
||||
echo "## Deployment Results" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "**Fingerprint**: \`${{ steps.deploy.outputs.fingerprint }}\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "**Build needed**: ${{ steps.deploy.outputs.build-new-build }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -n "${{ steps.deploy.outputs.ios-build-id }}" ]; then
|
||||
echo "**iOS Build ID**: ${{ steps.deploy.outputs.ios-build-id }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
if [ -n "${{ steps.deploy.outputs.android-build-id }}" ]; then
|
||||
echo "**Android Build ID**: ${{ steps.deploy.outputs.android-build-id }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
if [ -n "${{ steps.deploy.outputs.update-id }}" ]; then
|
||||
echo "**Update ID**: ${{ steps.deploy.outputs.update-id }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
# Fallback: Manual OTA if fingerprint action has issues
|
||||
# This job only runs if deploy fails and we want to try OTA anyway
|
||||
ota-fallback:
|
||||
name: OTA Fallback
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type, check-mobile-changes, deploy]
|
||||
if: |
|
||||
always() &&
|
||||
needs.deploy.result == 'failure' &&
|
||||
needs.check-release-type.outputs.prerelease == 'true' &&
|
||||
needs.check-mobile-changes.outputs.mobile_changed == 'true'
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: .
|
||||
|
||||
- name: Build shared package
|
||||
run: pnpm --filter @tracearr/shared build
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Push OTA update with retry
|
||||
run: |
|
||||
TAG="${{ inputs.tag }}"
|
||||
echo "::warning::Deploy failed, attempting OTA fallback for $TAG"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
echo "Attempt $attempt of 3..."
|
||||
if eas update \
|
||||
--channel preview \
|
||||
--message "Release $TAG (fallback)" \
|
||||
--non-interactive; then
|
||||
echo "OTA update successful on attempt $attempt"
|
||||
exit 0
|
||||
fi
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo "Attempt $attempt failed, waiting before retry..."
|
||||
sleep $((30 * attempt))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "::error::OTA update failed after 3 attempts"
|
||||
exit 1
|
||||
|
||||
# Summary job that reports status
|
||||
summary:
|
||||
name: Release Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type, check-mobile-changes, deploy, ota-fallback]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Report status
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
PRERELEASE: ${{ needs.check-release-type.outputs.prerelease }}
|
||||
MOBILE_CHANGED: ${{ needs.check-mobile-changes.outputs.mobile_changed || 'skipped' }}
|
||||
DEPLOY_RESULT: ${{ needs.deploy.result }}
|
||||
FALLBACK_RESULT: ${{ needs.ota-fallback.result }}
|
||||
run: |
|
||||
{
|
||||
echo "## Mobile Release Summary"
|
||||
echo ""
|
||||
echo "**Tag**: $TAG"
|
||||
echo "**Prerelease**: $PRERELEASE"
|
||||
echo "**Mobile Changed**: $MOBILE_CHANGED"
|
||||
echo ""
|
||||
|
||||
if [[ "$PRERELEASE" != "true" ]]; then
|
||||
echo "ℹ️ Skipped: Not a prerelease (stable releases don't trigger mobile builds)"
|
||||
elif [[ "$MOBILE_CHANGED" != "true" ]]; then
|
||||
echo "ℹ️ Skipped: No mobile code changes detected"
|
||||
else
|
||||
echo "### Deployment Status"
|
||||
echo ""
|
||||
if [[ "$DEPLOY_RESULT" == "success" ]]; then
|
||||
echo "✅ **Deploy**: Success"
|
||||
echo ""
|
||||
echo "The fingerprint-based deployment completed successfully."
|
||||
echo "- If native changes were detected, a new build was triggered"
|
||||
echo "- If only JS changes, an OTA update was pushed"
|
||||
echo ""
|
||||
echo "Check the [Expo dashboard](https://expo.dev) for details."
|
||||
elif [[ "$FALLBACK_RESULT" == "success" ]]; then
|
||||
echo "⚠️ **Deploy**: Failed (fingerprint action issue)"
|
||||
echo "✅ **OTA Fallback**: Success"
|
||||
echo ""
|
||||
echo "The main deploy failed but OTA fallback succeeded."
|
||||
echo "Users will receive the JS update on next app launch."
|
||||
else
|
||||
echo "❌ **Deploy**: $DEPLOY_RESULT"
|
||||
echo "❌ **OTA Fallback**: $FALLBACK_RESULT"
|
||||
echo ""
|
||||
echo "Both deployment methods failed. Check workflow logs for details."
|
||||
fi
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
137
.github/workflows/nightly.yml
vendored
Normal file
137
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 4am UTC daily
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
concurrency:
|
||||
group: nightly
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository must be lowercased for Docker registry compatibility
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/tracearr
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
name: Check for changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if new commits since last nightly
|
||||
id: check
|
||||
run: |
|
||||
# Get commits in last 25 hours (buffer for timing)
|
||||
COMMITS=$(git log --since="25 hours ago" --oneline | wc -l)
|
||||
if [ "$COMMITS" -gt 0 ]; then
|
||||
echo "should_build=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Found $COMMITS new commits"
|
||||
else
|
||||
echo "should_build=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No new commits, skipping build"
|
||||
fi
|
||||
|
||||
build-and-push:
|
||||
name: Build & Push Docker
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-changes]
|
||||
if: needs.check-changes.outputs.should_build == 'true' || github.event_name == 'workflow_dispatch'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=nightly
|
||||
type=raw,value=nightly-{{date 'YYYYMMDD'}}
|
||||
type=sha,prefix=sha-
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-and-push-supervised:
|
||||
name: Build & Push Supervised Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-changes]
|
||||
if: needs.check-changes.outputs.should_build == 'true' || github.event_name == 'workflow_dispatch'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for supervised image
|
||||
id: meta-supervised
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=supervised-nightly
|
||||
type=raw,value=supervised-nightly-{{date 'YYYYMMDD'}}
|
||||
|
||||
- name: Build and push supervised image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.supervised
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-supervised.outputs.tags }}
|
||||
labels: ${{ steps.meta-supervised.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
455
.github/workflows/release.yml
vendored
Normal file
455
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,455 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository must be lowercased for Docker registry compatibility
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/tracearr
|
||||
|
||||
jobs:
|
||||
# Run the same checks as CI
|
||||
lint-and-typecheck:
|
||||
name: Lint & Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.group }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
group: [unit, services, routes, security]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build dependencies (shared, test-utils)
|
||||
run: pnpm turbo run build --filter=@tracearr/shared --filter=@tracearr/test-utils
|
||||
|
||||
- name: Run ${{ matrix.group }} tests
|
||||
run: pnpm --filter @tracearr/server test:${{ matrix.group }}
|
||||
|
||||
# Integration tests require real database - run separately from unit tests
|
||||
test-integration:
|
||||
name: Test (integration)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TEST_DATABASE_URL: postgresql://test:test@localhost:5433/tracearr_test
|
||||
TEST_REDIS_URL: redis://localhost:6380
|
||||
|
||||
services:
|
||||
timescale:
|
||||
image: timescale/timescaledb:latest-pg15
|
||||
ports:
|
||||
- 5433:5432
|
||||
env:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
POSTGRES_DB: tracearr_test
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U test -d tracearr_test"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6380:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 5s
|
||||
--health-timeout 3s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache Turborepo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build dependencies (shared, test-utils)
|
||||
run: pnpm turbo run build --filter=@tracearr/shared --filter=@tracearr/test-utils
|
||||
|
||||
- name: Run integration tests
|
||||
run: pnpm --filter @tracearr/server test:integration
|
||||
|
||||
# Determine if this is a prerelease (alpha, beta, rc, etc.)
|
||||
check-release-type:
|
||||
name: Check Release Type
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-typecheck, test, test-integration]
|
||||
outputs:
|
||||
prerelease: ${{ steps.check.outputs.prerelease }}
|
||||
|
||||
steps:
|
||||
- name: Check if prerelease
|
||||
id: check
|
||||
run: |
|
||||
TAG="${{ github.ref_name }}"
|
||||
if [[ "$TAG" == *-* ]]; then
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
echo "This is a prerelease: $TAG"
|
||||
else
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
echo "This is a stable release: $TAG"
|
||||
fi
|
||||
|
||||
# Build each platform natively in parallel (much faster than QEMU emulation)
|
||||
build-docker:
|
||||
name: Build Docker (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: [check-release-type]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=docker-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=docker-${{ matrix.arch }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-docker-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Merge platform-specific images into multi-arch manifest
|
||||
merge-docker:
|
||||
name: Merge Docker Manifest
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type, build-docker]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-docker-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ needs.check-release-type.outputs.prerelease == 'false' }}
|
||||
type=semver,pattern={{major}},enable=${{ needs.check-release-type.outputs.prerelease == 'false' }}
|
||||
type=raw,value=next,enable=${{ needs.check-release-type.outputs.prerelease == 'true' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
if [ -z "$(ls -A .)" ]; then
|
||||
echo "Error: No digests found!"
|
||||
exit 1
|
||||
fi
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
# Build supervised image on each platform
|
||||
build-supervised:
|
||||
name: Build Supervised (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: [check-release-type]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile.supervised
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=supervised-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=supervised-${{ matrix.arch }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-supervised-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Merge supervised multi-arch manifest
|
||||
merge-supervised:
|
||||
name: Merge Supervised Manifest
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type, build-supervised]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-supervised-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Use flavor prefix for reliable tag prefixing
|
||||
flavor: |
|
||||
prefix=supervised-,onlatest=false
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=supervised,enable=${{ needs.check-release-type.outputs.prerelease == 'false' }},prefix=
|
||||
type=raw,value=supervised-next,enable=${{ needs.check-release-type.outputs.prerelease == 'true' }},prefix=
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
if [ -z "$(ls -A .)" ]; then
|
||||
echo "Error: No digests found!"
|
||||
exit 1
|
||||
fi
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-release-type, merge-docker, merge-supervised]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get tag annotation
|
||||
id: tag_info
|
||||
run: |
|
||||
# Get the tag message (annotation)
|
||||
TAG_MESSAGE=$(git tag -l --format='%(contents)' ${{ github.ref_name }})
|
||||
# Write to file to preserve newlines
|
||||
echo "$TAG_MESSAGE" > tag_message.txt
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ github.ref_name }}
|
||||
body_path: tag_message.txt
|
||||
prerelease: ${{ needs.check-release-type.outputs.prerelease }}
|
||||
generate_release_notes: false
|
||||
Reference in New Issue
Block a user