From 28ac494fe74c38842d708932ab72a095e0811830 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 14:34:09 +0000 Subject: [PATCH 1/5] ci: build and publish the dashboard image to GHCR Add a multi-stage Dockerfile that builds apps/dashboard with the self-hosted node preset and ships only the Nitro .output bundle, plus a cd workflow modelled on wolfstar-project/.github's reusable publish workflow: native amd64/arm64 builds pushed by digest, merged into one manifest list on ghcr.io. The image listens on $PORT, so it runs on Docker and Railway unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016Xv8HFiGBXf14HBy5yko6J --- .dockerignore | 30 +++++++ .github/workflows/cd.yml | 165 +++++++++++++++++++++++++++++++++++++++ Dockerfile | 72 +++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/cd.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8644d67 --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..4aa8a4d --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,165 @@ +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*' + 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" + + - 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 + build_env=release + elif [[ "$REF_NAME" == "$DEFAULT_BRANCH" ]]; then + build_env=canary + else + build_env=preview + fi + echo "BUILD_ENV=$build_env" >> "$GITHUB_ENV" + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - 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//: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}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a63a125 --- /dev/null +++ b/Dockerfile @@ -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/* + +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"] From a4e638b4e29735f190d9cf2ecac60b7bf1ebd3d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 14:34:09 +0000 Subject: [PATCH 2/5] docs: document the container image and cd workflow Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016Xv8HFiGBXf14HBy5yko6J --- AGENTS.md | 19 ++++++++++--------- README.md | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c6a71b9..ffed95a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`, `v*` 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 diff --git a/README.md b/README.md index c442ce7..4e3e65b 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,29 @@ 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 `v*` tags 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 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). + --- ## Toolchain From 34e852ef5d54ac4ce2d21b2ef04b75fa17768728 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 18:26:47 +0000 Subject: [PATCH 3/5] fix(ci): validate release tags and document the image's policy and isolation limits Release tags must now be exactly v before the cd workflow builds the release channel. The README's docker run example mounts code-zero.deployment.yml, and the container section states that the image ships no container engine, so runner.isolation: container is unsupported there and fails closed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016Xv8HFiGBXf14HBy5yko6J --- .github/workflows/cd.yml | 17 ++++++++++++----- AGENTS.md | 20 ++++++++++---------- README.md | 15 +++++++++++++-- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4aa8a4d..df42c9d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -11,7 +11,7 @@ on: branches: - main tags: - - 'v*' + - 'v[0-9]+.[0-9]+.[0-9]+*' workflow_dispatch: concurrency: @@ -46,6 +46,12 @@ jobs: 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` 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 }} @@ -53,6 +59,11 @@ jobs: 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 @@ -61,10 +72,6 @@ jobs: fi echo "BUILD_ENV=$build_env" >> "$GITHUB_ENV" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: Extract metadata (labels) for Docker id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 diff --git a/AGENTS.md b/AGENTS.md index ffed95a..9aee323 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,16 +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 | -| `cd.yml` | Builds and publishes the dashboard image to GHCR | Push to `main`, `v*` 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 | +| 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 diff --git a/README.md b/README.md index 4e3e65b..f80f91c 100644 --- a/README.md +++ b/README.md @@ -416,11 +416,14 @@ The root `Dockerfile` builds the dashboard with the self-hosted `node` preset an `.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 `v*` tags add `X.Y.Z` and `X.Y`. +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 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 @@ -433,6 +436,14 @@ environment sets. Apply migrations against `DATABASE_URL` with `/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 From a96a02d65500d926dd87274b1164196d5a6793d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 07:50:18 +0000 Subject: [PATCH 4/5] fix(i18n): replace the expired pkg.pr.new lunaria build with @lunariajs/core 0.2.0 packages/i18n pinned @lunariajs/core to a pkg.pr.new preview build that now returns 404, so every clean `aube ci` fails before any task runs. Depend on the published 0.2.0 release instead. Its API is the one i18n-status.ts already uses; missing keys are now reported as key paths (string arrays), so the status report joins them with dots. The lockfile was regenerated with `aube add`, which also rewrote the @prisma/client and drizzle-orm peer references of the two @better-auth/*-adapter@1.6.26 snapshots to a variant with no snapshot of its own. A frozen `aube ci` then links the drizzle adapter to a drizzle-orm directory that is never created and the dashboard build fails, so those two references keep their main values; `aube install` accepts the result unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016Xv8HFiGBXf14HBy5yko6J --- packages/i18n/package.json | 2 +- packages/i18n/scripts/i18n-status.ts | 6 ++- pnpm-lock.yaml | 70 ++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/packages/i18n/package.json b/packages/i18n/package.json index bfc5974..025c8b9 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -38,7 +38,7 @@ "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" }, "dependencies": { - "@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935", + "@lunariajs/core": "^0.2.0", "vue-i18n-extract": "2.0.7" }, "devDependencies": { diff --git a/packages/i18n/scripts/i18n-status.ts b/packages/i18n/scripts/i18n-status.ts index 340e81b..fa09998 100644 --- a/packages/i18n/scripts/i18n-status.ts +++ b/packages/i18n/scripts/i18n-status.ts @@ -1,5 +1,5 @@ // Translation status reporter, inspired by npmx.dev's Lunaria build script (MIT license). -// The pinned @lunariajs/core build has no CLI, so the status is computed through its API. +// The status is computed through @lunariajs/core's API rather than its CLI. import { mkdirSync, writeFileSync } from 'node:fs'; import { createLunaria } from '@lunariajs/core'; @@ -27,7 +27,9 @@ const summaries = lunaria.config.locales.map((locale) => { } if ('missingKeys' in localization) { - missingKeys.push(...localization.missingKeys.map((key) => `${localization.path}: ${key}`)); + missingKeys.push( + ...localization.missingKeys.map((key) => `${localization.path}: ${key.join('.')}`), + ); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 493da9c..0aed57e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -644,8 +644,8 @@ importers: packages/i18n: dependencies: '@lunariajs/core': - specifier: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935 - version: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935 + specifier: ^0.2.0 + version: 0.2.0(supports-color@8.1.1) vue-i18n-extract: specifier: 2.0.7 version: 2.0.7 @@ -3836,9 +3836,10 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} - '@lunariajs/core@https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935': - resolution: {integrity: sha512-N0PDFIitA/Vzh5V6BtTacWU9jgSDJJtPLHamRLMU85mohmyuVW/pggHX8dzdV5ieqZeHEz8pDZSzH4I0hOYxOg==, tarball: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935} - version: 0.1.1 + '@lunariajs/core@0.2.0': + resolution: {integrity: sha512-JuQsDStxiznw4Km0ab/FNH2VxVfWm08oJdjevRUOY7duN6nPKvBkk9rvXD6zPDlvauPWTf9RFuJ3lD4l+o9M4g==} + engines: {node: '>=18.17.0'} + hasBin: true '@maizzle/framework@6.1.0': resolution: {integrity: sha512-DrwSD3CuTQHGy7cClLtyi+W0d3v9j3KOUatdfDjzJQa13WMMsQupaxEhT2WN1lQRTgh0R7ViojeQteaUGeTxMA==} @@ -10516,6 +10517,9 @@ packages: encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -11375,6 +11379,10 @@ packages: resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} + gettext-parser@9.1.1: + resolution: {integrity: sha512-ZLeqWPz9OMNrTgMuww0C22kkcNqis+e4059R94t7L7ERlZ2rUNpiDbaAUus+esBC6uBAQWbS9N+R5vJIJg//lw==} + engines: {node: '>=20'} + giget@3.3.1: resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} hasBin: true @@ -12136,10 +12144,6 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jiti@2.3.3: - resolution: {integrity: sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==} - hasBin: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -13103,8 +13107,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - neotraverse@0.6.18: - resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + neotraverse@1.0.1: + resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} engines: {node: '>= 10'} netmask@2.1.1: @@ -13853,6 +13857,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -16713,6 +16721,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -16798,6 +16811,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -19641,19 +19657,20 @@ snapshots: '@lukeed/csprng@1.1.0': {} - '@lunariajs/core@https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935': + '@lunariajs/core@0.2.0(supports-color@8.1.1)': dependencies: consola: 3.4.2 - jiti: 2.3.3 - js-yaml: 4.3.1 - neotraverse: 0.6.18 + gettext-parser: 9.1.1 + jiti: 2.7.0 + neotraverse: 1.0.1 p-all: 5.0.1 path-to-regexp: 6.3.0 - picomatch: 4.0.5 + picomatch: 4.0.7 simple-git: 3.36.0(supports-color@8.1.1) tinyglobby: 0.2.17 ultramatter: 0.0.4 - zod: 3.25.76 + yaml: 2.9.1 + zod: 4.6.5 '@maizzle/framework@6.1.0(shiki@4.4.3)(tailwind-merge@3.6.0)(vue@3.5.41)': dependencies: @@ -26288,6 +26305,10 @@ snapshots: iconv-lite: 0.6.3 whatwg-encoding: 3.1.1 + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -27239,6 +27260,11 @@ snapshots: data-uri-to-buffer: 6.0.2 debug: 4.4.3(supports-color@8.1.1) + gettext-parser@9.1.1: + dependencies: + content-type: 1.0.5 + encoding: 0.1.13 + giget@3.3.1: {} git-up@8.1.1: @@ -28126,8 +28152,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jiti@2.3.3: {} - jiti@2.6.1: {} jiti@2.7.0: {} @@ -29193,7 +29217,7 @@ snapshots: neo-async@2.6.2: {} - neotraverse@0.6.18: {} + neotraverse@1.0.1: {} netmask@2.1.1: {} @@ -30410,6 +30434,8 @@ snapshots: picomatch@4.0.5: {} + picomatch@4.0.7: {} + pify@4.0.1: {} piscina@4.9.3: @@ -33696,6 +33722,8 @@ snapshots: yaml@2.9.0: {} + yaml@2.9.1: {} + yargs-parser@20.2.9: {} yargs-parser@22.0.0: {} @@ -33821,4 +33849,6 @@ snapshots: zod@4.4.3: {} + zod@4.6.5: {} + zwitch@2.0.4: {} From 1561a629bbc0a23446ac248fc7d199279e4828f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:15:16 +0000 Subject: [PATCH 5/5] test(i18n): cover the missing-key path format of the status report Extract the formatting into scripts/utils/lunaria-status.ts and assert that key path segments are dot-joined, so a wrong separator fails the package tests instead of only surfacing in status.json. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016Xv8HFiGBXf14HBy5yko6J --- packages/i18n/scripts/i18n-status.ts | 4 +++- packages/i18n/scripts/utils/lunaria-status.ts | 11 +++++++++++ packages/i18n/src/lunaria-status.test.ts | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/i18n/scripts/utils/lunaria-status.ts create mode 100644 packages/i18n/src/lunaria-status.test.ts diff --git a/packages/i18n/scripts/i18n-status.ts b/packages/i18n/scripts/i18n-status.ts index fa09998..cf52a43 100644 --- a/packages/i18n/scripts/i18n-status.ts +++ b/packages/i18n/scripts/i18n-status.ts @@ -4,6 +4,8 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { createLunaria } from '@lunariajs/core'; +import { formatMissingKey } from './utils/lunaria-status.ts'; + // `force: true` bypasses git caching so the status stays correct after rebases and merges. const lunaria = await createLunaria({ force: true }); const status = await lunaria.getFullStatus(); @@ -28,7 +30,7 @@ const summaries = lunaria.config.locales.map((locale) => { if ('missingKeys' in localization) { missingKeys.push( - ...localization.missingKeys.map((key) => `${localization.path}: ${key.join('.')}`), + ...localization.missingKeys.map((key) => formatMissingKey(localization.path, key)), ); } } diff --git a/packages/i18n/scripts/utils/lunaria-status.ts b/packages/i18n/scripts/utils/lunaria-status.ts new file mode 100644 index 0000000..2e86f88 --- /dev/null +++ b/packages/i18n/scripts/utils/lunaria-status.ts @@ -0,0 +1,11 @@ +// Formatting for the translation status report written by `scripts/i18n-status.ts`. + +/** + * One missing-key entry: the localization file, then the key as a dotted path. + * + * @lunariajs/core reports a missing key as its path segments (`['auth', 'signIn', 'title']`), so + * the segments are joined here rather than interpolated, which would comma-separate them. + */ +export function formatMissingKey(localizationPath: string, key: readonly string[]): string { + return `${localizationPath}: ${key.join('.')}`; +} diff --git a/packages/i18n/src/lunaria-status.test.ts b/packages/i18n/src/lunaria-status.test.ts new file mode 100644 index 0000000..c5bac64 --- /dev/null +++ b/packages/i18n/src/lunaria-status.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { formatMissingKey } from '../scripts/utils/lunaria-status.js'; + +describe('formatMissingKey', () => { + it('joins the key path segments with dots after the localization path', () => { + expect(formatMissingKey('locales/it/auth.json', ['auth', 'signIn', 'title'])).toBe( + 'locales/it/auth.json: auth.signIn.title', + ); + }); + + it('renders a top-level key without a separator', () => { + expect(formatMissingKey('locales/it/common.json', ['appName'])).toBe( + 'locales/it/common.json: appName', + ); + }); +});