Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .env.docker.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
COMPOSE_PROJECT_NAME=opencli-admin
DOCKER_REGISTRY=ghcr.io/
DOCKER_IMAGE_NAMESPACE=2233admin
IMAGE_TAG=0.4.0

FRONTEND_PORT=3010
API_PORT=8031
PUBLIC_URL=http://localhost:8031

# Required. The installers generate all four values automatically.
API_AUTH_TOKEN=
BOOTSTRAP_ADMIN_TOKEN=
SECRET_KEY=
CREDENTIAL_ENCRYPTION_KEY=

DATABASE_URL=sqlite+aiosqlite:////data/opencli_admin.db
TASK_EXECUTOR=local
COLLECTION_MODE=local
DEBUG=false

# Interactive browser is available at http://localhost:6080.
OPENCLI_CDP_ENDPOINT=http://agent-1:19222
NOVNC_PORT=6080
NOVNC_BASE_PORT=6080

# Remote-agent defaults.
AGENT_MODE=bridge
OPENCLI_DAEMON_PORT=19825
CHROME_SUFFIX=
82 changes: 44 additions & 38 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,10 @@ on:
pull_request:
workflow_dispatch:

jobs:
frontend:
runs-on: ubuntu-latest
name: Frontend (Next.js)
defaults:
run:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 11

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm exec tsc --noEmit

# TODO: add pnpm test once a frontend test suite exists

- name: Lint
run: pnpm run lint

- name: Build
run: pnpm run build
permissions:
contents: read

jobs:
extension:
runs-on: ubuntu-latest
name: Browser Extension
Expand Down Expand Up @@ -99,8 +66,8 @@ jobs:
with:
python-version: "3.13"

- name: Install workflow contract dependency
run: python -m pip install "pydantic>=2.10.0"
- name: Install backend compiler dependencies
run: python -m pip install -e ..

- name: Install dependencies
run: |
Expand All @@ -126,6 +93,45 @@ jobs:
- name: Browser smoke test
run: pnpm test:smoke

release-contract:
runs-on: ubuntu-latest
name: Public Install Smoke
env:
API_AUTH_TOKEN: ci-release-token
BOOTSTRAP_ADMIN_TOKEN: ci-bootstrap-admin-token
SECRET_KEY: ci-release-secret
CREDENTIAL_ENCRYPTION_KEY: MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=
COMPOSE_PROJECT_NAME: opencli-admin-ci
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Validate Compose
run: docker compose --env-file .env.docker.example -f docker-compose.yml -f docker-compose.build.yml config --quiet

- name: Build and start public stack
run: docker compose --env-file .env.docker.example -f docker-compose.yml -f docker-compose.build.yml up -d --build --wait api frontend agent-1

- name: Verify public endpoints
run: |
curl --fail --silent --show-error http://localhost:3010/login >/dev/null
curl --fail --silent --show-error http://localhost:8031/health >/dev/null
curl --fail --silent --show-error \
-H "Authorization: Bearer ci-bootstrap-admin-token" \
-H "X-API-Token: ci-release-token" \
http://localhost:8031/api/v1/auth/me |
python -c 'import json,sys; assert json.load(sys.stdin)["data"]["subject"] == "bootstrap-admin"'

- name: Show logs on failure
if: failure()
run: docker compose --env-file .env.docker.example -f docker-compose.yml -f docker-compose.build.yml logs --tail=200 api frontend agent-1

- name: Stop public stack
if: always()
run: docker compose --env-file .env.docker.example -f docker-compose.yml -f docker-compose.build.yml down -v

backend:
runs-on: ubuntu-latest
name: Backend Quality
Expand Down
129 changes: 129 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: release

