Files
eranos/.gitlab-ci.yml
T
Chad Curtis 6dcae6385a ci: use uniform PKCS12 password for signing keystore
packageRelease failed with 'Given final block not properly padded'
because the migrated PKCS12 entry was protected with the store password,
not the key password Gradle read from key.properties. Write the PKCS12
with a single uniform password ($KEY_PASSWORD) for store and entry, and
point both storePassword and keyPassword at it.
2026-06-02 03:39:54 -05:00

482 lines
18 KiB
YAML

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