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
30 changes: 30 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Build context for the dashboard image (see Dockerfile). Keep it close to .gitignore so the image
# is built from what a clean checkout contains, and never from local secrets or build output.
.git
.github
.husky/_
.vscode
**/node_modules
**/.turbo
**/dist
**/.output
**/.nuxt
**/.vercel
**/.vitehub
**/.maizzle
**/.data
**/.code-zero
**/coverage
**/playwright-report
**/test-results
**/*.tsbuildinfo
**/*.log
.skilld
.env
.env.*
**/.env
**/.env.*
!**/.env.example
code-zero.deployment.yml
Dockerfile
.dockerignore
172 changes: 172 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
name: cd

# Builds the dashboard container image from the root Dockerfile and publishes it to the GitHub
# Container Registry, modelled on wolfstar-project/.github's reusable-publish-image workflow:
# each platform builds natively on its own runner and pushes by digest, then one job merges the
# digests into a single multi-arch manifest list. The resulting image runs on Docker, Railway, or
# any other container platform (see the Dockerfile for the runtime contract).

on:
push:
branches:
- main
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions: {}

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
name: 🐳 Build ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read # checkout repository
packages: write # push image layers to ghcr.io
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Prepare
env:
PLATFORM: ${{ matrix.platform }}
run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV"

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# A tag builds the `release` channel only when it is exactly `v<version>` of the dashboard
# package, so a stray or stale tag cannot publish an image that reports a different release.
- name: Resolve build environment
env:
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
if [[ "$REF_TYPE" == "tag" ]]; then
version="$(node --print "require('./apps/dashboard/package.json').version")"
if [[ "$REF_NAME" != "v$version" ]]; then
echo "::error::Tag $REF_NAME does not match apps/dashboard version v$version" >&2
exit 1
fi
build_env=release
elif [[ "$REF_NAME" == "$DEFAULT_BRANCH" ]]; then
build_env=canary
else
build_env=preview
fi
Comment thread
RedStar071 marked this conversation as resolved.
echo "BUILD_ENV=$build_env" >> "$GITHUB_ENV"

- name: Extract metadata (labels) for Docker
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push by digest
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
CODE_ZERO_BUILD_COMMIT=${{ github.sha }}
CODE_ZERO_BUILD_BRANCH=${{ github.ref_name }}
CODE_ZERO_BUILD_ENV=${{ env.BUILD_ENV }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true

- name: Export digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p "$RUNNER_TEMP/digests"
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

merge:
name: 📦 Create and push manifest list
runs-on: ubuntu-24.04
timeout-minutes: 10
needs: build
permissions:
packages: write # push the manifest list to ghcr.io
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

# `latest` follows the default branch, so a Railway or Docker service pointed at
# `ghcr.io/<owner>/<repo>:latest` tracks `main`; release tags add semver tags alongside.
- name: Docker meta
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=long,prefix=

- name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
run: |
# shellcheck disable=SC2046
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "${IMAGE}@sha256:%s " *)

- name: Inspect image
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
VERSION: ${{ steps.meta.outputs.version }}
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
19 changes: 10 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,15 +175,16 @@ aube run build

## CI/CD

| Workflow | Purpose | Trigger |
| ---------------------------- | --------------------------------------------------------- | ------------------------------- |
| `ci.yaml` | Lint, repository metadata, typecheck, tests, build, i18n | PR, push to `main`, merge group |
| `autofix.yml` | Pushes formatting and lint fixes back to the pull request | PR, merge group |
| `zizmor.yaml` | Static analysis of GitHub Actions workflows | PR, push to `main`, merge group |
| `semantic-pull-requests.yml` | Validates PR titles against Conventional Commits | PR opened, edited, synchronized |
| `release.yaml` | Validates release artifacts | Manual dispatch |
| `labelsync.yml` | Syncs repository labels | Daily schedule, manual dispatch |
| `stale.yml` | Marks and closes stale issues and pull requests | Daily schedule, manual dispatch |
| Workflow | Purpose | Trigger |
| ---------------------------- | --------------------------------------------------------- | ------------------------------------- |
| `ci.yaml` | Lint, repository metadata, typecheck, tests, build, i18n | PR, push to `main`, merge group |
| `cd.yml` | Builds and publishes the dashboard image to GHCR | Push to `main`, `vX.Y.Z` tags, manual |
| `autofix.yml` | Pushes formatting and lint fixes back to the pull request | PR, merge group |
| `zizmor.yaml` | Static analysis of GitHub Actions workflows | PR, push to `main`, merge group |
| `semantic-pull-requests.yml` | Validates PR titles against Conventional Commits | PR opened, edited, synchronized |
| `release.yaml` | Validates release artifacts | Manual dispatch |
| `labelsync.yml` | Syncs repository labels | Daily schedule, manual dispatch |
| `stale.yml` | Marks and closes stale issues and pull requests | Daily schedule, manual dispatch |

## Pull requests

Expand Down
72 changes: 72 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# syntax=docker/dockerfile:1

# Container image for the single deployable app, `apps/dashboard`.
#
# The build stage installs the workspace with the pinned aube version and builds the dashboard
# (and the workspace packages it depends on) through Turborepo, with the self-hosted `node`
# ViteHub preset. That emits Nitro's self-contained `.output/` bundle, which is the only thing
# the runtime stage copies: no workspace sources, no dev dependencies, no package manager.
#
# The image listens on `$PORT` (default 3000), so it runs unchanged on Docker, Railway, and any
# other container platform that injects the port it routes to.

