Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 30 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@ RUN groupadd --gid $USER_GID $USERNAME \
# monorepo root.
RUN pip install --no-cache-dir uv

# ---------- Simple Secrets Manager (ssm / ssm-cli) ----------
# The workspace agent is launched through `ssm run`, so an image without it on
# PATH boots the agent straight into exit 127. Installed here (Tier 0) rather
# than init.d because it is toolchain, not project state.
#
# All three UV_* paths are load-bearing, because this installs as root while
# the agent runs as $USERNAME:
# BIN_DIR — uv's default (~/.local/bin) is NOT on a non-login shell's
# PATH, and the agent pane runs `sh -c`, so a default install
# is present and still unfindable (exit 127).
# TOOL_DIR — the shim is a symlink INTO the tool venv; left at the default
# it points inside /root and the agent gets exit 126.
# PYTHON_... — the venv's interpreter is likewise fetched under /root by
# default, so the venv resolves to an unreadable python.
# `a+rX` then grants traverse/read without marking data files executable. The
# python dir is only created when uv had to FETCH an interpreter; this image
# already ships one it can reuse, so the loop skips what does not exist rather
# than failing the build on it.
#
# The null keyring backend is required at RUNTIME, not just here: a container
# has no secret service, and keyring's search for one hangs a headless client.
# The CLI reads SSM_BASE_URL/SSM_TOKEN from the environment, which the
# workspace supervisor already injects — so no credential file is baked in.
ENV PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
RUN UV_PYTHON_INSTALL_DIR=/opt/uv-python \
UV_TOOL_DIR=/opt/uv-tools \
UV_TOOL_BIN_DIR=/usr/local/bin \
uv tool install git+https://github.com/bearlike/Simple-Secrets-Manager.git \
&& for d in /opt/uv-python /opt/uv-tools; do [ -d "$d" ] && chmod -R a+rX "$d"; done

# ---------- Shared cache mount points ----------
# Named volumes are mounted here by devcontainer.json. Create and chown them at
# build time: a volume is created empty and root-owned on first use, and the
Expand Down
11 changes: 11 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ services:
init: true
command: sleep infinity

# Compose does NOT add host.docker.internal for you the way a plain
# `docker run` on Linux does with `--add-host=…:host-gateway` — this is
# that same mapping, spelled for Compose. It is what lets an agent in here
# reach the host's own grove-mcp (networked MCP transport, :7431) by a
# name that also resolves for a host-side Claude session, so ONE .mcp.json
# URL serves both. `host-gateway` is Docker's own sentinel, resolved to
# whatever bridge gateway this container actually gets — never hardcode
# the IP, a fresh network is minted per workspace.
extra_hosts:
- "host.docker.internal:host-gateway"

volumes:
# Relative to THIS file, so it resolves correctly in any git worktree
# rather than being pinned to one checkout path.
Expand Down
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
# API authentication token — generate a strong random string.
MEWBO_MASTER_API_TOKEN=CHANGE_ME

# Published image source. Set a private registry as git.example.com/bearlike.
MEWBO_REGISTRY=ghcr.io/bearlike
MEWBO_TAG=latest

# Exposed ports (host networking)
MEWBO_API_PORT=5125

Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/agent-pickup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,14 @@ jobs:
shell: bash
run: |
set -euo pipefail
# A self-hosted forge behind a private CA is invisible to curl's system
# trust store: the runner exports that CA for node and for git, and
# nothing else. Trust the CA narrowly; AGENT_TLS_NO_VERIFY remains an
# explicit escape hatch only when it is configured.
curl_flags=()
if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -r "${NODE_EXTRA_CA_CERTS}" ]; then
curl_flags+=(--cacert "${NODE_EXTRA_CA_CERTS}")
fi
case "${AGENT_TLS_NO_VERIFY,,}" in true|1|yes) curl_flags+=(-k) ;; esac
# Gitea act_runner may leave github.api_url empty; derive it.
api="$API_URL"
Expand Down Expand Up @@ -163,6 +170,9 @@ jobs:
# pull_request events carry head/base inline; comment- and
# dispatch-triggered PR pickups must look them up.
curl_flags=()
if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -r "${NODE_EXTRA_CA_CERTS}" ]; then
curl_flags+=(--cacert "${NODE_EXTRA_CA_CERTS}")
fi
case "${AGENT_TLS_NO_VERIFY,,}" in true|1|yes) curl_flags+=(-k) ;; esac
if [[ "${ITEM_IS_PR:-false}" == "true" && -z "${HEAD_REF:-}" ]]; then
pr=$(curl "${curl_flags[@]}" --fail-with-body --silent --show-error \
Expand All @@ -179,6 +189,9 @@ jobs:
run: |
set -euo pipefail
curl_flags=()
if [ -n "${NODE_EXTRA_CA_CERTS:-}" ] && [ -r "${NODE_EXTRA_CA_CERTS}" ]; then
curl_flags+=(--cacert "${NODE_EXTRA_CA_CERTS}")
fi
case "${AGENT_TLS_NO_VERIFY,,}" in true|1|yes) curl_flags+=(-k) ;; esac
provider="gitea"
[[ "$SERVER_URL" == "https://github.com" ]] && provider="github"
Expand Down
142 changes: 142 additions & 0 deletions .github/workflows/android-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#@doc
# * PR gate for Mewbo Aura (apps/mewbo_aura): lints and unit-tests the `public`
# distribution flavor ONLY. The `enterprise` flavor needs a private CA cert
# that CI does not have (see android-release.yml) — never build/test it here.
#
# One job, not two: `:app:lintPublicDebug` and `:app:testPublicDebugUnitTest`
# would otherwise provision JDK/SDK into the SAME persistent
# `/opt/hostedtoolcache` volume in parallel, which is exactly the race
# android-release.yml's concurrency comment documents (colliding unzip/mv →
# mangled repo metadata → "Failed to find package"). Sequential steps in one
# job reuse the already-provisioned toolcache for the second step for free.

