image: node:22 default: interruptible: true cache: key: files: - package-lock.json paths: - node_modules/ stages: - test - deploy - build - release - publish test: stage: test timeout: 5 minutes rules: - if: $CI_COMMIT_TAG when: never - when: always script: - npm run test # Deploy the built web app to agora.spot on venus.vps via rsync over SSH. # Uses the per-site jailed deploy key documented in GITLAB_DEPLOY.md. # DEPLOY_SSH_KEY and DEPLOY_TARGET are protected CI/CD variables; they're # only exposed to jobs on the protected default branch. deploy-web: stage: deploy timeout: 10 minutes rules: - if: $CI_COMMIT_TAG when: never - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH && $DEPLOY_SSH_KEY && $DEPLOY_TARGET # Vite inlines VITE_* env vars at build time. These are sourced directly from # project-level CI/CD variables, which are already present in the job # environment — do NOT re-declare them here as `KEY: $KEY`. That self-reference # overwrites the real value with the literal string "$KEY" whenever the source # variable is out of scope (e.g. a Protected variable on an unprotected ref), # which is how "$VITE_TRANSLATE_WORKER_URL" leaked into the built app. script: # Build the web app - npm ci - npm run build - cp dist/index.html dist/404.html # Install rsync + ssh client and load the deploy key - apt-get update -qq && apt-get install -y --no-install-recommends rsync openssh-client >/dev/null - mkdir -p ~/.ssh && chmod 700 ~/.ssh - echo "$DEPLOY_SSH_KEY" | tr -d '\r' > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519 - ssh-keyscan -H "${DEPLOY_TARGET##*@}" >> ~/.ssh/known_hosts 2>/dev/null # Two-phase rsync: upload hashed assets first, then index.html and sw.js, # so the site never serves an index.html that points at assets that # haven't finished uploading. sw.js is in the second pass for the same # reason — it's a stable filename that all browsers re-fetch to check # for updates, so we want it to land last. The destination ":/" is the # rrsync jail root on venus, which maps to /var/www/agora.spot/. - rsync -av --exclude=/sw.js --exclude=/index.html -e "ssh -i ~/.ssh/id_ed25519" dist/ "${DEPLOY_TARGET}:/" - rsync -av -e "ssh -i ~/.ssh/id_ed25519" dist/index.html dist/sw.js "${DEPLOY_TARGET}:/" # Disabled: nsite deploy not needed right now; re-enable by restoring the # rules below to run on default branch (and ensure NSITE_NBUNKSEC is set). deploy-nsite: stage: deploy timeout: 10 minutes rules: - when: never # rules: # - if: $CI_COMMIT_TAG # when: never # - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH variables: NSYTE_VERSION: "v0.24.1" script: # Build the web app - npm ci - npm run build - cp dist/index.html dist/404.html # Download nsyte binary - curl -fsSL "https://github.com/sandwichfarm/nsyte/releases/download/${NSYTE_VERSION}/nsyte-linux" -o /usr/local/bin/nsyte - chmod +x /usr/local/bin/nsyte # Deploy to nsite via nsyte using the nbunksec credential - >- nsyte deploy ./dist -i --sec "$NSITE_NBUNKSEC" --name agora --relays "wss://relay.ditto.pub,wss://relay.nsite.lol,wss://relay.dreamith.to,wss://relay.primal.net" --servers "https://blossom.primal.net,https://blossom.ditto.pub,https://blossom.dreamith.to" --fallback "/index.html" --use-fallback-relays --use-fallback-servers release-notes: stage: build timeout: 2 minutes needs: [] rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ script: # Extract release notes from CHANGELOG.md for this tag. # release-notes.md is the full section (summary + bulleted lists), used as # the GitLab Release description. release-notes-summary.txt is the leading # plaintext paragraph only, used as the App Store / Play Store release # blurb. Falls back to "Agora vX.Y.Z" when the section has no summary. - mkdir -p artifacts - node scripts/extract-release-notes.mjs "$CI_COMMIT_TAG" > artifacts/release-notes.md - node scripts/extract-release-notes.mjs "$CI_COMMIT_TAG" --summary > artifacts/release-notes-summary.txt - echo "--- release-notes.md ---" - cat artifacts/release-notes.md - echo "--- release-notes-summary.txt (length $(wc -c < artifacts/release-notes-summary.txt)) ---" - cat artifacts/release-notes-summary.txt - echo "------------------------" # Warn (don't fail) when the summary exceeds the documented 500-character # limit so the user spots it before App Store / Play Store reject the upload. - | SUMMARY_LEN=$(wc -c < artifacts/release-notes-summary.txt) if [ "$SUMMARY_LEN" -gt 501 ]; then echo "WARNING: release-notes-summary.txt is $SUMMARY_LEN bytes; convention is <=500." fi artifacts: paths: - artifacts/release-notes.md - artifacts/release-notes-summary.txt expire_in: 90 days build-apk: stage: build image: eclipse-temurin:21-jdk timeout: 15 minutes needs: [] rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ variables: ANDROID_SDK_ROOT: /opt/android-sdk ANDROID_HOME: /opt/android-sdk before_script: # Install system dependencies - apt-get update -qq - apt-get install -y -qq curl unzip > /dev/null # Install Node.js 22 - curl -fsSL https://deb.nodesource.com/setup_22.x | bash - - apt-get install -y -qq nodejs > /dev/null - node --version - npm --version # Install Android SDK command-line tools - mkdir -p $ANDROID_SDK_ROOT/cmdline-tools - curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o cmdline-tools.zip - unzip -q cmdline-tools.zip -d $ANDROID_SDK_ROOT/cmdline-tools - mv $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools/latest - export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" # Accept licenses and install SDK components - printf 'y\ny\ny\ny\ny\ny\ny\n' | sdkmanager --licenses > /dev/null 2>&1 || true - sdkmanager --install "platforms;android-36" "build-tools;36.0.0" "platform-tools" > /dev/null # Write local.properties for Gradle - echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties # Decode signing keystore and migrate JKS -> PKCS12 for Gradle compatibility. # PKCS12 conceptually uses one password for the store and every entry; if the # store and key passwords differ, keytool protects the migrated entry with the # STORE password regardless of -destkeypass, so Gradle's later read with the # key password fails ("Given final block not properly padded"). Unlock the # source key with its own password ($KEY_PASSWORD), then write the PKCS12 with # a single uniform password ($KEY_PASSWORD) for both store and entry so the # key.properties below is internally consistent. - echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/my-upload-key.jks - keytool -importkeystore -srckeystore android/app/my-upload-key.jks -destkeystore android/app/my-upload-key.keystore -deststoretype pkcs12 -srcstorepass "$KEYSTORE_PASSWORD" -srcalias upload -destalias upload -srckeypass "$KEY_PASSWORD" -deststorepass "$KEY_PASSWORD" -destkeypass "$KEY_PASSWORD" -noprompt - rm android/app/my-upload-key.jks # Write key.properties from CI/CD variables. The PKCS12 above uses # $KEY_PASSWORD uniformly, so both storePassword and keyPassword point to it. - | cat > android/key.properties << EOF storePassword=$KEY_PASSWORD keyPassword=$KEY_PASSWORD keyAlias=upload storeFile=my-upload-key.keystore EOF script: # Extract semver version from git tag (e.g., v2.1.0 -> 2.1.0) - TAG="${CI_COMMIT_TAG#v}" - VERSION_NAME="${TAG}" - VERSION_CODE="${CI_PIPELINE_IID}" - echo "Building version $VERSION_NAME (code $VERSION_CODE) from tag $CI_COMMIT_TAG" # Stamp version into build.gradle - sed -i "s/versionCode [0-9]*/versionCode ${VERSION_CODE}/" android/app/build.gradle - sed -i "s/versionName \"[^\"]*\"/versionName \"${VERSION_NAME}\"/" android/app/build.gradle # Build web assets - npm ci - npx vite build -l error - cp dist/index.html dist/404.html # Sync web assets to Capacitor Android project and register local plugins - npx cap sync android - node scripts/patch-cap-config.mjs # Build signed release APK - cd android && chmod +x gradlew && ./gradlew assembleRelease bundleRelease && cd .. # Copy APK to a predictable artifact path - mkdir -p artifacts - cp android/app/build/outputs/apk/release/app-release.apk "artifacts/Agora.apk" - cp android/app/build/outputs/bundle/release/app-release.aab "artifacts/Agora.aab" - ls -lh artifacts/ # Upload to Generic Packages registry for a stable public download URL - | curl --fail --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \ --upload-file "artifacts/Agora.apk" \ "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/agora/${CI_COMMIT_TAG}/Agora-${CI_COMMIT_TAG}.apk" - | curl --fail --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \ --upload-file "artifacts/Agora.aab" \ "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/agora/${CI_COMMIT_TAG}/Agora-${CI_COMMIT_TAG}.aab" artifacts: paths: - artifacts/Agora.apk - artifacts/Agora.aab expire_in: 90 days cache: key: android-gradle paths: - android/.gradle/ - .gradle/ build-ipa: stage: build tags: - macos timeout: 20 minutes needs: [] rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ variables: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 FASTLANE_HIDE_CHANGELOG: "1" FASTLANE_SKIP_UPDATE_CHECK: "1" before_script: # PATH is set up via ~/.bash_profile on the runner host (brew + Ruby 3.3 + user gems) - node --version - ruby --version - fastlane --version | head -3 # Decode the App Store Connect API key (.p8) into a private location. # The Fastfile reads this directly via File.binread. We pass the API # key into match so it contacts Apple's portal to verify the cert is # still valid for the team — fails fast on a revoked / expired cert. - mkdir -p "$HOME/.private_keys" - chmod 700 "$HOME/.private_keys" - export ASC_KEY_PATH="$HOME/.private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" - echo "$APP_STORE_CONNECT_API_KEY_P8_BASE64" | base64 -d > "$ASC_KEY_PATH" - chmod 600 "$ASC_KEY_PATH" # Avoid env-var collision: match's APP_STORE_CONNECT_API_KEY_PATH expects # a JSON descriptor; we pass the API key inline via the Fastfile. - unset APP_STORE_CONNECT_API_KEY_PATH || true # Build web assets and sync to Capacitor iOS project - npm ci - npx vite build -l error - cp dist/index.html dist/404.html - npx cap sync ios - node scripts/patch-cap-config.mjs script: # Stamp marketing version from the git tag (e.g. v2.1.0 -> 2.1.0) - VERSION="${CI_COMMIT_TAG#v}" - echo "Building iOS version $VERSION (build ${CI_PIPELINE_IID}) from tag $CI_COMMIT_TAG" - >- /usr/bin/sed -i '' "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = ${VERSION};/g" ios/App/App.xcodeproj/project.pbxproj # Run match (cert verify + decrypt) and build_app to produce the IPA. # build_app writes ./artifacts/Agora.ipa relative to the project root. - cd ios - fastlane build_ipa - cd .. # Move the IPA to a stable name in the artifact directory. - ls -lh artifacts/ - test -f artifacts/Agora.ipa # Upload to the Generic Packages registry for a stable public download URL, # mirroring how build-apk publishes the APK and AAB. - | curl --fail --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \ --upload-file "artifacts/Agora.ipa" \ "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/agora/${CI_COMMIT_TAG}/Agora-${CI_COMMIT_TAG}.ipa" after_script: # Wipe the API key so nothing sensitive sticks around between jobs. - rm -f "$HOME/.private_keys"/AuthKey_*.p8 || true artifacts: paths: - artifacts/Agora.ipa expire_in: 90 days release: stage: release image: registry.gitlab.com/gitlab-org/release-cli:latest needs: - job: build-apk artifacts: false - job: build-ipa artifacts: false - job: release-notes artifacts: true rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ script: - echo "Creating release for $CI_COMMIT_TAG" - test -f artifacts/release-notes.md - echo "--- release-notes.md ---" - cat artifacts/release-notes.md - echo "------------------------" release: tag_name: $CI_COMMIT_TAG name: $CI_COMMIT_TAG description: './artifacts/release-notes.md' assets: links: - name: Agora-${CI_COMMIT_TAG}.apk url: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/agora/${CI_COMMIT_TAG}/Agora-${CI_COMMIT_TAG}.apk link_type: package - name: Agora-${CI_COMMIT_TAG}.aab url: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/agora/${CI_COMMIT_TAG}/Agora-${CI_COMMIT_TAG}.aab link_type: package - name: Agora-${CI_COMMIT_TAG}.ipa url: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/agora/${CI_COMMIT_TAG}/Agora-${CI_COMMIT_TAG}.ipa link_type: package publish-zapstore: stage: publish image: golang:1.24 needs: - build-apk rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ variables: SIGN_WITH: $ZAPSTORE_BUNKER_URL RELAY_URLS: "wss://relay.zapstore.dev,wss://relay.ditto.pub,wss://relay.dreamith.to,wss://relay.primal.net" BLOSSOM_URL: "https://blossom.ditto.pub" script: - go install github.com/zapstore/zsp@latest # Restore the persistent NIP-46 client key so the bunker recognizes us across CI runs. # zsp stores client keys at ~/.config/zsp/bunker-keys/.key - BUNKER_PUBKEY=$(echo "$ZAPSTORE_BUNKER_URL" | sed 's|bunker://||;s|?.*||') - mkdir -p ~/.config/zsp/bunker-keys - echo "$ZAPSTORE_CLIENT_KEY" > ~/.config/zsp/bunker-keys/${BUNKER_PUBKEY}.key - APK_PATH="artifacts/Agora.apk" - VERSION="${CI_COMMIT_TAG#v}" - sed -i "2i release_source:\ ./${APK_PATH}" zapstore.yaml - sed -i "2i version:\ ${VERSION}" zapstore.yaml - zsp publish --quiet --skip-metadata --skip-preview zapstore.yaml publish-google-play: stage: publish image: ruby:3.3 needs: - job: build-apk artifacts: true - job: release-notes artifacts: true rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ script: - gem install fastlane --no-document # Decode base64-encoded service account JSON to a temp file - echo "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" | base64 -d > /tmp/play-service-account.json # Build the fastlane supply metadata layout for the changelog. # supply maps changelogs/.txt to the Play Console "What's # new in this version" field. versionCode matches what build-apk stamped # into build.gradle (= CI_PIPELINE_IID). - VERSION_CODE="${CI_PIPELINE_IID}" - CHANGELOG_DIR="android/fastlane/metadata/android/en-US/changelogs" - mkdir -p "$CHANGELOG_DIR" - cp artifacts/release-notes-summary.txt "${CHANGELOG_DIR}/${VERSION_CODE}.txt" - echo "--- ${CHANGELOG_DIR}/${VERSION_CODE}.txt ---" - cat "${CHANGELOG_DIR}/${VERSION_CODE}.txt" - echo "-------------------------------------------" # Upload the AAB to Google Play production track with the changelog. - >- fastlane supply --aab artifacts/Agora.aab --package_name spot.agora.app --track production --json_key /tmp/play-service-account.json --metadata_path android/fastlane/metadata/android --skip_upload_metadata --skip_upload_images --skip_upload_screenshots --skip_upload_apk # Clean up - rm -f /tmp/play-service-account.json publish-app-store: stage: publish # Runs on the self-hosted Mac runner, same as build-ipa. fastlane's `deliver` # action shells out to Apple's iTMSTransporter / altool to upload the IPA # binary, and those tools ship inside Xcode. On a generic Linux container # the upload step crashes with `No such file or directory @ dir_chdir0` # because `Helper.itms_path` resolves to a path inside Xcode that doesn't # exist. The IPA is already signed in `build-ipa`; we just need an Apple # tool to push it, which means macOS. tags: - macos needs: - job: build-ipa artifacts: true - job: release-notes artifacts: true rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ variables: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 FASTLANE_HIDE_CHANGELOG: "1" FASTLANE_SKIP_UPDATE_CHECK: "1" before_script: # PATH is set up via ~/.bash_profile on the runner host (brew + Ruby 3.3 + user gems) - ruby --version - fastlane --version | head -3 # Decode the App Store Connect API key (.p8) into a private location. # The Fastfile reads this directly via File.binread. - mkdir -p "$HOME/.private_keys" - chmod 700 "$HOME/.private_keys" - export ASC_KEY_PATH="$HOME/.private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" - echo "$APP_STORE_CONNECT_API_KEY_P8_BASE64" | base64 -d > "$ASC_KEY_PATH" - chmod 600 "$ASC_KEY_PATH" # Avoid env-var collision: match's APP_STORE_CONNECT_API_KEY_PATH expects # a JSON descriptor; we pass the API key inline via the Fastfile. - unset APP_STORE_CONNECT_API_KEY_PATH || true script: - test -f artifacts/Agora.ipa - test -f artifacts/release-notes-summary.txt # Use the release summary paragraph as the App Store "What's New" text. # Generated by the release-notes job from CHANGELOG.md. - mkdir -p ios/fastlane/metadata/en-US - cp artifacts/release-notes-summary.txt ios/fastlane/metadata/en-US/release_notes.txt - echo "--- release_notes.txt ---" - cat ios/fastlane/metadata/en-US/release_notes.txt - echo "-------------------------" # Submit the prebuilt IPA from build-ipa to App Store Connect for review. - export IPA_PATH="$CI_PROJECT_DIR/artifacts/Agora.ipa" - cd ios - fastlane submit_release after_script: - rm -f "$HOME/.private_keys"/AuthKey_*.p8 || true