ARG NODE_VERSION=24.19.0

FROM node:${NODE_VERSION}-bookworm-slim AS build

ARG AUBE_VERSION=1.41.0

# Build metadata published under `runtimeConfig.public.buildInfo` (see packages/build-env). The
# checkout's `.git` is not part of the build context, so CI passes these in explicitly.
ARG CODE_ZERO_BUILD_COMMIT=""
ARG CODE_ZERO_BUILD_BRANCH=""
ARG CODE_ZERO_BUILD_URL=""
ARG CODE_ZERO_BUILD_PRODUCTION_URL=""
ARG CODE_ZERO_BUILD_ENV=""

# `CI=true` skips the Husky install in the `prepare` script and keeps tools non-interactive.
ENV CI=true \
HUSKY=0 \
NITRO_PRESET=node-server \
CODE_ZERO_BUILD_COMMIT=${CODE_ZERO_BUILD_COMMIT} \
CODE_ZERO_BUILD_BRANCH=${CODE_ZERO_BUILD_BRANCH} \
CODE_ZERO_BUILD_URL=${CODE_ZERO_BUILD_URL} \
CODE_ZERO_BUILD_PRODUCTION_URL=${CODE_ZERO_BUILD_PRODUCTION_URL} \
CODE_ZERO_BUILD_ENV=${CODE_ZERO_BUILD_ENV}

RUN npm install --global --ignore-scripts=false "@endevco/aube@${AUBE_VERSION}"

WORKDIR /workspace

COPY . .

RUN aube ci \
&& aube exec turbo run build --filter=@code-zero/dashboard

FROM node:${NODE_VERSION}-bookworm-slim AS runtime

# The runner boundary clones and inspects target repositories, so the image ships git and the CA
# bundle it needs for HTTPS remotes.
RUN apt-get update \
&& apt-get install --yes --no-install-recommends ca-certificates git tini \
&& rm -rf /var/lib/apt/lists/*
Comment thread
RedStar071 marked this conversation as resolved.

ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=3000

WORKDIR /app

COPY --from=build --chown=node:node /workspace/apps/dashboard/.output ./.output

# `fs-lite` KV keeps task history under `.data/kv` relative to the working directory. Mount a
# volume at /app/.data to keep it across restarts. No `VOLUME` instruction on purpose: Railway
# rejects images that declare one and attaches its own volumes instead.
RUN mkdir -p /app/.data && chown node:node /app/.data

USER node

EXPOSE 3000

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", ".output/server/index.mjs"]
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,40 @@ model:

Issue-to-PR work is opt-in twice: `issues.enabled` must be true and the issue must carry the `issues.requireLabel` label, so arbitrary issue text can never start a run. Issue text is untrusted input for the runtime to validate — never instructions. The run first decides from repository evidence whether the issue actually reports a real problem, and (unless `issues.validationComment` is disabled) posts that verdict back on the issue: confirmed with its evidence, not confirmed with every rejection reason, or inconclusive for a human. A pull request is opened only when the run completed, its changes were applied, and every repository check passed. Verified changes are published to a fresh `issues.branchPrefix` branch (never force-updated, never the default branch), and the pull request body is the run's evidence: acceptance criteria, plan, checks, and lifecycle.

### Container image

The root `Dockerfile` builds the dashboard with the self-hosted `node` preset and ships only the
`.output/` bundle on `node:24-bookworm-slim`, with `git` for the runner and `tini` as PID 1. The
`cd` workflow publishes it as a multi-arch (`linux/amd64`, `linux/arm64`) image to
`ghcr.io/wolfstar-project/code-zero`: `latest` and `main` follow the default branch, every build is
also tagged with its full commit SHA, and `vX.Y.Z` tags matching the dashboard version add `X.Y.Z` and `X.Y`.

```bash
docker build -t code-zero .
docker run --rm -p 3000:3000 --env-file apps/dashboard/.env \
-v code-zero-data:/app/.data \
-v "$PWD/code-zero.deployment.yml:/app/code-zero.deployment.yml:ro" \
code-zero
```

The server listens on `$PORT` (default `3000`), so Railway and other platforms that inject a port
work without extra configuration: point a Railway service at the GHCR image, or let it build the
repository's `Dockerfile` directly. Runtime configuration is the environment described above. The
published image is built with the default auth policy, so its sign-in pages are labelled for
sign-up and GitHub OAuth being off; the server still enforces whatever policy the runtime
environment sets. Apply migrations against `DATABASE_URL` with
`aube run db:migrate` from a checkout before the first start. Task history lives in
`/app/.data`; mount a volume there to keep it across restarts (on Railway, a volume mounted as
root needs `RAILWAY_RUN_UID=0`, since the image runs as the unprivileged `node` user).

The image carries only `.output/`, so the deployment policy has to be supplied: mount
`code-zero.deployment.yml` at `/app/code-zero.deployment.yml` as above, or mount it elsewhere and
point `CODE_ZERO_CONFIG` at it. Without it the process falls back to the closed defaults (no CORS
origins, no `fix` or `autonomous` grants). The image ships `git` for the host runner but no
container engine, so `runner.isolation: container` is not supported by it: tasks that require
container isolation fail closed instead of running on the host. Deploy the `.output/` bundle on a
host with Docker or Podman when you need that mode.

---

## Toolchain
Expand Down
Loading