name: Android CI

on:
pull_request:
paths:
- "apps/mewbo_aura/**"
- ".github/workflows/android-ci.yml"
workflow_dispatch:

permissions:
contents: read

# TWO axes, and the outer one is not about this PR at all.
#
# Per-PR cancellation is what you want for signal (a new commit supersedes the
# old run), but it CANNOT be the concurrency group: every Android job on this
# runner provisions into the same persistent /opt/hostedtoolcache volume, and
# two concurrent provisioners race on the SDK dir — colliding unzip/mv, mangled
# repo metadata, "Failed to find package". That is why android-release.yml
# serializes on ONE global group rather than per-tag; a per-PR group here would
# re-open the same race between two PRs, and between a PR and a release.
#
# So: share the release workflow's global group. Two Android runs never overlap.
# cancel-in-progress stays FALSE — a PR run must never kill a release build.
#
# ⚠️ The cost is NOT "concurrent PRs queue" — it is sharper than that. A group
# holds at most ONE pending run, so with a release building and PR A waiting, PR
# B's arrival CANCELS A. A cancelled check is neither a pass nor a failure, so
# nobody re-runs it; the PR simply has no Android signal until someone notices.
# Accepted deliberately: PR volume here is low, and a missing gate you can see is
# better than the toolcache race, which corrupts the SDK for every later run.
# Re-push to re-trigger. If this starts biting, the fix is a per-PR group plus a
# lockfile around the provisioning step only — NOT dropping the shared group.
concurrency:
group: android-toolcache
cancel-in-progress: false

defaults:
run:
working-directory: apps/mewbo_aura

jobs:
lint-and-test:
name: Lint + unit test (public flavor)
runs-on: ubuntu-22.04
timeout-minutes: 30
env:
# Same persistent toolcache volume as android-release.yml, so a JDK/SDK
# already provisioned by a release run (or an earlier CI run) is reused
# instead of re-downloaded.
ANDROID_HOME: /opt/hostedtoolcache/android-sdk
GRADLE_USER_HOME: /opt/hostedtoolcache/gradle-home
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Provision Temurin JDK 21 (persistent toolcache, skipped when present)
run: |
set -euo pipefail
JDK_DIR=/opt/hostedtoolcache/temurin-21-jdk
if [ ! -x "$JDK_DIR/bin/java" ]; then
curl -fsSL -o /tmp/jdk.tar.gz "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jdk/hotspot/normal/eclipse"
mkdir -p "$JDK_DIR"
tar -xzf /tmp/jdk.tar.gz -C "$JDK_DIR" --strip-components=1
rm /tmp/jdk.tar.gz
fi
echo "JAVA_HOME=$JDK_DIR" >> "$GITHUB_ENV"
echo "$JDK_DIR/bin" >> "$GITHUB_PATH"

- name: Provision Android SDK (persistent toolcache, skipped when present)
run: |
set -euo pipefail
if [ ! -d "$ANDROID_HOME/platforms/android-37.0" ] || [ ! -d "$ANDROID_HOME/build-tools/37.0.0" ]; then
mkdir -p "$ANDROID_HOME/cmdline-tools"
cd "$ANDROID_HOME/cmdline-tools"
rm -rf latest cmdline-tools.zip
curl -fsSL -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
unzip -q cmdline-tools.zip
mv cmdline-tools latest
rm cmdline-tools.zip
(yes || true) | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" --licenses >/dev/null
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" "platform-tools" "platforms;android-37.0" "build-tools;37.0.0"
fi

# Both Gradle invocations below cap the JVM the same way android-release.yml
# does, and for the same measured reason: the project's gradle.properties
# asks for -Xmx4096m, while a job container here is capped at 2.5 GiB with
# no swap, so the daemon is OOM-killed by construction and the build
# reports only "Gradle build daemon disappeared unexpectedly". The jvmargs
# value is quoted as ONE argument because it contains a space — unquoted,
# Gradle receives `-XX:...` as its own flag and rejects it.
- name: Lint (public flavor)
run: |
set -euo pipefail
./gradlew :app:lintPublicDebug \
"-Dorg.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8" \
-Dkotlin.compiler.execution.strategy=in-process \
-Dorg.gradle.caching=true \
--no-daemon --max-workers=2

- name: Unit tests (public flavor; test sources are flavor-agnostic)
run: |
set -euo pipefail
./gradlew :app:testPublicDebugUnitTest \
"-Dorg.gradle.jvmargs=-Xmx1536m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8" \
-Dkotlin.compiler.execution.strategy=in-process \
-Dorg.gradle.caching=true \
--no-daemon --max-workers=2

- name: Upload lint report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: android-lint-report
path: apps/mewbo_aura/app/build/reports/lint-results-publicDebug.html
retention-days: 7
# warn, not ignore: a drifted report path must be visible, not a silent green.
if-no-files-found: warn

- name: Upload unit test results on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: android-unit-test-results
path: |
apps/mewbo_aura/app/build/reports/tests/testPublicDebugUnitTest
apps/mewbo_aura/app/build/test-results/testPublicDebugUnitTest
retention-days: 7
# warn, not ignore: a drifted report path must be visible, not a silent green.
if-no-files-found: warn
Loading
Loading