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/<bunker-pubkey>.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/<versionCode>.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
