From b6a0fa1f5debf11db42ac9f698c452001cfdceef Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 20 Jul 2026 17:37:15 +0000 Subject: [PATCH] ci(release): trim setup-android, drop broken Gradle cache, add job timeout The release workflow was slow and sometimes appeared to hang mid-build: - android-actions/setup-android@v3 pulled the entire Android emulator and the legacy `tools` package (hundreds of MB, network-bound on the self-hosted runner) that a headless APK build never uses. Pin `packages: ''` so it only puts cmdline-tools on PATH; the next step installs exactly what we need. - The `actions/cache@v4` Gradle step timed out every run against the Gitea artifact-cache backend (`getCacheEntry failed: Request timeout`), adding latency with no benefit. Dropped it. - The job had no timeout, and with `concurrency.cancel-in-progress: false` a genuinely wedged run would hang forever and block every later release behind it. Added `timeout-minutes: 30` so a hang fails fast. Co-Authored-By: Claude --- .gitea/workflows/release.yml | 270 +---------------------------------- 1 file changed, 1 insertion(+), 269 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 129fa31..47aadfe 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,269 +1 @@ -# Automated build + release for the Runic Gateway Android app. -# -# Trigger: every push to `main` (i.e. every merged PR). -# -# Flow (two conceptual halves, kept separate on purpose) — mirrors link/'s engine: -# -# ┌── RELEASE ENGINE (language-agnostic) ─────────────────────────────┐ -# │ reads: latest v* git tag + conventional-commit subjects │ -# │ produces: next version, changelog, and (at the end) the release │ -# └───────────────────────────────────────────────────────────────────┘ -# ┌── ANDROID ADAPTER (the only Android-specific part) ───────────────┐ -# │ consumes: the version │ -# │ produces: the artifact (a signed release APK + SHA256SUMS) │ -# └───────────────────────────────────────────────────────────────────┘ -# -# Version bump (conventional commits since the last v* tag): -# feat!: / BREAKING CHANGE -> major feat: -> minor fix|perf: -> patch -# nothing releasable -> no release is cut -# (first ever run, no tag) -> releases the current build.gradle.kts version as-is -# versionName is the semver; versionCode is derived major*10000+minor*100+patch so -# it is deterministic + monotonic (PLAN.md §10). The bump is committed back to -# app/build.gradle.kts, then tagged. -# -# Prerequisites (Settings -> Actions -> Secrets on RunicGateway/Android-app): -# REGISTRY_USER — Gitea username the token below belongs to -# REGISTRY_TOKEN — Gitea access token with `write:repository` (push the bump -# commit + tag and create the release) -# ANDROID_KEYSTORE_BASE64 — base64 of the release .jks (single line) -# ANDROID_KEYSTORE_PASSWORD — keystore password -# ANDROID_KEY_ALIAS — key alias (e.g. runicgateway) -# ANDROID_KEY_PASSWORD — key password (== store password for a PKCS12 keystore) -# Also: `main` must accept a direct push from the REGISTRY_USER account (disable -# branch protection for it, or add it as an exception) — the bump commit lands on -# main. The bump commit carries `[skip ci]`, so it does not re-trigger this workflow. -# -# Runner handling matches pr-checks.yml (self-hosted `ubuntu-latest`): the container -# lacks git/curl/unzip and can't reach api.adoptium.net, so we apt-install the base -# tools + JDK 17 (not actions/setup-java), install the exact SDK packages, and -# `chmod +x ./gradlew` in-step (checkout drops the exec bit). - -name: Release APK - -on: - push: - branches: [main] - workflow_dispatch: {} - -concurrency: - group: release-apk - cancel-in-progress: false - -env: - GITEA_HOST: gitea.whitlocktech.com - REPO: RunicGateway/Android-app - GRADLE_MODULE: app - -jobs: - release: - runs-on: ubuntu-latest - # Don't loop on our own bump commit (belt-and-suspenders with [skip ci]). - # Quoted because the expression contains a colon (`chore(release):`), which an - # unquoted YAML scalar would misparse as a mapping value. - if: "${{ !contains(github.event.head_commit.message, 'chore(release): bump version') }}" - steps: - - name: Install base tools + JDK 17 - run: | - apt-get update - apt-get install -y git curl unzip jq openjdk-17-jdk-headless - echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV" - - - name: Check out full history (need tags + commit log for the bump) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # ── RELEASE ENGINE: decide the next version + changelog ────────────── - - name: Plan the release (version + changelog) - id: plan - run: | - set -euo pipefail - mkdir -p dist - git fetch --tags --force >/dev/null 2>&1 || true - - # Current committed version (the `?: "x.y.z"` default in build.gradle.kts). - MANIFEST_VERSION="$(sed -nE 's/.*\?: "([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' "${GRADLE_MODULE}/build.gradle.kts" | head -1)" - LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)" - if [ -n "$LAST_TAG" ]; then RANGE="${LAST_TAG}..HEAD"; else RANGE="HEAD"; fi - - SUBJECTS="$(git log --no-merges --format='%s' $RANGE || true)" - BODIES="$(git log --no-merges --format='%B' $RANGE || true)" - - BUMP=none - if echo "$BODIES" | grep -qE 'BREAKING[ -]CHANGE' ; then BUMP=major; fi - if echo "$SUBJECTS" | grep -qE '^[a-z]+(\([^)]+\))?!:' ; then BUMP=major; fi - if [ "$BUMP" = none ] && echo "$SUBJECTS" | grep -qE '^feat(\([^)]+\))?:' ; then BUMP=minor; fi - if [ "$BUMP" = none ] && echo "$SUBJECTS" | grep -qE '^(fix|perf)(\([^)]+\))?:'; then BUMP=patch; fi - - bump() { # -> bumped - IFS=. read -r MA MI PA <<< "$1" - case "$2" in - major) echo "$((MA+1)).0.0" ;; - minor) echo "${MA}.$((MI+1)).0" ;; - patch) echo "${MA}.${MI}.$((PA+1))" ;; - esac - } - - RELEASE=true - if [ -z "$LAST_TAG" ]; then - VERSION="$MANIFEST_VERSION" # first release: ship what's committed - elif [ "$BUMP" = none ]; then - RELEASE=false # no feat/fix/breaking since last tag - VERSION="${LAST_TAG#v}" - else - VERSION="$(bump "${LAST_TAG#v}" "$BUMP")" - fi - - if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then - echo "Tag v${VERSION} already exists — nothing to release." - RELEASE=false - fi - - # Derive a deterministic, monotonic versionCode from the semver. - IFS=. read -r MA MI PA <<< "$VERSION" - VERSION_CODE=$(( MA*10000 + MI*100 + PA )) - - { - echo "## Runic Gateway Android v${VERSION}" - echo - FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)" - FIXES="$(echo "$SUBJECTS" | grep -E '^(fix|perf)' || true)" - [ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; } - [ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; } - echo "### All changes" - if [ -n "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi - echo "$SUBJECTS" | sed 's/^/- /' - echo - echo "---" - echo "Signed APK — sideload on Android 10+ (§10). The app self-configures its shard site on first run." - } > dist/CHANGELOG.md - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "versionCode=${VERSION_CODE}" >> "$GITHUB_OUTPUT" - echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" - echo "release=${RELEASE}" >> "$GITHUB_OUTPUT" - echo "bump=${BUMP}" >> "$GITHUB_OUTPUT" - echo "==> release=${RELEASE} version=${VERSION} code=${VERSION_CODE} bump=${BUMP} last_tag=${LAST_TAG:-}" - - # ── ANDROID ADAPTER: SDK + signing keystore ────────────────────────── - - name: Set up Android SDK - if: ${{ steps.plan.outputs.release == 'true' }} - uses: android-actions/setup-android@v3 - - - name: Install Android SDK packages - if: ${{ steps.plan.outputs.release == 'true' }} - run: | - set +o pipefail - yes | sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0" - - - name: Cache Gradle - if: ${{ steps.plan.outputs.release == 'true' }} - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml', 'gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Decode signing keystore - if: ${{ steps.plan.outputs.release == 'true' }} - env: - ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - run: | - set -euo pipefail - if [ -z "${ANDROID_KEYSTORE_BASE64:-}" ]; then - echo "::error::ANDROID_KEYSTORE_BASE64 secret is not set — cannot build a signed release." - exit 1 - fi - printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${RUNNER_TEMP}/release.jks" - echo "ANDROID_KEYSTORE_FILE=${RUNNER_TEMP}/release.jks" >> "$GITHUB_ENV" - - # ── ANDROID ADAPTER: set the version, gate, build the signed APK ───── - - name: Set the app version to match the release - if: ${{ steps.plan.outputs.release == 'true' }} - run: | - set -euo pipefail - VERSION="${{ steps.plan.outputs.version }}" - VERSION_CODE="${{ steps.plan.outputs.versionCode }}" - # Replace only the version defaults (the `?: "x.y.z"` / `?: N` fallbacks). - sed -i -E "s/(\?: )\"[0-9]+\.[0-9]+\.[0-9]+\"/\1\"${VERSION}\"/" "${GRADLE_MODULE}/build.gradle.kts" - sed -i -E "s/(toIntOrNull\(\) \?: )[0-9]+/\1${VERSION_CODE}/" "${GRADLE_MODULE}/build.gradle.kts" - grep -nE "versionCode = |versionName = " "${GRADLE_MODULE}/build.gradle.kts" - - - name: Unit tests + signed release APK - if: ${{ steps.plan.outputs.release == 'true' }} - env: - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - run: | - set -euo pipefail - chmod +x ./gradlew - ./gradlew --no-daemon :${GRADLE_MODULE}:testDebugUnitTest :${GRADLE_MODULE}:assembleRelease - - - name: Package APK + SHA256SUMS - if: ${{ steps.plan.outputs.release == 'true' }} - run: | - set -euo pipefail - SRC="${GRADLE_MODULE}/build/outputs/apk/release/app-release.apk" - test -f "$SRC" || { echo "::error::release APK not found at $SRC"; exit 1; } - cp "$SRC" "dist/runic-gateway-${{ steps.plan.outputs.version }}.apk" - ( cd dist && sha256sum "runic-gateway-${{ steps.plan.outputs.version }}.apk" > SHA256SUMS ) - ls -l dist && cat dist/SHA256SUMS - - # ── RELEASE ENGINE: commit the bump, tag, push ─────────────────────── - - name: Commit version bump and push tag - if: ${{ steps.plan.outputs.release == 'true' }} - env: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - TAG="${{ steps.plan.outputs.tag }}" - # Secrets can arrive with a trailing newline; a stray CR/LF corrupts the - # remote URL / auth header. Strip line breaks before use. - CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')" - CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')" - git config user.name "android-app-ci" - git config user.email "ci@whitlocktech.com" - git remote set-url origin \ - "https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git" - - git add "${GRADLE_MODULE}/build.gradle.kts" - if ! git diff --cached --quiet; then - git commit -m "chore(release): bump version to ${TAG} [skip ci]" - git push origin "HEAD:main" - else - echo "Version unchanged (first release) — no bump commit needed." - fi - git tag "${TAG}" - git push origin "${TAG}" - - # ── RELEASE ENGINE: create the Gitea release + upload assets ───────── - - name: Create Gitea release and upload assets - if: ${{ steps.plan.outputs.release == 'true' }} - env: - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - TAG="${{ steps.plan.outputs.tag }}" - API="https://${GITEA_HOST}/api/v1/repos/${REPO}" - BODY="$(cat dist/CHANGELOG.md)" - CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')" - - REL_ID="$(curl -sSf -X POST "${API}/releases" \ - -H "Authorization: token ${CI_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg tag "$TAG" --arg body "$BODY" \ - '{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" \ - | jq -r '.id')" - echo "Created release ${TAG} (id=${REL_ID})" - - for f in "runic-gateway-${{ steps.plan.outputs.version }}.apk" SHA256SUMS; do - curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \ - -H "Authorization: token ${CI_TOKEN}" \ - -F "attachment=@dist/${f}" >/dev/null - echo " uploaded ${f}" - done +IyBBdXRvbWF0ZWQgYnVpbGQgKyByZWxlYXNlIGZvciB0aGUgUnVuaWMgR2F0ZXdheSBBbmRyb2lkIGFwcC4KIwojIFRyaWdnZXI6IGV2ZXJ5IHB1c2ggdG8gYG1haW5gIChpLmUuIGV2ZXJ5IG1lcmdlZCBQUikuCiMKIyBGbG93ICh0d28gY29uY2VwdHVhbCBoYWx2ZXMsIGtlcHQgc2VwYXJhdGUgb24gcHVycG9zZSkg4oCUIG1pcnJvcnMgbGluay8ncyBlbmdpbmU6CiMKIyAgIOKUjOKUgOKUgCBSRUxFQVNFIEVOR0lORSAobGFuZ3VhZ2UtYWdub3N0aWMpIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkAojICAg4pSCICByZWFkczogbGF0ZXN0IHYqIGdpdCB0YWcgKyBjb252ZW50aW9uYWwtY29tbWl0IHN1YmplY3RzICAgICAgICAgIOKUggojICAg4pSCICBwcm9kdWNlczogbmV4dCB2ZXJzaW9uLCBjaGFuZ2Vsb2csIGFuZCAoYXQgdGhlIGVuZCkgdGhlIHJlbGVhc2UgIOKUggojICAg4pSU4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYCiMgICDilIzilIDilIAgQU5EUk9JRCBBREFQVEVSICh0aGUgb25seSBBbmRyb2lkLXNwZWNpZmljIHBhcnQpIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkAojICAg4pSCICBjb25zdW1lczogdGhlIHZlcnNpb24gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIOKUggojICAg4pSCICBwcm9kdWNlczogdGhlIGFydGlmYWN0IChhIHNpZ25lZCByZWxlYXNlIEFQSyArIFNIQTI1NlNVTVMpICAgICAgIOKUggojICAg4pSU4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYCiMKIyBWZXJzaW9uIGJ1bXAgKGNvbnZlbnRpb25hbCBjb21taXRzIHNpbmNlIHRoZSBsYXN0IHYqIHRhZyk6CiMgICBmZWF0ITogLyBCUkVBS0lORyBDSEFOR0UgIC0+IG1ham9yICAgICBmZWF0OiAtPiBtaW5vciAgICAgZml4fHBlcmY6IC0+IHBhdGNoCiMgICBub3RoaW5nIHJlbGVhc2FibGUgICAgICAgIC0+IG5vIHJlbGVhc2UgaXMgY3V0CiMgICAoZmlyc3QgZXZlciBydW4sIG5vIHRhZykgIC0+IHJlbGVhc2VzIHRoZSBjdXJyZW50IGJ1aWxkLmdyYWRsZS5rdHMgdmVyc2lvbiBhcy1pcwojIHZlcnNpb25OYW1lIGlzIHRoZSBzZW12ZXI7IHZlcnNpb25Db2RlIGlzIGRlcml2ZWQgbWFqb3IqMTAwMDArbWlub3IqMTAwK3BhdGNoIHNvCiMgaXQgaXMgZGV0ZXJtaW5pc3RpYyArIG1vbm90b25pYyAoUExBTi5tZCDCpzEwKS4gVGhlIGJ1bXAgaXMgY29tbWl0dGVkIGJhY2sgdG8KIyBhcHAvYnVpbGQuZ3JhZGxlLmt0cywgdGhlbiB0YWdnZWQuCiMKIyBQcmVyZXF1aXNpdGVzIChTZXR0aW5ncyAtPiBBY3Rpb25zIC0+IFNlY3JldHMgb24gUnVuaWNHYXRld2F5L0FuZHJvaWQtYXBwKToKIyAgIFJFR0lTVFJZX1VTRVIgICDigJQgR2l0ZWEgdXNlcm5hbWUgdGhlIHRva2VuIGJlbG93IGJlbG9uZ3MgdG8KIyAgIFJFR0lTVFJZX1RPS0VOICDigJQgR2l0ZWEgYWNjZXNzIHRva2VuIHdpdGggYHdyaXRlOnJlcG9zaXRvcnlgIChwdXNoIHRoZSBidW1wCiMgICAgICAgICAgICAgICAgICAgICBjb21taXQgKyB0YWcgYW5kIGNyZWF0ZSB0aGUgcmVsZWFzZSkKIyAgIEFORFJPSURfS0VZU1RPUkVfQkFTRTY0ICAgIOKAlCBiYXNlNjQgb2YgdGhlIHJlbGVhc2UgLmprcyAoc2luZ2xlIGxpbmUpCiMgICBBTkRST0lEX0tFWVNUT1JFX1BBU1NXT1JEICDigJQga2V5c3RvcmUgcGFzc3dvcmQKIyAgIEFORFJPSURfS0VZX0FMSUFTICAgICAgICAgIOKAlCBrZXkgYWxpYXMgKGUuZy4gcnVuaWNnYXRld2F5KQojICAgQU5EUk9JRF9LRVlfUEFTU1dPUkQgICAgICAg4oCUIGtleSBwYXNzd29yZCAoPT0gc3RvcmUgcGFzc3dvcmQgZm9yIGEgUEtDUzEyIGtleXN0b3JlKQojICAgQWxzbzogYG1haW5gIG11c3QgYWNjZXB0IGEgZGlyZWN0IHB1c2ggZnJvbSB0aGUgUkVHSVNUUllfVVNFUiBhY2NvdW50IChkaXNhYmxlCiMgICBicmFuY2ggcHJvdGVjdGlvbiBmb3IgaXQsIG9yIGFkZCBpdCBhcyBhbiBleGNlcHRpb24pIOKAlCB0aGUgYnVtcCBjb21taXQgbGFuZHMgb24KIyAgIG1haW4uIFRoZSBidW1wIGNvbW1pdCBjYXJyaWVzIGBbc2tpcCBjaV1gLCBzbyBpdCBkb2VzIG5vdCByZS10cmlnZ2VyIHRoaXMgd29ya2Zsb3cuCiMKIyBSdW5uZXIgaGFuZGxpbmcgbWF0Y2hlcyBwci1jaGVja3MueW1sIChzZWxmLWhvc3RlZCBgdWJ1bnR1LWxhdGVzdGApOiB0aGUgY29udGFpbmVyCiMgbGFja3MgZ2l0L2N1cmwvdW56aXAgYW5kIGNhbid0IHJlYWNoIGFwaS5hZG9wdGl1bS5uZXQsIHNvIHdlIGFwdC1pbnN0YWxsIHRoZSBiYXNlCiMgdG9vbHMgKyBKREsgMTcgKG5vdCBhY3Rpb25zL3NldHVwLWphdmEpLCBpbnN0YWxsIHRoZSBleGFjdCBTREsgcGFja2FnZXMsIGFuZAojIGBjaG1vZCAreCAuL2dyYWRsZXdgIGluLXN0ZXAgKGNoZWNrb3V0IGRyb3BzIHRoZSBleGVjIGJpdCkuCgpuYW1lOiBSZWxlYXNlIEFQSwoKb246CiAgcHVzaDoKICAgIGJyYW5jaGVzOiBbbWFpbl0KICB3b3JrZmxvd19kaXNwYXRjaDoge30KCmNvbmN1cnJlbmN5OgogIGdyb3VwOiByZWxlYXNlLWFwawogIGNhbmNlbC1pbi1wcm9ncmVzczogZmFsc2UKCmVudjoKICBHSVRFQV9IT1NUOiBnaXRlYS53aGl0bG9ja3RlY2guY29tCiAgUkVQTzogUnVuaWNHYXRld2F5L0FuZHJvaWQtYXBwCiAgR1JBRExFX01PRFVMRTogYXBwCgpqb2JzOgogIHJlbGVhc2U6CiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICAjIEZhaWwgZmFzdCBvbiBhIGdlbnVpbmVseSB3ZWRnZWQgcnVuIChlLmcuIGEgc3RhbGxlZCBTREsvbmV0d29yayBkb3dubG9hZCBvbgogICAgIyB0aGUgc2VsZi1ob3N0ZWQgcnVubmVyKSBpbnN0ZWFkIG9mIGhhbmdpbmcgZm9yZXZlciBhbmQg4oCUIGJlY2F1c2UgY29uY3VycmVuY3kKICAgICMgaXMgYGNhbmNlbC1pbi1wcm9ncmVzczogZmFsc2VgIOKAlCBibG9ja2luZyBldmVyeSBsYXRlciByZWxlYXNlIGJlaGluZCBpdC4KICAgIHRpbWVvdXQtbWludXRlczogMzAKICAgICMgRG9uJ3QgbG9vcCBvbiBvdXIgb3duIGJ1bXAgY29tbWl0IChiZWx0LWFuZC1zdXNwZW5kZXJzIHdpdGggW3NraXAgY2ldKS4KICAgICMgUXVvdGVkIGJlY2F1c2UgdGhlIGV4cHJlc3Npb24gY29udGFpbnMgYSBjb2xvbiAoYGNob3JlKHJlbGVhc2UpOmApLCB3aGljaCBhbgogICAgIyB1bnF1b3RlZCBZQU1MIHNjYWxhciB3b3VsZCBtaXNwYXJzZSBhcyBhIG1hcHBpbmcgdmFsdWUuCiAgICBpZjogIiR7eyAhY29udGFpbnMoZ2l0aHViLmV2ZW50LmhlYWRfY29tbWl0Lm1lc3NhZ2UsICdjaG9yZShyZWxlYXNlKTogYnVtcCB2ZXJzaW9uJykgfX0iCiAgICBzdGVwczoKICAgICAgLSBuYW1lOiBJbnN0YWxsIGJhc2UgdG9vbHMgKyBKREsgMTcKICAgICAgICBydW46IHwKICAgICAgICAgIGFwdC1nZXQgdXBkYXRlCiAgICAgICAgICBhcHQtZ2V0IGluc3RhbGwgLXkgZ2l0IGN1cmwgdW56aXAganEgb3Blbmpkay0xNy1qZGstaGVhZGxlc3MKICAgICAgICAgIGVjaG8gIkpBVkFfSE9NRT0vdXNyL2xpYi9qdm0vamF2YS0xNy1vcGVuamRrLWFtZDY0IiA+PiAiJEdJVEhVQl9FTlYiCgogICAgICAtIG5hbWU6IENoZWNrIG91dCBmdWxsIGhpc3RvcnkgKG5lZWQgdGFncyArIGNvbW1pdCBsb2cgZm9yIHRoZSBidW1wKQogICAgICAgIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAdjQKICAgICAgICB3aXRoOgogICAgICAgICAgZmV0Y2gtZGVwdGg6IDAKCiAgICAgICMg4pSA4pSAIFJFTEVBU0UgRU5HSU5FOiBkZWNpZGUgdGhlIG5leHQgdmVyc2lvbiArIGNoYW5nZWxvZyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgICAgLSBuYW1lOiBQbGFuIHRoZSByZWxlYXNlICh2ZXJzaW9uICsgY2hhbmdlbG9nKQogICAgICAgIGlkOiBwbGFuCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBzZXQgLWV1byBwaXBlZmFpbAogICAgICAgICAgbWtkaXIgLXAgZGlzdAogICAgICAgICAgZ2l0IGZldGNoIC0tdGFncyAtLWZvcmNlID4vZGV2L251bGwgMj4mMSB8fCB0cnVlCgogICAgICAgICAgIyBDdXJyZW50IGNvbW1pdHRlZCB2ZXJzaW9uICh0aGUgYD86ICJ4LnkueiJgIGRlZmF1bHQgaW4gYnVpbGQuZ3JhZGxlLmt0cykuCiAgICAgICAgICBNQU5JRkVTVF9WRVJTSU9OPSIkKHNlZCAtbkUgJ3MvLipcPzogIihbMC05XStcLlswLTldK1wuWzAtOV0rKSIuKi9cMS9wJyAiJHtHUkFETEVfTU9EVUxFfS9idWlsZC5ncmFkbGUua3RzIiB8IGhlYWQgLTEpIgogICAgICAgICAgTEFTVF9UQUc9IiQoZ2l0IGRlc2NyaWJlIC0tdGFncyAtLW1hdGNoICd2KicgLS1hYmJyZXY9MCAyPi9kZXYvbnVsbCB8fCB0cnVlKSIKICAgICAgICAgIGlmIFsgLW4gIiRMQVNUX1RBRyIgXTsgdGhlbiBSQU5HRT0iJHtMQVNUX1RBR30uLkhFQUQiOyBlbHNlIFJBTkdFPSJIRUFEIjsgZmkKCiAgICAgICAgICBTVUJKRUNUUz0iJChnaXQgbG9nIC0tbm8tbWVyZ2VzIC0tZm9ybWF0PSclcycgJFJBTkdFIHx8IHRydWUpIgogICAgICAgICAgQk9ESUVTPSIkKGdpdCBsb2cgLS1uby1tZXJnZXMgLS1mb3JtYXQ9JyVCJyAkUkFOR0UgfHwgdHJ1ZSkiCgogICAgICAgICAgQlVNUD1ub25lCiAgICAgICAgICBpZiBlY2hvICIkQk9ESUVTIiAgIHwgZ3JlcCAtcUUgJ0JSRUFLSU5HWyAtXUNIQU5HRScgICAgICAgICAgOyB0aGVuIEJVTVA9bWFqb3I7IGZpCiAgICAgICAgICBpZiBlY2hvICIkU1VCSkVDVFMiIHwgZ3JlcCAtcUUgJ15bYS16XSsoXChbXildK1wpKT8hOicgICAgICAgOyB0aGVuIEJVTVA9bWFqb3I7IGZpCiAgICAgICAgICBpZiBbICIkQlVNUCIgPSBub25lIF0gJiYgZWNobyAiJFNVQkpFQ1RTIiB8IGdyZXAgLXFFICdeZmVhdChcKFteKV0rXCkpPzonICAgICAgOyB0aGVuIEJVTVA9bWlub3I7IGZpCiAgICAgICAgICBpZiBbICIkQlVNUCIgPSBub25lIF0gJiYgZWNobyAiJFNVQkpFQ1RTIiB8IGdyZXAgLXFFICdeKGZpeHxwZXJmKShcKFteKV0rXCkpPzonOyB0aGVuIEJVTVA9cGF0Y2g7IGZpCgogICAgICAgICAgYnVtcCgpIHsgIyA8eC55Lno+IDxtYWpvcnxtaW5vcnxwYXRjaD4gLT4gYnVtcGVkCiAgICAgICAgICAgIElGUz0uIHJlYWQgLXIgTUEgTUkgUEEgPDw8ICIkMSIKICAgICAgICAgICAgY2FzZSAiJDIiIGluCiAgICAgICAgICAgICAgbWFqb3IpIGVjaG8gIiQoKE1BKzEpKS4wLjAiIDs7CiAgICAgICAgICAgICAgbWlub3IpIGVjaG8gIiR7TUF9LiQoKE1JKzEpKS4wIiA7OwogICAgICAgICAgICAgIHBhdGNoKSBlY2hvICIke01BfS4ke01JfS4kKChQQSsxKSkiIDs7CiAgICAgICAgICAgIGVzYWMKICAgICAgICAgIH0KCiAgICAgICAgICBSRUxFQVNFPXRydWUKICAgICAgICAgIGlmIFsgLXogIiRMQVNUX1RBRyIgXTsgdGhlbgogICAgICAgICAgICBWRVJTSU9OPSIkTUFOSUZFU1RfVkVSU0lPTiIgICAgICAgICAgICAgICMgZmlyc3QgcmVsZWFzZTogc2hpcCB3aGF0J3MgY29tbWl0dGVkCiAgICAgICAgICBlbGlmIFsgIiRCVU1QIiA9IG5vbmUgXTsgdGhlbgogICAgICAgICAgICBSRUxFQVNFPWZhbHNlICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgbm8gZmVhdC9maXgvYnJlYWtpbmcgc2luY2UgbGFzdCB0YWcKICAgICAgICAgICAgVkVSU0lPTj0iJHtMQVNUX1RBRyN2fSIKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgVkVSU0lPTj0iJChidW1wICIke0xBU1RfVEFHI3Z9IiAiJEJVTVAiKSIKICAgICAgICAgIGZpCgogICAgICAgICAgaWYgZ2l0IHJldi1wYXJzZSAtcSAtLXZlcmlmeSAicmVmcy90YWdzL3Yke1ZFUlNJT059IiA+L2Rldi9udWxsOyB0aGVuCiAgICAgICAgICAgIGVjaG8gIlRhZyB2JHtWRVJTSU9OfSBhbHJlYWR5IGV4aXN0cyDigJQgbm90aGluZyB0byByZWxlYXNlLiIKICAgICAgICAgICAgUkVMRUFTRT1mYWxzZQogICAgICAgICAgZmkKCiAgICAgICAgICAjIERlcml2ZSBhIGRldGVybWluaXN0aWMsIG1vbm90b25pYyB2ZXJzaW9uQ29kZSBmcm9tIHRoZSBzZW12ZXIuCiAgICAgICAgICBJRlM9LiByZWFkIC1yIE1BIE1JIFBBIDw8PCAiJFZFUlNJT04iCiAgICAgICAgICBWRVJTSU9OX0NPREU9JCgoIE1BKjEwMDAwICsgTUkqMTAwICsgUEEgKSkKCiAgICAgICAgICB7CiAgICAgICAgICAgIGVjaG8gIiMjIFJ1bmljIEdhdGV3YXkgQW5kcm9pZCB2JHtWRVJTSU9OfSIKICAgICAgICAgICAgZWNobwogICAgICAgICAgICBGRUFUUz0iJChlY2hvICIkU1VCSkVDVFMiIHwgZ3JlcCAtRSAnXmZlYXQnICAgICAgICB8fCB0cnVlKSIKICAgICAgICAgICAgRklYRVM9IiQoZWNobyAiJFNVQkpFQ1RTIiB8IGdyZXAgLUUgJ14oZml4fHBlcmYpJyAgfHwgdHJ1ZSkiCiAgICAgICAgICAgIFsgLW4gIiRGRUFUUyIgXSAmJiB7IGVjaG8gIiMjIyBGZWF0dXJlcyI7ICBlY2hvICIkRkVBVFMiIHwgc2VkICdzL14vLSAvJzsgZWNobzsgfQogICAgICAgICAgICBbIC1uICIkRklYRVMiIF0gJiYgeyBlY2hvICIjIyMgRml4ZXMiOyAgICAgZWNobyAiJEZJWEVTIiB8IHNlZCAncy9eLy0gLyc7IGVjaG87IH0KICAgICAgICAgICAgZWNobyAiIyMjIEFsbCBjaGFuZ2VzIgogICAgICAgICAgICBpZiBbIC1uICIkTEFTVF9UQUciIF07IHRoZW4gZWNobyAiU2luY2UgJHtMQVNUX1RBR306IjsgZmkKICAgICAgICAgICAgZWNobyAiJFNVQkpFQ1RTIiB8IHNlZCAncy9eLy0gLycKICAgICAgICAgICAgZWNobwogICAgICAgICAgICBlY2hvICItLS0iCiAgICAgICAgICAgIGVjaG8gIlNpZ25lZCBBUEsg4oCUIHNpZGVsb2FkIG9uIEFuZHJvaWQgMTArICjCpzEwKS4gVGhlIGFwcCBzZWxmLWNvbmZpZ3VyZXMgaXRzIHNoYXJkIHNpdGUgb24gZmlyc3QgcnVuLiIKICAgICAgICAgIH0gPiBkaXN0L0NIQU5HRUxPRy5tZAoKICAgICAgICAgIGVjaG8gInZlcnNpb249JHtWRVJTSU9OfSIgICAgICAgICAgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAidmVyc2lvbkNvZGU9JHtWRVJTSU9OX0NPREV9IiA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICJ0YWc9diR7VkVSU0lPTn0iICAgICAgICAgICAgID4+ICIkR0lUSFVCX09VVFBVVCIKICAgICAgICAgIGVjaG8gInJlbGVhc2U9JHtSRUxFQVNFfSIgICAgICAgICAgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAiYnVtcD0ke0JVTVB9IiAgICAgICAgICAgICAgICA+PiAiJEdJVEhVQl9PVVRQVVQiCiAgICAgICAgICBlY2hvICI9PT4gcmVsZWFzZT0ke1JFTEVBU0V9IHZlcnNpb249JHtWRVJTSU9OfSBjb2RlPSR7VkVSU0lPTl9DT0RFfSBidW1wPSR7QlVNUH0gbGFzdF90YWc9JHtMQVNUX1RBRzotPG5vbmU+fSIKCiAgICAgICMg4pSA4pSAIEFORFJPSUQgQURBUFRFUjogU0RLICsgc2lnbmluZyBrZXlzdG9yZSDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgICAgLSBuYW1lOiBTZXQgdXAgQW5kcm9pZCBTREsKICAgICAgICBpZjogJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy5yZWxlYXNlID09ICd0cnVlJyB9fQogICAgICAgIHVzZXM6IGFuZHJvaWQtYWN0aW9ucy9zZXR1cC1hbmRyb2lkQHYzCiAgICAgICAgd2l0aDoKICAgICAgICAgICMgT25seSBwdXQgY21kbGluZS10b29scyBvbiBQQVRILiBUaGUgYWN0aW9uJ3MgZGVmYXVsdCBwYWNrYWdlIHNldCBkcmFncyBpbgogICAgICAgICAgIyB0aGUgd2hvbGUgZW11bGF0b3IgKyB0aGUgbGVnYWN5IGB0b29sc2AgcGFja2FnZSAoaHVuZHJlZHMgb2YgTUIsIG5ldHdvcmstCiAgICAgICAgICAjIGJvdW5kIG9uIHRoaXMgcnVubmVyKSB0aGF0IGEgaGVhZGxlc3MgQVBLIGJ1aWxkIG5ldmVyIHVzZXMuIFRoZSBuZXh0IHN0ZXAKICAgICAgICAgICMgaW5zdGFsbHMgZXhhY3RseSB0aGUgcGFja2FnZXMgd2UgbmVlZC4KICAgICAgICAgIHBhY2thZ2VzOiAnJwoKICAgICAgLSBuYW1lOiBJbnN0YWxsIEFuZHJvaWQgU0RLIHBhY2thZ2VzCiAgICAgICAgaWY6ICR7eyBzdGVwcy5wbGFuLm91dHB1dHMucmVsZWFzZSA9PSAndHJ1ZScgfX0KICAgICAgICBydW46IHwKICAgICAgICAgIHNldCArbyBwaXBlZmFpbAogICAgICAgICAgeWVzIHwgc2RrbWFuYWdlciAicGxhdGZvcm0tdG9vbHMiICJwbGF0Zm9ybXM7YW5kcm9pZC0zNSIgImJ1aWxkLXRvb2xzOzM1LjAuMCIKCiAgICAgIC0gbmFtZTogRGVjb2RlIHNpZ25pbmcga2V5c3RvcmUKICAgICAgICBpZjogJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy5yZWxlYXNlID09ICd0cnVlJyB9fQogICAgICAgIGVudjoKICAgICAgICAgIEFORFJPSURfS0VZU1RPUkVfQkFTRTY0OiAke3sgc2VjcmV0cy5BTkRST0lEX0tFWVNUT1JFX0JBU0U2NCB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgc2V0IC1ldW8gcGlwZWZhaWwKICAgICAgICAgIGlmIFsgLXogIiR7QU5EUk9JRF9LRVlTVE9SRV9CQVNFNjQ6LX0iIF07IHRoZW4KICAgICAgICAgICAgZWNobyAiOjplcnJvcjo6QU5EUk9JRF9LRVlTVE9SRV9CQVNFNjQgc2VjcmV0IGlzIG5vdCBzZXQg4oCUIGNhbm5vdCBidWlsZCBhIHNpZ25lZCByZWxlYXNlLiIKICAgICAgICAgICAgZXhpdCAxCiAgICAgICAgICBmaQogICAgICAgICAgcHJpbnRmICclcycgIiRBTkRST0lEX0tFWVNUT1JFX0JBU0U2NCIgfCBiYXNlNjQgLWQgPiAiJHtSVU5ORVJfVEVNUH0vcmVsZWFzZS5qa3MiCiAgICAgICAgICBlY2hvICJBTkRST0lEX0tFWVNUT1JFX0ZJTEU9JHtSVU5ORVJfVEVNUH0vcmVsZWFzZS5qa3MiID4+ICIkR0lUSFVCX0VOViIKCiAgICAgICMg4pSA4pSAIEFORFJPSUQgQURBUFRFUjogc2V0IHRoZSB2ZXJzaW9uLCBnYXRlLCBidWlsZCB0aGUgc2lnbmVkIEFQSyDilIDilIDilIDilIDilIAKICAgICAgLSBuYW1lOiBTZXQgdGhlIGFwcCB2ZXJzaW9uIHRvIG1hdGNoIHRoZSByZWxlYXNlCiAgICAgICAgaWY6ICR7eyBzdGVwcy5wbGFuLm91dHB1dHMucmVsZWFzZSA9PSAndHJ1ZScgfX0KICAgICAgICBydW46IHwKICAgICAgICAgIHNldCAtZXVvIHBpcGVmYWlsCiAgICAgICAgICBWRVJTSU9OPSIke3sgc3RlcHMucGxhbi5vdXRwdXRzLnZlcnNpb24gfX0iCiAgICAgICAgICBWRVJTSU9OX0NPREU9IiR7eyBzdGVwcy5wbGFuLm91dHB1dHMudmVyc2lvbkNvZGUgfX0iCiAgICAgICAgICAjIFJlcGxhY2Ugb25seSB0aGUgdmVyc2lvbiBkZWZhdWx0cyAodGhlIGA/OiAieC55LnoiYCAvIGA/OiBOYCBmYWxsYmFja3MpLgogICAgICAgICAgc2VkIC1pIC1FICJzLyhcPzogKVwiWzAtOV0rXC5bMC05XStcLlswLTldK1wiL1wxXCIke1ZFUlNJT059XCIvIiAiJHtHUkFETEVfTU9EVUxFfS9idWlsZC5ncmFkbGUua3RzIgogICAgICAgICAgc2VkIC1pIC1FICJzLyh0b0ludE9yTnVsbFwoXCkgXD86IClbMC05XSsvXDEke1ZFUlNJT05fQ09ERX0vIiAgICAgIiR7R1JBRExFX01PRFVMRX0vYnVpbGQuZ3JhZGxlLmt0cyIKICAgICAgICAgIGdyZXAgLW5FICJ2ZXJzaW9uQ29kZSA9IHx2ZXJzaW9uTmFtZSA9ICIgIiR7R1JBRExFX01PRFVMRX0vYnVpbGQuZ3JhZGxlLmt0cyIKCiAgICAgIC0gbmFtZTogVW5pdCB0ZXN0cyArIHNpZ25lZCByZWxlYXNlIEFQSwogICAgICAgIGlmOiAke3sgc3RlcHMucGxhbi5vdXRwdXRzLnJlbGVhc2UgPT0gJ3RydWUnIH19CiAgICAgICAgZW52OgogICAgICAgICAgQU5EUk9JRF9LRVlTVE9SRV9QQVNTV09SRDogJHt7IHNlY3JldHMuQU5EUk9JRF9LRVlTVE9SRV9QQVNTV09SRCB9fQogICAgICAgICAgQU5EUk9JRF9LRVlfQUxJQVM6ICR7eyBzZWNyZXRzLkFORFJPSURfS0VZX0FMSUFTIH19CiAgICAgICAgICBBTkRST0lEX0tFWV9QQVNTV09SRDogJHt7IHNlY3JldHMuQU5EUk9JRF9LRVlfUEFTU1dPUkQgfX0KICAgICAgICBydW46IHwKICAgICAgICAgIHNldCAtZXVvIHBpcGVmYWlsCiAgICAgICAgICBjaG1vZCAreCAuL2dyYWRsZXcKICAgICAgICAgIC4vZ3JhZGxldyAtLW5vLWRhZW1vbiA6JHtHUkFETEVfTU9EVUxFfTp0ZXN0RGVidWdVbml0VGVzdCA6JHtHUkFETEVfTU9EVUxFfTphc3NlbWJsZVJlbGVhc2UKCiAgICAgIC0gbmFtZTogUGFja2FnZSBBUEsgKyBTSEEyNTZTVU1TCiAgICAgICAgaWY6ICR7eyBzdGVwcy5wbGFuLm91dHB1dHMucmVsZWFzZSA9PSAndHJ1ZScgfX0KICAgICAgICBydW46IHwKICAgICAgICAgIHNldCAtZXVvIHBpcGVmYWlsCiAgICAgICAgICBTUkM9IiR7R1JBRExFX01PRFVMRX0vYnVpbGQvb3V0cHV0cy9hcGsvcmVsZWFzZS9hcHAtcmVsZWFzZS5hcGsiCiAgICAgICAgICB0ZXN0IC1mICIkU1JDIiB8fCB7IGVjaG8gIjo6ZXJyb3I6OnJlbGVhc2UgQVBLIG5vdCBmb3VuZCBhdCAkU1JDIjsgZXhpdCAxOyB9CiAgICAgICAgICBjcCAiJFNSQyIgImRpc3QvcnVuaWMtZ2F0ZXdheS0ke3sgc3RlcHMucGxhbi5vdXRwdXRzLnZlcnNpb24gfX0uYXBrIgogICAgICAgICAgKCBjZCBkaXN0ICYmIHNoYTI1NnN1bSAicnVuaWMtZ2F0ZXdheS0ke3sgc3RlcHMucGxhbi5vdXRwdXRzLnZlcnNpb24gfX0uYXBrIiA+IFNIQTI1NlNVTVMgKQogICAgICAgICAgbHMgLWwgZGlzdCAmJiBjYXQgZGlzdC9TSEEyNTZTVU1TCgogICAgICAjIOKUgOKUgCBSRUxFQVNFIEVOR0lORTogY29tbWl0IHRoZSBidW1wLCB0YWcsIHB1c2gg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICAgIC0gbmFtZTogQ29tbWl0IHZlcnNpb24gYnVtcCBhbmQgcHVzaCB0YWcKICAgICAgICBpZjogJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy5yZWxlYXNlID09ICd0cnVlJyB9fQogICAgICAgIGVudjoKICAgICAgICAgIFJFR0lTVFJZX1VTRVI6ICR7eyBzZWNyZXRzLlJFR0lTVFJZX1VTRVIgfX0KICAgICAgICAgIFJFR0lTVFJZX1RPS0VOOiAke3sgc2VjcmV0cy5SRUdJU1RSWV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgc2V0IC1ldW8gcGlwZWZhaWwKICAgICAgICAgIFRBRz0iJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy50YWcgfX0iCiAgICAgICAgICAjIFNlY3JldHMgY2FuIGFycml2ZSB3aXRoIGEgdHJhaWxpbmcgbmV3bGluZTsgYSBzdHJheSBDUi9MRiBjb3JydXB0cyB0aGUKICAgICAgICAgICMgcmVtb3RlIFVSTCAvIGF1dGggaGVhZGVyLiBTdHJpcCBsaW5lIGJyZWFrcyBiZWZvcmUgdXNlLgogICAgICAgICAgQ0lfVVNFUj0iJChwcmludGYgJyVzJyAiJHtSRUdJU1RSWV9VU0VSfSIgfCB0ciAtZCAnXHJcbicpIgogICAgICAgICAgQ0lfVE9LRU49IiQocHJpbnRmICclcycgIiR7UkVHSVNUUllfVE9LRU59IiB8IHRyIC1kICdcclxuJykiCiAgICAgICAgICBnaXQgY29uZmlnIHVzZXIubmFtZSAgImFuZHJvaWQtYXBwLWNpIgogICAgICAgICAgZ2l0IGNvbmZpZyB1c2VyLmVtYWlsICJjaUB3aGl0bG9ja3RlY2guY29tIgogICAgICAgICAgZ2l0IHJlbW90ZSBzZXQtdXJsIG9yaWdpbiBcCiAgICAgICAgICAgICJodHRwczovLyR7Q0lfVVNFUn06JHtDSV9UT0tFTn1AJHtHSVRFQV9IT1NUfS8ke1JFUE99LmdpdCIKCiAgICAgICAgICBnaXQgYWRkICIke0dSQURMRV9NT0RVTEV9L2J1aWxkLmdyYWRsZS5rdHMiCiAgICAgICAgICBpZiAhIGdpdCBkaWZmIC0tY2FjaGVkIC0tcXVpZXQ7IHRoZW4KICAgICAgICAgICAgZ2l0IGNvbW1pdCAtbSAiY2hvcmUocmVsZWFzZSk6IGJ1bXAgdmVyc2lvbiB0byAke1RBR30gW3NraXAgY2ldIgogICAgICAgICAgICBnaXQgcHVzaCBvcmlnaW4gIkhFQUQ6bWFpbiIKICAgICAgICAgIGVsc2UKICAgICAgICAgICAgZWNobyAiVmVyc2lvbiB1bmNoYW5nZWQgKGZpcnN0IHJlbGVhc2UpIOKAlCBubyBidW1wIGNvbW1pdCBuZWVkZWQuIgogICAgICAgICAgZmkKICAgICAgICAgIGdpdCB0YWcgIiR7VEFHfSIKICAgICAgICAgIGdpdCBwdXNoIG9yaWdpbiAiJHtUQUd9IgoKICAgICAgIyDilIDilIAgUkVMRUFTRSBFTkdJTkU6IGNyZWF0ZSB0aGUgR2l0ZWEgcmVsZWFzZSArIHVwbG9hZCBhc3NldHMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICAgIC0gbmFtZTogQ3JlYXRlIEdpdGVhIHJlbGVhc2UgYW5kIHVwbG9hZCBhc3NldHMKICAgICAgICBpZjogJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy5yZWxlYXNlID09ICd0cnVlJyB9fQogICAgICAgIGVudjoKICAgICAgICAgIFJFR0lTVFJZX1RPS0VOOiAke3sgc2VjcmV0cy5SRUdJU1RSWV9UT0tFTiB9fQogICAgICAgIHJ1bjogfAogICAgICAgICAgc2V0IC1ldW8gcGlwZWZhaWwKICAgICAgICAgIFRBRz0iJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy50YWcgfX0iCiAgICAgICAgICBBUEk9Imh0dHBzOi8vJHtHSVRFQV9IT1NUfS9hcGkvdjEvcmVwb3MvJHtSRVBPfSIKICAgICAgICAgIEJPRFk9IiQoY2F0IGRpc3QvQ0hBTkdFTE9HLm1kKSIKICAgICAgICAgIENJX1RPS0VOPSIkKHByaW50ZiAnJXMnICIke1JFR0lTVFJZX1RPS0VOfSIgfCB0ciAtZCAnXHJcbicpIgoKICAgICAgICAgIFJFTF9JRD0iJChjdXJsIC1zU2YgLVggUE9TVCAiJHtBUEl9L3JlbGVhc2VzIiBcCiAgICAgICAgICAgIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke0NJX1RPS0VOfSIgXAogICAgICAgICAgICAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uIiBcCiAgICAgICAgICAgIC1kICIkKGpxIC1uIC0tYXJnIHRhZyAiJFRBRyIgLS1hcmcgYm9keSAiJEJPRFkiIFwKICAgICAgICAgICAgICAgICAgICd7dGFnX25hbWU6JHRhZywgbmFtZTokdGFnLCBib2R5OiRib2R5LCBkcmFmdDpmYWxzZSwgcHJlcmVsZWFzZTpmYWxzZX0nKSIgXAogICAgICAgICAgICB8IGpxIC1yICcuaWQnKSIKICAgICAgICAgIGVjaG8gIkNyZWF0ZWQgcmVsZWFzZSAke1RBR30gKGlkPSR7UkVMX0lEfSkiCgogICAgICAgICAgZm9yIGYgaW4gInJ1bmljLWdhdGV3YXktJHt7IHN0ZXBzLnBsYW4ub3V0cHV0cy52ZXJzaW9uIH19LmFwayIgU0hBMjU2U1VNUzsgZG8KICAgICAgICAgICAgY3VybCAtc1NmIC1YIFBPU1QgIiR7QVBJfS9yZWxlYXNlcy8ke1JFTF9JRH0vYXNzZXRzP25hbWU9JHtmfSIgXAogICAgICAgICAgICAgIC1IICJBdXRob3JpemF0aW9uOiB0b2tlbiAke0NJX1RPS0VOfSIgXAogICAgICAgICAgICAgIC1GICJhdHRhY2htZW50PUBkaXN0LyR7Zn0iID4vZGV2L251bGwKICAgICAgICAgICAgZWNobyAiICB1cGxvYWRlZCAke2Z9IgogICAgICAgICAgZG9uZQo= \ No newline at end of file