on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
images:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- name: API image
image: opencli-admin-api
context: .
file: Dockerfile
build_args: |
IMAGE_TAG=__VERSION__
suffix: ""
- name: Frontend image
image: opencli-admin-frontend
context: ./frontend
file: ./frontend/Dockerfile
build_args: |
BACKEND_URL=http://api:8000
suffix: ""
- name: Agent image
image: opencli-admin-agent
context: .
file: ./agent/Dockerfile
build_args: ""
suffix: ""
- name: Interactive Chrome image
image: opencli-admin-chrome
context: .
file: ./chrome/Dockerfile
build_args: ""
suffix: ""
- name: Agent Chrome image
image: opencli-admin-agent
context: .
file: ./agent/Dockerfile
suffix: "-chrome"
build_args: |
INSTALL_CHROME=true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Resolve release version
id: version
shell: bash
run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Resolve build arguments
id: build_args
shell: bash
env:
MATRIX_BUILD_ARGS: ${{ matrix.build_args }}
RELEASE_VERSION: ${{ steps.version.outputs.value }}
run: |
{
echo "value<<EOF"
printf '%s\n' "$MATRIX_BUILD_ARGS" | sed "s/__VERSION__/$RELEASE_VERSION/g"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
platforms: linux/amd64,linux/arm64
push: true
build-args: ${{ steps.build_args.outputs.value }}
tags: |
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ steps.version.outputs.value }}${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:latest${{ matrix.suffix }}
cache-from: type=gha,scope=${{ matrix.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.name }}
provenance: mode=max
sbom: true

github-release:
name: GitHub Release
runs-on: ubuntu-latest
needs: images
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Create release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
docker-compose.yml
docker-compose.build.yml
.env.docker.example
scripts/install.sh
scripts/install.ps1
32 changes: 5 additions & 27 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,22 @@ FROM ${REGISTRY}python:3.13-slim AS builder

WORKDIR /app

# Switch to Aliyun apt mirror for faster downloads in China
RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true

# Install build deps
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# Install Python deps into a prefix so we can copy them cleanly
COPY pyproject.toml .
RUN pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/ && \
pip install --prefix=/install . -i https://mirrors.aliyun.com/pypi/simple/
RUN pip install --upgrade pip \
&& pip install --prefix=/install .

# ── Stage 2: runtime ──────────────────────────────────────────────────────────
ARG REGISTRY=
FROM ${REGISTRY}python:3.13-slim AS runtime

WORKDIR /app

# Switch to Aliyun apt mirror for faster downloads in China
RUN sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's|http://deb.debian.org|http://mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true

# Runtime system deps (psycopg2 needs libpq, opencli needs Node.js 22+)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl ca-certificates git \
Expand All @@ -44,23 +36,13 @@ RUN npm install -g @jackwener/opencli@${OPENCLI_VERSION} \
&& rm /tmp/patch-opencli.js \
&& rm -rf /root/.npm

ARG OHMYOPENCLI_REPO=https://github.com/2233admin/OhMyOpenCLI.git
ARG OHMYOPENCLI_COMMIT=73cc60c83586ef2c95469b3b70d6cfc80fa5bc53
ARG OFFICIAL_SITE_CAPABILITY_COMMIT=73cc60c83586ef2c95469b3b70d6cfc80fa5bc53
RUN git clone ${OHMYOPENCLI_REPO} /opt/ohmyopencli \
&& cd /opt/ohmyopencli \
&& git checkout --detach ${OHMYOPENCLI_COMMIT} \
&& git merge-base --is-ancestor ${OFFICIAL_SITE_CAPABILITY_COMMIT} HEAD \
&& npm ci \
&& test "$(git rev-parse HEAD)" = "${OHMYOPENCLI_COMMIT}"

# Copy installed packages from builder
COPY --from=builder /install /usr/local

# Copy application source
COPY backend/ ./backend/
COPY scripts/patch-opencli.js ./scripts/patch-opencli.js
COPY scripts/verify_managed_opencli_runtime.py ./scripts/verify_managed_opencli_runtime.py
COPY scripts/install-agent.sh ./scripts/install-agent.sh
COPY alembic.ini .

# Entrypoint handles migrations
Expand All @@ -70,16 +52,12 @@ RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
# Non-root user for security; pre-create /data so the SQLite volume is writable
RUN useradd -m -u 1000 appuser && \
mkdir -p /data && \
chown -R appuser:appuser /app /data /opt/ohmyopencli \
&& cd /opt/ohmyopencli \
&& HOME=/home/appuser npm run bootstrap \
&& chown -R appuser:appuser /home/appuser/.opencli /opt/ohmyopencli
chown -R appuser:appuser /app /data
USER appuser

ENV PYTHONPATH=/app \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
OHMYOPENCLI_ROOT=/opt/ohmyopencli
PYTHONUNBUFFERED=1
# Bake the image tag so the system config API can serve it to clients.
ARG IMAGE_TAG=latest
ENV IMAGE_TAG=${IMAGE_TAG}
Expand Down
Loading
Loading