Implement session group cloning #2108
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [main, master, dev, 'repro/**'] | |
| tags: ['v*'] | |
| pull_request: | |
| branches: [main, master] | |
| env: | |
| NODE_VERSION_PRIMARY: '24' | |
| NODE_VERSION_SERVER: '22' | |
| jobs: | |
| # ── Lint ────────────────────────────────────────────────────────────────── | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: npm run lint | |
| # ── Typecheck (daemon + server) ─────────────────────────────────────────── | |
| typecheck: | |
| name: Typecheck | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - name: Install server deps | |
| run: ./scripts/ci-npm-ci.sh server | |
| - name: Daemon | |
| run: npx tsc --noEmit | |
| - name: Server | |
| run: npx tsc --noEmit | |
| working-directory: server | |
| # ── Secret scanning ──────────────────────────────────────────────────────── | |
| secret-scan: | |
| name: Secret Scan (gitleaks) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ── Daemon unit tests ───────────────────────────────────────────────────── | |
| unit-tests: | |
| name: Unit Tests (Node ${{ matrix.node }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| node: ['22', '24'] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: npm run build | |
| - run: npm run test:unit | |
| preview-dist-smoke: | |
| name: Preview Dist Smoke | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: npm run test:preview-dist | |
| embedding-real-tests: | |
| name: Embedding Integration Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - name: Cache embedding model | |
| uses: actions/cache@v4 | |
| with: | |
| path: .cache/imcodes-embeddings | |
| key: embedding-${{ runner.os }}-${{ env.NODE_VERSION_PRIMARY }}-${{ hashFiles('package-lock.json', 'shared/embedding-config.ts') }} | |
| - name: Run real transformers.js embedding integration test | |
| run: npx vitest run test/context/embedding-real.test.ts | |
| env: | |
| RUN_REAL_EMBEDDING_TESTS: '1' | |
| IMCODES_EMBEDDING_CACHE_DIR: ${{ github.workspace }}/.cache/imcodes-embeddings | |
| # ── macOS daemon unit tests ───────────────────────────────────────────── | |
| macos-unit-tests: | |
| name: Unit Tests (macOS) | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - name: Install tmux | |
| run: brew install tmux | |
| - name: Prime tmux server | |
| run: tmux new-session -d -s init && tmux kill-session -t init | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: npm run build | |
| - run: npm run test:unit | |
| # ── Windows unit tests (WezTerm backend) ────────────────────────────────── | |
| windows-unit-tests: | |
| name: Unit Tests (Windows) | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: bash ./scripts/ci-npm-ci.sh . | |
| - run: npm run build | |
| - name: Run Windows-specific unit tests | |
| run: npx vitest run test/agent/wezterm.test.ts test/daemon/hook-send.test.ts test/daemon/env-injection.test.ts test/cli/send.test.ts test/util/windows-daemon.test.ts test/util/windows-upgrade-script.test.ts test/util/windows-upgrade-runner.test.ts test/util/windows-launch-artifacts.test.ts test/util/windows-launch-artifacts.cmd-parse.test.ts test/util/postinstall-sharp-repair.test.ts test/util/sharp-repair-script.test.ts test/util/restart-daemon-cmd.test.ts | |
| env: | |
| IMCODES_MUX: wezterm | |
| - name: Run Windows process-cleanup regression tests | |
| run: npx vitest run test/util/windows-stale-watchdog-cleanup.test.ts | |
| env: | |
| IMCODES_MUX: wezterm | |
| windows-conpty-tests: | |
| name: Unit Tests (Windows ConPTY) | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: bash ./scripts/ci-npm-ci.sh . | |
| - run: npm run build | |
| - name: Run Windows ConPTY / startup regression tests | |
| run: npx vitest run test/agent/conpty.test.ts test/agent/drivers/drivers.test.ts test/util/windows-daemon.test.ts test/util/windows-upgrade-script.test.ts test/util/windows-upgrade-runner.test.ts test/util/windows-launch-artifacts.test.ts test/util/windows-launch-artifacts.cmd-parse.test.ts test/util/sharp-repair-script.test.ts test/util/restart-daemon-cmd.test.ts | |
| - name: Run Windows ConPTY process-cleanup regression tests | |
| run: npx vitest run test/util/windows-stale-watchdog-cleanup.test.ts | |
| # ── Web frontend tests ──────────────────────────────────────────────────── | |
| web-tests-unit: | |
| name: Web Tests (Unit) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: ./scripts/ci-npm-ci.sh web | |
| - run: cd web && npx vitest run --config vitest.unit.config.ts | |
| web-tests-components: | |
| name: Web Tests (Components) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: ./scripts/ci-npm-ci.sh web | |
| - run: cd web && npx vitest run --config vitest.components.config.ts | |
| # FileBrowser component test skipped in CI (OOM — renders full 1300-line component in jsdom). | |
| # Run locally: cd web && npx vitest run --config vitest.filebrowser.config.ts | |
| # ── Server unit tests ───────────────────────────────────────────────────── | |
| server-tests: | |
| name: Server Unit Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_SERVER }} | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: ./scripts/ci-npm-ci.sh server | |
| - run: npm run test:server | |
| - name: Run server-native tests (auth-flow, proxy-addr — require server/node_modules) | |
| run: npm test | |
| working-directory: server | |
| - name: Build server for runtime check | |
| run: npm run build | |
| working-directory: server | |
| - name: Runtime import smoke test (catches import path errors that tsc misses) | |
| run: node -e "import('./dist/server/src/index.js').then(() => { console.log('Server module loaded OK'); process.exit(0); }).catch(e => { console.error('IMPORT FAILED:', e.message); process.exit(1); })" | |
| working-directory: server | |
| # ── Server DB integration tests (testcontainers + real PostgreSQL) ───────── | |
| server-db-tests: | |
| name: Server DB Integration Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| cache-dependency-path: server/package-lock.json | |
| - run: npm ci | |
| working-directory: server | |
| - name: Run DB integration tests | |
| run: npm run test:integration | |
| working-directory: server | |
| env: | |
| TESTCONTAINERS_RYUK_DISABLED: 'true' | |
| # ── E2E tests (tmux installed — tests run for real) ────────────────────── | |
| e2e-tests: | |
| name: E2E Tests | |
| runs-on: ubuntu-latest | |
| needs: [unit-tests] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - name: Install tmux | |
| run: sudo apt-get install -y tmux | |
| - name: Prime tmux server (ensures socket dir exists) | |
| run: tmux new-session -d -s init && tmux kill-session -t init | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - name: Install web deps (active timeline refresh e2e wrapper invokes web vitest) | |
| run: ./scripts/ci-npm-ci.sh web | |
| - name: Run pipe-pane e2e tests | |
| run: npx vitest run test/e2e/pipe-pane-stream.test.ts | |
| - name: Run other e2e tests | |
| run: npm run test:e2e | |
| # ── Repo provider integration tests (gh/glab against public repos) ──────── | |
| repo-integration-tests: | |
| name: Repo Provider Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: [unit-tests] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - name: Install & authenticate glab (optional — tests skip if unavailable) | |
| continue-on-error: true | |
| run: | | |
| curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v1.89.0/downloads/glab_1.89.0_linux_amd64.deb" -o glab.deb | |
| sudo dpkg -i glab.deb | |
| if [ -n "$GITLAB_TOKEN" ]; then | |
| glab auth login --hostname gitlab.com --token "$GITLAB_TOKEN" | |
| fi | |
| env: | |
| GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} | |
| - name: Run integration tests | |
| run: npm run test:integration | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ── Coverage ────────────────────────────────────────────────────────────── | |
| coverage: | |
| name: Coverage Report | |
| runs-on: ubuntu-latest | |
| needs: [unit-tests, web-tests-unit, web-tests-components] | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| # tmux is no longer needed here: `test:coverage` skips the e2e project | |
| # (which spawns real tmux + agent processes), saving ~1 min per run. | |
| # | |
| # `npm run build` IS still needed: although most tests resolve from | |
| # `src/` via vitest's tsx transform, two suites assert against the | |
| # built output and FAIL without `dist/`: | |
| # - test/packaging.test.ts (verifies bin/main/files paths) | |
| # - test/util/postinstall-sharp-repair.test.ts (executes dist/.../postinstall-sharp-repair.js) | |
| # Both run in the `daemon` project, which is included in coverage. They | |
| # pass in the regular Unit Tests jobs because those run `npm run build` | |
| # first; coverage must do the same. | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - name: Install web deps (needed for tsx component tests) | |
| run: ./scripts/ci-npm-ci.sh web | |
| - name: Install server deps (needed for server route tests) | |
| run: ./scripts/ci-npm-ci.sh server | |
| - run: npm run build | |
| - run: npm run test:coverage | |
| - name: Upload to Codecov | |
| if: ${{ env.CODECOV_TOKEN != '' }} | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| token: ${{ env.CODECOV_TOKEN }} | |
| fail_ci_if_error: false | |
| - name: Skip Codecov upload when token is unavailable | |
| if: ${{ env.CODECOV_TOKEN == '' }} | |
| run: echo "Skipping Codecov upload because CODECOV_TOKEN is not configured for this workflow context." | |
| - name: Comment PR with coverage diff | |
| if: github.event_name == 'pull_request' | |
| uses: davelosert/vitest-coverage-report-action@v2 | |
| # ── Publish to npm ──────────────────────────────────────────────────────── | |
| publish: | |
| name: Publish to npm | |
| runs-on: ubuntu-latest | |
| needs: [docker] | |
| if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: 'npm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Remove deprecated always-auth from setup-node npmrc | |
| run: | | |
| if [ -n "${NPM_CONFIG_USERCONFIG:-}" ] && [ -f "$NPM_CONFIG_USERCONFIG" ]; then | |
| sed -i '/^always-auth=/d' "$NPM_CONFIG_USERCONFIG" | |
| fi | |
| - run: npm install -g [email protected] | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - name: Install web deps | |
| run: ./scripts/ci-npm-ci.sh web | |
| - name: Install server deps | |
| run: ./scripts/ci-npm-ci.sh server | |
| - run: npm run build | |
| - name: Set version | |
| run: npm version ${{ needs.docker.outputs.npm_version }} --no-git-tag-version | |
| - name: Publish (skip if version already on npm) | |
| run: | | |
| VERSION=${{ needs.docker.outputs.npm_version }} | |
| if npm view "imcodes@${VERSION}" version >/dev/null 2>&1; then | |
| echo "::warning::Version ${VERSION} already published — skipping" | |
| elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then | |
| npm publish --tag dev --provenance --access public | |
| else | |
| npm publish --provenance --access public | |
| fi | |
| # ── Build & push Docker image ───────────────────────────────────────────── | |
| docker: | |
| name: Docker Build & Push | |
| runs-on: ubuntu-latest | |
| needs: [lint, typecheck, secret-scan, unit-tests, macos-unit-tests, windows-unit-tests, windows-conpty-tests, web-tests-unit, web-tests-components, server-tests, server-db-tests, e2e-tests] | |
| if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' | |
| outputs: | |
| npm_version: ${{ steps.version_meta.outputs.npm_version }} | |
| permissions: | |
| contents: write | |
| packages: write | |
| env: | |
| IMAGE: ghcr.io/im4codes/imcodes | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # full history needed for git rev-list --count | |
| - name: Get build timestamp | |
| id: ts | |
| run: echo "value=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Resolve tags | |
| id: tags | |
| run: | | |
| set -euo pipefail | |
| DATE_TAG="v$(date -u +%Y.%-m.%-d)" | |
| SHA_SHORT="${GITHUB_SHA::7}" | |
| if [ "${GITHUB_REF}" = "refs/heads/dev" ]; then | |
| TAGS="${IMAGE}:dev" | |
| TAGS="${TAGS},${IMAGE}:dev-${DATE_TAG#v}" | |
| TAGS="${TAGS},${IMAGE}:dev-${DATE_TAG#v}-${SHA_SHORT}" | |
| TAGS="${TAGS},${IMAGE}:dev-sha-${SHA_SHORT}" | |
| else | |
| TAGS="${IMAGE}:latest" | |
| TAGS="${TAGS},${IMAGE}:${DATE_TAG}" | |
| TAGS="${TAGS},${IMAGE}:${DATE_TAG}-${SHA_SHORT}" | |
| TAGS="${TAGS},${IMAGE}:sha-${SHA_SHORT}" | |
| fi | |
| echo "tags=${TAGS}" >> "$GITHUB_OUTPUT" | |
| echo "date_tag=${DATE_TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Get OTA version (commit count) and app version (semver) | |
| id: ota | |
| run: | | |
| COMMIT_COUNT=$(( $(git rev-list --count HEAD) + 620 )) | |
| echo "version=${COMMIT_COUNT}" >> "$GITHUB_OUTPUT" | |
| echo "app_version=$(date -u +%Y.%-m).${COMMIT_COUNT}" >> "$GITHUB_OUTPUT" | |
| - name: Resolve publish version | |
| id: version_meta | |
| run: | | |
| set -euo pipefail | |
| COMMIT_COUNT=$(( $(git rev-list --count HEAD) + 620 )) | |
| if [ "${GITHUB_REF}" = "refs/heads/dev" ]; then | |
| NPM_VERSION="$(date -u +%Y).$(date -u +%-m).${COMMIT_COUNT}-dev.${GITHUB_RUN_NUMBER}" | |
| else | |
| NPM_VERSION="$(date -u +%Y.%-m).${COMMIT_COUNT}" | |
| fi | |
| echo "npm_version=${NPM_VERSION}" >> "$GITHUB_OUTPUT" | |
| # Build the image into the local docker daemon first so we can smoke-test | |
| # it BEFORE pushing. Without this step, an image whose Node process | |
| # crashes on startup (e.g. ERR_MODULE_NOT_FOUND from a missing prod dep) | |
| # would still get pushed and auto-deployed to production. | |
| - name: Build (load to local docker for smoke test) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: server/Dockerfile | |
| platforms: linux/amd64 | |
| tags: imcodes-smoke:test | |
| build-args: | | |
| BUILD_TIME=${{ steps.ts.outputs.value }} | |
| OTA_VERSION=${{ steps.ota.outputs.version }} | |
| APP_VERSION=${{ steps.version_meta.outputs.npm_version }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| load: true | |
| push: false | |
| - name: Container startup smoke test | |
| run: | | |
| set -euo pipefail | |
| # Override entrypoint to run an import-only check inside the actual | |
| # production image. index.ts has an isMain guard so the import | |
| # resolves all static deps (including all routes/* modules) without | |
| # binding ports or hitting the database. Any ERR_MODULE_NOT_FOUND or | |
| # other top-level eval failure surfaces here, before the image ships. | |
| docker run --rm --entrypoint node imcodes-smoke:test \ | |
| -e "import('./dist/server/src/index.js').then(() => { console.log('OK: image loads cleanly'); process.exit(0); }).catch(e => { console.error('FAIL:', e.message); console.error(e.stack); process.exit(1); })" | |
| - name: Build and push (cache hit — only pushes layers) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: server/Dockerfile | |
| platforms: linux/amd64 | |
| tags: ${{ steps.tags.outputs.tags }} | |
| build-args: | | |
| BUILD_TIME=${{ steps.ts.outputs.value }} | |
| OTA_VERSION=${{ steps.ota.outputs.version }} | |
| APP_VERSION=${{ steps.version_meta.outputs.npm_version }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| push: true | |
| - name: Create git tag | |
| id: git_tag | |
| if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git fetch --tags | |
| NPM_VERSION="${{ steps.version_meta.outputs.npm_version }}" | |
| GIT_TAG="v${NPM_VERSION}" | |
| # Skip if this exact tag already exists (idempotent) | |
| if ! git rev-parse "$GIT_TAG" >/dev/null 2>&1; then | |
| git tag "$GIT_TAG" | |
| git push origin "$GIT_TAG" | |
| fi | |
| # ── Android APK Build ──────────────────────────────────────────────────── | |
| android-release: | |
| name: Android Release Build | |
| runs-on: ubuntu-latest | |
| needs: [lint, typecheck, secret-scan, unit-tests, web-tests-unit, web-tests-components, server-tests, e2e-tests] | |
| if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' | |
| permissions: | |
| contents: write | |
| env: | |
| KEYSTORE_FILE: ${{ github.workspace }}/web/android/app/release.keystore | |
| KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION_PRIMARY }} | |
| cache: npm | |
| cache-dependency-path: | | |
| package-lock.json | |
| web/package-lock.json | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - uses: android-actions/setup-android@v3 | |
| - name: Install Android SDK packages | |
| run: | | |
| yes | sdkmanager --licenses | |
| sdkmanager "platforms;android-36" "build-tools;36.0.0" "platform-tools" | |
| - run: ./scripts/ci-npm-ci.sh . | |
| - run: npm ci | |
| working-directory: web | |
| - name: Restore optional Android secrets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${GOOGLE_SERVICES_JSON_BASE64:-}" ]; then | |
| printf '%s' "$GOOGLE_SERVICES_JSON_BASE64" | base64 --decode > web/android/app/google-services.json | |
| echo "Decoded google-services.json from secret." | |
| else | |
| echo "GOOGLE_SERVICES_JSON_BASE64 not set; Firebase push notifications will remain disabled in this build." | |
| fi | |
| if [ -n "${ANDROID_KEYSTORE_BASE64:-}" ]; then | |
| printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > web/android/app/release.keystore | |
| echo "Decoded Android keystore from secret." | |
| else | |
| echo "ANDROID_KEYSTORE_BASE64 not set; assembleRelease will build without production signing." | |
| fi | |
| env: | |
| GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} | |
| ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| - name: Build web assets | |
| working-directory: web | |
| run: npm run build | |
| - name: Sync Capacitor Android project | |
| working-directory: web | |
| run: npx cap sync android | |
| - name: Build Android release APK + AAB | |
| working-directory: web/android | |
| run: ./gradlew assembleRelease bundleRelease --stacktrace | |
| - name: Upload release APK artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: imcodes-android-release-apk | |
| path: web/android/app/build/outputs/apk/release/*.apk | |
| if-no-files-found: error | |
| - name: Upload release AAB artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: imcodes-android-release-aab | |
| path: web/android/app/build/outputs/bundle/release/*.aab | |
| if-no-files-found: error | |
| - name: Get version info | |
| id: version | |
| run: | | |
| VERSION=$(grep versionName web/android/app/build.gradle | head -1 | sed 's/.*"\(.*\)".*/\1/') | |
| COMMIT_SHORT=$(git rev-parse --short HEAD) | |
| echo "tag=android-v${VERSION}-${COMMIT_SHORT}" >> "$GITHUB_OUTPUT" | |
| echo "name=Android v${VERSION} (${COMMIT_SHORT})" >> "$GITHUB_OUTPUT" | |
| - name: Rename artifacts with version | |
| run: | | |
| APK=$(ls web/android/app/build/outputs/apk/release/*.apk | head -1) | |
| cp "$APK" "imcodes-${{ steps.version.outputs.tag }}.apk" | |
| AAB=$(ls web/android/app/build/outputs/bundle/release/*.aab | head -1) | |
| cp "$AAB" "imcodes-${{ steps.version.outputs.tag }}.aab" | |
| - name: Create GitHub Release | |
| if: github.ref == 'refs/heads/master' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: ${{ steps.version.outputs.name }} | |
| files: | | |
| imcodes-*.apk | |
| imcodes-*.aab | |
| generate_release_notes: true | |
| make_latest: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |