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

This commit is contained in:
2025-12-17 12:32:50 +13:00
commit 3015f48118
471 changed files with 141143 additions and 0 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: connorgallopo
patreon: Gallapagos
thanks_dev: connorgallopo

266
.github/workflows/ci.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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