feat: emails + legal pages + auto-deploy CI#27
Conversation
Six V3 Dark Cinematic email-client-safe HTML templates for Supabase Auth flows (confirm signup, magic link, change email, reset password, invite user, reauthentication) plus install/usage README. Co-Authored-By: Gradata <noreply@gradata.ai>
Add /legal/privacy and /legal/terms as public routes outside the (dashboard) auth group so prospects and unauthenticated users can read them. Both use the V3 Dark Cinematic theme with GlassCard. Content is Gradata-specific: - Privacy describes the local-first architecture (raw corrections never leave device; only synthesized principles + aggregate metrics reach cloud), names Supabase/Stripe/Sentry/Cloudflare as sub-processors with links, and lists concrete security claims (TLS 1.2+, RLS, Stripe handles PCI) instead of marketing superlatives. - Terms cover the two-part SDK (AGPL-3.0) + cloud (proprietary) split, billing for Free/Cloud ($29)/Team ($99)/Enterprise with no prorated refunds, AGPL carve-out, liability cap at fees paid in last 12 months, and Delaware governing law. Also wire tiny "Privacy · Terms · (c) 2026 Gradata" footer into DashboardLayout (font-mono, opacity-50, rendered after children) and a matching "Privacy · Terms" link pair below auth cards via a new AuthLegalLinks component (login, signup, forgot-password). Build goes from 22 to 24 static routes. Co-Authored-By: Gradata <noreply@gradata.ai>
Replaces the broken Cloudflare-native git-builder with a GH Actions workflow that builds cloud/dashboard/ with Sentry source-map upload, then deploys the dist/ output to Cloudflare Pages via wrangler. Runs on every push to main that touches the dashboard; supports [skip ci] and manual dispatch. Without source-map upload, prod Sentry stack traces are minified and useless for debugging. Co-Authored-By: Gradata <noreply@gradata.ai>
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (30)
📝 Walkthrough
WalkthroughAdds CI workflows (dashboard source-map build/deploy, nightly k6 load tests, SDK test/publish), dashboard legal/docs pages and layout/footer, auth-page legal links, Supabase email templates, marketing site, k6 load-test scripts, notification preferences model/migration/API/tests, and related docs and migrations. Changes
Sequence Diagram(s)sequenceDiagram
participant GH as GitHub Actions
participant Repo as Repository (cloud/dashboard)
participant Build as Vite / pnpm Build
participant Sentry as Sentry API
participant CF as Cloudflare Pages
GH->>Repo: trigger on push to main or workflow_dispatch
GH->>Repo: checkout `cloud/dashboard` (working-directory)
GH->>Build: setup Node/pnpm, pnpm install
GH->>Build: run `pnpm build` (Vite)
Build->>Sentry: upload source maps (SENTRY_AUTH_TOKEN, release)
Build->>Build: delete uploaded maps (post-upload cleanup)
GH->>CF: run `npx wrangler pages deploy out` with CF token
CF->>CF: deploy site `gradata-dashboard` (branch main)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
Behind auth so we can later inject personalized snippets (user's API key in the example). 6 sections, 11 snippets covering install, brain init (local + cloud), session/correct, rule injection, inspection, and TS SDK. Sidebar gets a new /docs entry under Brain. Co-Authored-By: Gradata <noreply@gradata.ai>
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/dashboard-source-maps.yml:
- Line 63: Replace the non-reproducible usage of "wrangler@latest" in the GH
Actions step that runs "npx wrangler@latest pages deploy out
--project-name=gradata-dashboard --branch=main --commit-dirty=true" by pinning
to a specific semantic version (for example "wrangler@4.0.0") or alternatively
add Wrangler to devDependencies and run it via the package runner (e.g., "pnpm
exec wrangler ..."); update the command string accordingly so the deploy step
uses the pinned version or pnpm exec invocation instead of "wrangler@latest".
- Line 21: The current job if-condition accesses
github.event.head_commit.message which is undefined for workflow_dispatch;
update the if-condition to first check github.event_name and only access
github.event.head_commit.message when the event is a push. Concretely, wrap the
existing contains(...) check behind a guard that validates github.event_name ==
'push' (or allow workflow_dispatch unconditionally), so the job runs for manual
dispatches and still skips CI on push commits containing "[skip ci]"; change the
if-expression used at the top-level condition accordingly to implement this
guard.
In `@cloud/dashboard/app/legal/terms/page.tsx`:
- Around line 253-275: Extract the duplicated Section and EmailLink components
into a shared legal components module (e.g., a new legal components file) and
import them from both pages to avoid drift: create a single exported Section and
EmailLink component (matching the current props/signature) and replace the
inline definitions in page.tsx and privacy/page.tsx with imports of these shared
components, updating any className/prop usage to match the exported API; ensure
the new module exports are properly typed and run a build to confirm no JSX/type
mismatches.
In `@cloud/dashboard/SOURCE-MAPS.md`:
- Line 42: Replace the concrete Cloudflare account ID shown in SOURCE-MAPS.md
(the `CLOUDFLARE_ACCOUNT_ID` example value `d568e4421afe0100d09df9e4d29bef81`)
with a non-sensitive placeholder like `<CLOUDFLARE_ACCOUNT_ID>` or
`your-account-id-here`; update any accompanying example to note that the real
value must be stored in CI/GitHub Actions secrets rather than committed to docs,
and ensure any references to `CLOUDFLARE_ACCOUNT_ID` in the file explicitly
instruct users to pull the value from secrets or environment variables.
- Line 28: Update the documentation line that currently shows the deploy command
`wrangler pages deploy dist` to match the actual workflow by using `wrangler
pages deploy out`; change the reference in SOURCE-MAPS.md so the command string
matches the workflow's deployed artifact (`out`) and keeps docs consistent with
`.github/workflows/dashboard-source-maps.yml`.
In `@cloud/dashboard/src/components/layout/AuthLegalLinks.tsx`:
- Around line 9-20: AuthLegalLinks.tsx renders tiny, low-contrast legal links
that are hard to read and have no keyboard focus affordance; update the Link
elements inside the AuthLegalLinks component to use a larger, more readable font
size (e.g., remove or replace text-[10px] with text-sm or text-xs) and higher
default opacity (remove opacity-50 or set opacity-80/100), and add explicit
focus-visible/focus classes (e.g., focus:outline-none focus-visible:ring-2
focus-visible:ring-[var(--color-accent-blue)] focus-visible:rounded-sm) to each
Link so keyboard users get a clear visual indicator when tabbing. Ensure you
update the wrapper className and the Link className strings referenced in
AuthLegalLinks.tsx rather than adding new elements.
In `@cloud/SENTRY-SETUP.md`:
- Around line 41-53: Documentation mismatch: update the env-var docs to reflect
Next.js usage (use NEXT_PUBLIC_* for client-side vars and SENTRY_* for
build-time Sentry config as used in next.config.ts) and correct the GitHub
Actions secret list to include all ten required secrets (SENTRY_AUTH_TOKEN,
SENTRY_ORG, SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_RELEASE,
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_API_URL,
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) so the description matches the
dashboard-source-maps.yml workflow; edit the VITE_* mention to NEXT_PUBLIC_* and
replace the “five secrets” line with the full ten-secret list referenced above.
In `@cloud/supabase/email-templates/confirm-signup.html`:
- Line 90: Replace the hard-coded year "© 2026 Gradata" in the email footer with
a non-yeared footer (e.g., "© Gradata") or a template variable so the footer
doesn't become stale; locate the footer string "© 2026 Gradata" in the
confirm-signup.html template and update it to remove the year or reference a
runtime year variable (e.g., site/company footer text) so future years won't
require template changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 293950d5-32b9-4dcf-ac6b-526e1ad5aa9e
📒 Files selected for processing (17)
.github/workflows/dashboard-source-maps.ymlcloud/SENTRY-SETUP.mdcloud/dashboard/SOURCE-MAPS.mdcloud/dashboard/app/forgot-password/page.tsxcloud/dashboard/app/legal/privacy/page.tsxcloud/dashboard/app/legal/terms/page.tsxcloud/dashboard/app/login/page.tsxcloud/dashboard/app/signup/page.tsxcloud/dashboard/src/components/layout/AuthLegalLinks.tsxcloud/dashboard/src/components/layout/DashboardLayout.tsxcloud/supabase/email-templates/README.mdcloud/supabase/email-templates/change-email.htmlcloud/supabase/email-templates/confirm-signup.htmlcloud/supabase/email-templates/invite-user.htmlcloud/supabase/email-templates/magic-link.htmlcloud/supabase/email-templates/reauthentication.htmlcloud/supabase/email-templates/reset-password.html
📜 Review details
🧰 Additional context used
🪛 LanguageTool
cloud/SENTRY-SETUP.md
[uncategorized] ~48-~48: The official name of this software platform is spelled with a capital “H”.
Context: ... that touches cloud/dashboard/**, via .github/workflows/dashboard-source-maps.yml. N...
(GITHUB)
cloud/dashboard/SOURCE-MAPS.md
[uncategorized] ~23-~23: The official name of this software platform is spelled with a capital “H”.
Context: ...t touches cloud/dashboard/** triggers .github/workflows/dashboard-source-maps.yml: ...
(GITHUB)
🪛 markdownlint-cli2 (0.22.0)
cloud/supabase/email-templates/README.md
[warning] 20-20: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 28-28: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 38-38: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (12)
cloud/supabase/email-templates/README.md (1)
10-27: Clear operational mapping and install flow.Template-to-variable mapping and the 5-step install guidance are explicit and easy to execute.
cloud/dashboard/app/signup/page.tsx (1)
10-10: Consistent legal-link integration across both signup states.
AuthLegalLinksis rendered in both the confirmation and form branches, and theflex-collayout update correctly supports the stacked composition.Also applies to: 43-43, 57-57, 63-63, 104-104
cloud/dashboard/app/login/page.tsx (1)
10-10: Login page composition update looks good.The column layout and placement of
AuthLegalLinksbelow the card are clean and consistent with the auth flow UX.Also applies to: 37-37, 95-95
cloud/dashboard/app/forgot-password/page.tsx (1)
9-9: Good parity across forgot-password states.Legal links are present in both UI paths, and the layout shift to
flex-colis correctly applied in each branch.Also applies to: 31-31, 45-45, 51-51, 78-78
cloud/supabase/email-templates/reset-password.html (1)
1-104: Template structure is solid and production-ready.CTA + plain-link fallback + contextual security copy are all present, with email-client-safe table/inline CSS conventions.
cloud/dashboard/src/components/layout/DashboardLayout.tsx (1)
164-184: Looks good: legal footer integration is clean and consistent.The new footer component and placement under page content are implemented correctly and align with the new legal routes.
Also applies to: 195-198
cloud/supabase/email-templates/magic-link.html (1)
1-101: Template implementation is solid.Placeholder usage, CTA/fallback behavior, and legal footer links are consistent and correctly wired for the magic-link flow.
cloud/supabase/email-templates/change-email.html (1)
1-96: Nice addition for the email-change flow.The template is well-structured, uses the expected Supabase variables, and includes appropriate fallback and disclaimer content.
cloud/supabase/email-templates/reauthentication.html (1)
1-117: Reauthentication template looks correct.Good use of both OTP and confirmation-link paths, with the expected Supabase variables and safe fallback rendering.
cloud/supabase/email-templates/invite-user.html (1)
1-104: Invite template is well-implemented.The variable usage matches the documented invite flow and the email structure is consistent with the rest of the template set.
cloud/dashboard/app/legal/privacy/page.tsx (1)
5-281: Privacy page implementation is strong.Metadata, structure, helper components, and link safety are all correctly handled for a public legal route.
cloud/dashboard/app/legal/terms/page.tsx (1)
5-251: Terms page is correctly implemented.Route metadata, layout composition, and legal navigation links are all wired properly.
| jobs: | ||
| build-deploy: | ||
| runs-on: ubuntu-latest | ||
| if: "!contains(github.event.head_commit.message, '[skip ci]')" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For GitHub Actions, is github.event.head_commitavailable onworkflow_dispatch, and what is the recommended conditional to support [skip ci]only forpush events?
💡 Result:
No, github.event.head_commit is not available on workflow_dispatch events. It is part of the push event payload, which includes commit details like the head commit message. For workflow_dispatch, the event payload primarily contains inputs if defined, but no head_commit object, as there is no push involved—it's a manual trigger using the commit at the specified ref (GITHUB_REF). The recommended conditional to support [skip ci] only for push events (where GitHub's built-in skipping doesn't apply to other triggers like workflow_dispatch) is to check both the event name and the absence of skip phrases in the head commit message: if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[skip ci]') This skips the job/step if it's not a push event or if the push's head commit message contains [skip ci]. Use github.event_name != 'workflow_dispatch' if you want to run on push unless skipped, but exclude workflow_dispatch entirely. Multiple skip phrases can be checked: !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') etc. Note: GitHub natively skips entire workflows (not individual jobs/steps) for push/pull_request if any commit contains skip phrases, but custom conditionals are needed for other events or finer control.
Citations:
- 1: https://docs.github.com/actions/managing-workflow-runs/skipping-workflow-runs
- 2: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
- 3: https://docs.github.com/actions/using-workflows/triggering-a-workflow
- 4: https://stackoverflow.com/questions/66205887/only-run-github-actions-step-if-not-a-pull-request/66206183
- 5: Docs:
if: github.event == 'push'doesn't work, it should begithub.event_nameactions/runner#950 - 6: https://stackoverflow.com/questions/59759921/how-to-skip-github-actions-job-on-push-event
- 7: https://github.community/t/what-happened-to-github-event-head-commit-modified/16843
- 8:
github.event.head_commitis missing nektos/act#779 - 9: https://stackoverflow.com/questions/63619329/how-to-get-the-commit-message-in-github-actions
- 10: https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
- 11: https://stackoverflow.com/questions/59439380/can-i-filter-a-github-action-step-based-on-the-commit-message
- 12: https://stackoverflow.com/questions/71361539/github-action-test-if-a-commit-containing-a-specific-word-was-previously-made
- 13: https://stackoverflow.com/questions/76663142/how-to-skip-step-in-github-action-when-no-ci-present-in-the-commit-comment
🏁 Script executed:
cat .github/workflows/dashboard-source-maps.ymlRepository: Gradata/gradata
Length of output: 2301
Guard the skip-CI condition for manual dispatch events.
github.event.head_commit is not available on workflow_dispatch events. Since this workflow is triggered by both push and workflow_dispatch, the condition at line 21 will fail when manually triggered, preventing the job from running as expected.
Apply the proposed conditional to check the event type before accessing the commit message:
Fix
- if: "!contains(github.event.head_commit.message, '[skip ci]')"
+ if: ${{ github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]') }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if: "!contains(github.event.head_commit.message, '[skip ci]')" | |
| if: ${{ github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]') }} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/dashboard-source-maps.yml at line 21, The current job
if-condition accesses github.event.head_commit.message which is undefined for
workflow_dispatch; update the if-condition to first check github.event_name and
only access github.event.head_commit.message when the event is a push.
Concretely, wrap the existing contains(...) check behind a guard that validates
github.event_name == 'push' (or allow workflow_dispatch unconditionally), so the
job runs for manual dispatches and still skips CI on push commits containing
"[skip ci]"; change the if-expression used at the top-level condition
accordingly to implement this guard.
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| run: npx wrangler@latest pages deploy out --project-name=gradata-dashboard --branch=main --commit-dirty=true |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the recommended way to pin wranglerin CI for Cloudflare Pages deploys to avoid@latest instability?
💡 Result:
The recommended way to pin wrangler in CI for Cloudflare Pages deploys is to use the official cloudflare/wrangler-action@v3 GitHub Action and specify a pinned wranglerVersion (e.g., "4.0.0") in the workflow YAML. This installs a specific Wrangler version from npm, avoiding @latest instability. For manual npx wrangler pages deploy commands in CI (e.g., GitHub Actions, CircleCI), use npx wrangler@4.0.0 pages deploy <BUILD_DIR> --project-name=, replacing "4.0.0" with your pinned version. Add Wrangler as a pinned devDependency in package.json (npm i -D wrangler@4.0.0) and run ./node_modules/.bin/wrangler pages deploy after npm ci for reproducible installs via lockfile. Official Cloudflare docs recommend local project installs over global/npx @latest for version control. The wrangler-action explicitly supports wranglerVersion pinning, as shown in its README and multiple GitHub issues/PRs confirming this best practice for CI stability.
Citations:
- 1: https://developers.cloudflare.com/workers/wrangler/install-and-update/
- 2: https://github.com/cloudflare/wrangler-action
- 3: Request: Install Newer Version of Wrangler by Default cloudflare/wrangler-action#366
- 4: Update wrangler to v4 cloudflare/wrangler-action#375
- 5: Add wranglerVersion options cloudflare/pages-action#82
- 6: https://circleci.com/blog/ci-cd-for-cloudflare-pages-using-wrangler/
- 7: https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/index.md
- 8: https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/
🏁 Script executed:
# First, check if the workflow file exists and read its content
cat -n .github/workflows/dashboard-source-maps.ymlRepository: Gradata/gradata
Length of output: 2742
🏁 Script executed:
# Check if package.json exists and see if wrangler is a devDependency
head -100 package.jsonRepository: Gradata/gradata
Length of output: 130
🏁 Script executed:
# Check for pnpm usage in the project
if [ -f "pnpm-lock.yaml" ] || [ -f ".npmrc" ]; then
echo "=== pnpm-lock.yaml exists ==="
ls -la pnpm-lock.yaml .npmrc 2>/dev/null || true
fi
# Check the package manager used
cat -n package.json | grep -A5 -B5 '"packageManager"' || echo "No packageManager field found"Repository: Gradata/gradata
Length of output: 150
🏁 Script executed:
# Check package.json in the cloud/dashboard directory
cat -n cloud/dashboard/package.json | head -80Repository: Gradata/gradata
Length of output: 1733
Pin wrangler to a specific version in the deploy command.
Using @latest makes deploys non-reproducible and can break on upstream releases. Pin to a semantic version (e.g., wrangler@4.0.0) rather than just a major version. Alternatively, add wrangler as a devDependency and run via pnpm exec wrangler.
✅ Proposed fix
- run: npx wrangler@latest pages deploy out --project-name=gradata-dashboard --branch=main --commit-dirty=true
+ run: npx wrangler@4.0.0 pages deploy out --project-name=gradata-dashboard --branch=main --commit-dirty=true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| run: npx wrangler@latest pages deploy out --project-name=gradata-dashboard --branch=main --commit-dirty=true | |
| run: npx wrangler@4.0.0 pages deploy out --project-name=gradata-dashboard --branch=main --commit-dirty=true |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/dashboard-source-maps.yml at line 63, Replace the
non-reproducible usage of "wrangler@latest" in the GH Actions step that runs
"npx wrangler@latest pages deploy out --project-name=gradata-dashboard
--branch=main --commit-dirty=true" by pinning to a specific semantic version
(for example "wrangler@4.0.0") or alternatively add Wrangler to devDependencies
and run it via the package runner (e.g., "pnpm exec wrangler ..."); update the
command string accordingly so the deploy step uses the pinned version or pnpm
exec invocation instead of "wrangler@latest".
| function Section({ title, children }: { title: string; children: React.ReactNode }) { | ||
| return ( | ||
| <section className="mb-6"> | ||
| <GlassCard> | ||
| <h2 className="mb-3 text-[18px]">{title}</h2> | ||
| <div className="space-y-2 text-[14px] leading-relaxed text-[var(--color-body)]"> | ||
| {children} | ||
| </div> | ||
| </GlassCard> | ||
| </section> | ||
| ) | ||
| } | ||
|
|
||
| function EmailLink({ children }: { children: string }) { | ||
| return ( | ||
| <a | ||
| href={`mailto:${children}`} | ||
| className="text-[var(--color-accent-blue)] underline-offset-4 hover:underline" | ||
| > | ||
| {children} | ||
| </a> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Extract shared legal helper components to reduce drift.
Section and EmailLink here duplicate the same patterns in cloud/dashboard/app/legal/privacy/page.tsx. Centralizing them (e.g., cloud/dashboard/src/components/legal/*) will keep legal pages consistent and easier to maintain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/dashboard/app/legal/terms/page.tsx` around lines 253 - 275, Extract the
duplicated Section and EmailLink components into a shared legal components
module (e.g., a new legal components file) and import them from both pages to
avoid drift: create a single exported Section and EmailLink component (matching
the current props/signature) and replace the inline definitions in page.tsx and
privacy/page.tsx with imports of these shared components, updating any
className/prop usage to match the exported API; ensure the new module exports
are properly typed and run a build to confirm no JSX/type mismatches.
| 1. Checkout, install pnpm 10 + Node 20 | ||
| 2. `pnpm install --frozen-lockfile` | ||
| 3. `pnpm build` with `SENTRY_AUTH_TOKEN` set → Vite builds, plugin uploads + deletes maps | ||
| 4. `wrangler pages deploy dist` → pushes the built output to Cloudflare Pages |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify what directory is actually deployed by the workflow.
fd -i 'dashboard-source-maps.yml' .github/workflows -x sh -c '
echo "== {} =="
rg -n "wrangler pages deploy|working-directory|pnpm build|dist|out" "{}"
'Repository: Gradata/gradata
Length of output: 364
Fix deploy artifact mismatch in documentation (Line 28).
The doc references wrangler pages deploy dist, but the actual workflow deploys out. Update this to wrangler pages deploy out to match the workflow configuration at .github/workflows/dashboard-source-maps.yml line 63.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/dashboard/SOURCE-MAPS.md` at line 28, Update the documentation line
that currently shows the deploy command `wrangler pages deploy dist` to match
the actual workflow by using `wrangler pages deploy out`; change the reference
in SOURCE-MAPS.md so the command string matches the workflow's deployed artifact
(`out`) and keeps docs consistent with
`.github/workflows/dashboard-source-maps.yml`.
| | `SENTRY_ORG` | `gradata` | | ||
| | `SENTRY_PROJECT` | `gradata-dashboard` | | ||
| | `CLOUDFLARE_API_TOKEN` | Cloudflare → My Profile → API Tokens → **Create Token**. Template: "Custom". Permission: `Account → Cloudflare Pages → Edit`. Account Resources: include your account. | | ||
| | `CLOUDFLARE_ACCOUNT_ID` | `d568e4421afe0100d09df9e4d29bef81` | |
There was a problem hiding this comment.
Avoid publishing a concrete Cloudflare account ID in docs (Line 42).
This leaks infra metadata and is unnecessary for onboarding. Replace with a placeholder/example and keep the real value only in GitHub Actions secrets.
Suggested doc change
-| `CLOUDFLARE_ACCOUNT_ID` | `d568e4421afe0100d09df9e4d29bef81` |
+| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare Account ID (from Cloudflare dashboard) |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | `CLOUDFLARE_ACCOUNT_ID` | `d568e4421afe0100d09df9e4d29bef81` | | |
| | `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare Account ID (from Cloudflare dashboard) | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/dashboard/SOURCE-MAPS.md` at line 42, Replace the concrete Cloudflare
account ID shown in SOURCE-MAPS.md (the `CLOUDFLARE_ACCOUNT_ID` example value
`d568e4421afe0100d09df9e4d29bef81`) with a non-sensitive placeholder like
`<CLOUDFLARE_ACCOUNT_ID>` or `your-account-id-here`; update any accompanying
example to note that the real value must be stored in CI/GitHub Actions secrets
rather than committed to docs, and ensure any references to
`CLOUDFLARE_ACCOUNT_ID` in the file explicitly instruct users to pull the value
from secrets or environment variables.
| <div className="mt-6 flex items-center justify-center gap-3 font-mono text-[10px] uppercase tracking-wider text-[var(--color-body)] opacity-50"> | ||
| <Link | ||
| href="/legal/privacy" | ||
| className="transition-colors hover:text-[var(--color-accent-blue)] hover:opacity-100" | ||
| > | ||
| Privacy | ||
| </Link> | ||
| <span aria-hidden>·</span> | ||
| <Link | ||
| href="/legal/terms" | ||
| className="transition-colors hover:text-[var(--color-accent-blue)] hover:opacity-100" | ||
| > |
There was a problem hiding this comment.
Increase readability and keyboard-focus visibility for legal links.
Current styling (text-[10px] + opacity-50) makes required legal links hard to read, and links lack explicit focus-visible affordances.
✅ Proposed fix
- <div className="mt-6 flex items-center justify-center gap-3 font-mono text-[10px] uppercase tracking-wider text-[var(--color-body)] opacity-50">
+ <div className="mt-6 flex items-center justify-center gap-3 font-mono text-xs uppercase tracking-wider text-[var(--color-body)]">
<Link
href="/legal/privacy"
- className="transition-colors hover:text-[var(--color-accent-blue)] hover:opacity-100"
+ className="transition-colors hover:text-[var(--color-accent-blue)] focus-visible:outline-none focus-visible:underline"
>
Privacy
</Link>
<span aria-hidden>·</span>
<Link
href="/legal/terms"
- className="transition-colors hover:text-[var(--color-accent-blue)] hover:opacity-100"
+ className="transition-colors hover:text-[var(--color-accent-blue)] focus-visible:outline-none focus-visible:underline"
>
Terms
</Link>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/dashboard/src/components/layout/AuthLegalLinks.tsx` around lines 9 -
20, AuthLegalLinks.tsx renders tiny, low-contrast legal links that are hard to
read and have no keyboard focus affordance; update the Link elements inside the
AuthLegalLinks component to use a larger, more readable font size (e.g., remove
or replace text-[10px] with text-sm or text-xs) and higher default opacity
(remove opacity-50 or set opacity-80/100), and add explicit focus-visible/focus
classes (e.g., focus:outline-none focus-visible:ring-2
focus-visible:ring-[var(--color-accent-blue)] focus-visible:rounded-sm) to each
Link so keyboard users get a clear visual indicator when tabbing. Ensure you
update the wrapper className and the Link className strings referenced in
AuthLegalLinks.tsx rather than adding new elements.
| VITE_SENTRY_RELEASE=gradata-dashboard@<ver> # optional; GH Actions overrides per build | ||
| ``` | ||
|
|
||
| **Important:** Vite reads `VITE_*` vars **at build time**. After setting them, trigger a rebuild (retry deploy, or push a commit). Reading env changes requires a new build. | ||
|
|
||
| ### Source maps (for readable stack traces) | ||
|
|
||
| Source-map upload happens **automatically on every push to `main`** that touches `cloud/dashboard/**`, via `.github/workflows/dashboard-source-maps.yml`. No manual `sentry-cli` runs, no Cloudflare-side env vars for the Sentry auth token — the workflow holds them as GitHub Actions secrets. | ||
|
|
||
| See [`cloud/dashboard/SOURCE-MAPS.md`](./dashboard/SOURCE-MAPS.md) for: | ||
| - The five GitHub Actions secrets to configure (`SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`) | ||
| - How to verify source maps uploaded after a deploy | ||
| - How to skip the workflow (`[skip ci]` in commit message) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SENTRY setup doc references =="
rg -n 'VITE_SENTRY|NEXT_PUBLIC_SENTRY|five GitHub Actions secrets|GitHub Actions secrets' cloud/SENTRY-SETUP.md
echo
echo "== Workflow-required secrets/env =="
rg -n 'secrets\.|NEXT_PUBLIC_|SENTRY_AUTH_TOKEN|SENTRY_ORG|SENTRY_PROJECT|CLOUDFLARE_' .github/workflows/dashboard-source-maps.yml
echo
echo "== Next.js Sentry config env usage =="
rg -n 'process\.env\.(SENTRY_AUTH_TOKEN|SENTRY_ORG|SENTRY_PROJECT|NEXT_PUBLIC_)' cloud/dashboard/next.config.ts cloud/dashboard -g '*.ts' -g '*.tsx'Repository: Gradata/gradata
Length of output: 2927
Update env-var documentation and secret count to match the Next.js workflow.
Lines 39–41 document VITE_* environment variables, but the actual build uses Next.js with NEXT_PUBLIC_* prefix for client-side variables and SENTRY_* (without prefix) for build-time Sentry configuration in next.config.ts. Additionally, line 51 claims only five secrets, but the workflow requires ten: three Sentry secrets (SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT), five NEXT_PUBLIC_* secrets (NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_RELEASE, NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_API_URL), and two Cloudflare secrets (CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID). As documented, this will cause CI and deploy failures.
Suggested corrections
Update lines 39–41 to use NEXT_PUBLIC_*:
- VITE_SENTRY_DSN=https://...@o0.ingest.sentry.io/<project-id>
- VITE_SENTRY_ENVIRONMENT=production # optional, defaults to MODE
- VITE_SENTRY_RELEASE=gradata-dashboard@<ver> # optional; GH Actions overrides per build
+ NEXT_PUBLIC_SENTRY_DSN=https://...@o0.ingest.sentry.io/<project-id>
+ NEXT_PUBLIC_SENTRY_ENVIRONMENT=production # optional
+ NEXT_PUBLIC_SENTRY_RELEASE=gradata-dashboard@<ver> # optional; GH Actions overrides per buildUpdate line 51 to reflect all required secrets:
- - The five GitHub Actions secrets to configure (`SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`)
+ - The GitHub Actions secrets required by the workflow:
+ - **Sentry** (build-time): `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`
+ - **Public build-time** (inlined by Next.js): `NEXT_PUBLIC_SENTRY_DSN`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `NEXT_PUBLIC_API_URL`
+ - **Cloudflare**: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`🧰 Tools
🪛 LanguageTool
[uncategorized] ~48-~48: The official name of this software platform is spelled with a capital “H”.
Context: ... that touches cloud/dashboard/**, via .github/workflows/dashboard-source-maps.yml. N...
(GITHUB)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/SENTRY-SETUP.md` around lines 41 - 53, Documentation mismatch: update
the env-var docs to reflect Next.js usage (use NEXT_PUBLIC_* for client-side
vars and SENTRY_* for build-time Sentry config as used in next.config.ts) and
correct the GitHub Actions secret list to include all ten required secrets
(SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_RELEASE, NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_API_URL, CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID) so the description matches the dashboard-source-maps.yml
workflow; edit the VITE_* mention to NEXT_PUBLIC_* and replace the “five
secrets” line with the full ten-secret list referenced above.
| · | ||
| <a href="https://app.gradata.ai/legal/terms" style="color:#94A3B8;text-decoration:underline;">Terms</a> | ||
| · | ||
| © 2026 Gradata |
There was a problem hiding this comment.
Avoid hard-coded footer year in transactional templates.
Line 90 hard-codes 2026, which will become stale. Prefer © Gradata (no year) to avoid annual template churn and inconsistency.
✅ Proposed fix
- © 2026 Gradata
+ © Gradata📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| © 2026 Gradata | |
| © Gradata |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/supabase/email-templates/confirm-signup.html` at line 90, Replace the
hard-coded year "© 2026 Gradata" in the email footer with a non-yeared footer
(e.g., "© Gradata") or a template variable so the footer doesn't become stale;
locate the footer string "© 2026 Gradata" in the confirm-signup.html template
and update it to remove the year or reference a runtime year variable (e.g.,
site/company footer text) so future years won't require template changes.
Four scenarios targeting the Railway-hosted FastAPI: - baseline.js: 5 VUs / 2m smoke test on public /health (p95<500ms, err<1%) - authed-read.js: 25 VUs / 5m read load on /brains + /users/me (p95<800ms, err<2%) - sync-write.js: 10 VUs / 3m POST /sync with SyncRequest payload (p95<1500ms, err<5%) - spike.js: 0->100 VUs rate-limit check, asserts zero 5xx (429s OK) Authed scenarios fail loudly if K6_API_KEY is unset — no fake passes. Nightly GH Action runs baseline + spike at 03:00 UTC against prod and comments on the commit if any SLO is breached. Authed + write scenarios are opt-in via workflow_dispatch to avoid uncoordinated test writes. README documents install, SLOs, how to read p95/p99, and the Railway rollback playbook.
- .github/workflows/sdk-publish.yml: tag-triggered (sdk-v*) build+publish flow. Uses PyPI OIDC trusted publishing (no API token in secrets). Verifies tag matches pyproject.toml version, checks whether the version is already on the target index and skips instead of erroring, and routes prerelease tags (rc/a/b/dev) to TestPyPI. - .github/workflows/sdk-test.yml: push/PR matrix test on Python 3.11, 3.12, 3.13 with uv cache, plus wheel build smoke test. - sdk/RELEASE.md: step-by-step release runbook including RC/TestPyPI flow and local dry-run instructions. - sdk/PYPI-SETUP.md: one-time trusted publisher configuration (PyPI + TestPyPI) and troubleshooting. Package is 'gradata' on PyPI (current 0.4.0, local 0.5.0). Existing sdk-release.yml (tag v*) is left untouched; new sdk-publish.yml uses the sdk-v* prefix as specified. Co-Authored-By: Gradata <noreply@gradata.ai>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cloud/dashboard/app/`(dashboard)/docs/page.tsx:
- Around line 203-207: The copy function currently calls
navigator.clipboard.writeText(snippet.code) without awaiting or error handling,
causing setCopied(true) to show success even if the write fails and leaving
unhandled promise rejections; modify the copy function to await
navigator.clipboard.writeText(...) inside a try/catch (or use .then/.catch),
only call setCopied(true) after a successful write, handle rejections by logging
or showing an error state (e.g., setCopied(false) and optionally set an error
flag/message), and ensure the existing setTimeout to revert setCopied(false)
only runs after success; update references to copy,
navigator.clipboard.writeText, setCopied accordingly.
In `@cloud/dashboard/src/components/layout/DashboardLayout.tsx`:
- Line 182: Replace the hardcoded "© 2026 Gradata" string in the DashboardLayout
component with a dynamically computed year (e.g., using new
Date().getFullYear()) so the rendered span shows the current year at runtime;
locate the span in DashboardLayout.tsx (the element currently containing "© 2026
Gradata") and update its content to include the computed year while preserving
the "©" and "Gradata" text.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3cf0bd91-0e96-45c4-b0e2-7e3fa93dbe8d
📒 Files selected for processing (2)
cloud/dashboard/app/(dashboard)/docs/page.tsxcloud/dashboard/src/components/layout/DashboardLayout.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
cloud/dashboard/app/(dashboard)/docs/page.tsx (1)
20-199: Solid docs page structure.
SNIPPETSbeing module-scoped and sectioned makes this page easy to maintain and keeps render cost predictable. Nice, clean composition withSnippetCard.cloud/dashboard/src/components/layout/DashboardLayout.tsx (1)
21-21: Navigation + footer integration looks correct.The
/docsentry is wired cleanly intoSECTIONS, andLegalFooterplacement under dashboard content is consistent with the new legal routes.Also applies to: 165-199
| const copy = () => { | ||
| navigator.clipboard.writeText(snippet.code) | ||
| setCopied(true) | ||
| setTimeout(() => setCopied(false), 1500) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "page.tsx" cloud/dashboard/app/\(dashboard\)/docs/Repository: Gradata/gradata
Length of output: 104
🏁 Script executed:
cat -n cloud/dashboard/app/\(dashboard\)/docs/page.tsx | head -220 | tail -40Repository: Gradata/gradata
Length of output: 1616
🏁 Script executed:
head -30 cloud/dashboard/app/\(dashboard\)/docs/page.tsxRepository: Gradata/gradata
Length of output: 738
🏁 Script executed:
rg "navigator.clipboard" cloud/dashboard/app/\(dashboard\)/docs/page.tsx -B 5 -A 10Repository: Gradata/gradata
Length of output: 611
🏁 Script executed:
web_search
navigator.clipboard.writeText promise rejection error handlingRepository: Gradata/gradata
Length of output: 177
🌐 Web query:
navigator.clipboard.writeText promise rejection MDN documentation
💡 Result:
The MDN documentation for navigator.clipboard.writeText states that it returns a Promise resolved once the clipboard is updated, but the Promise rejects "if writing to the clipboard is not allowed". The Exceptions section generically notes: "Thrown if writing to the clipboard is not allowed." No specific DOMException types (e.g., NotAllowedError, AbortError) are listed for writeText, unlike readText which explicitly documents NotAllowedError and NotFoundError. The method requires a secure context (HTTPS). Additional security details are in the Clipboard API overview, which explains browser-specific requirements like transient user activation (user gesture), document focus, or clipboard-write permission (auto-granted in active tab for some browsers). In practice, rejections occur as DOMExceptions for reasons including: - Non-secure context (HTTP) - No user gesture / transient activation expired - Document not focused - Permission denied (clipboard-write) - Browser-specific issues (e.g., Safari timing) Example handling from MDN: async function writeClipboardText(text) { try { await navigator.clipboard.writeText(text); } catch (error) { console.error(error.message); // e.g., "Document is not focused.", "The request is not allowed..." } }
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
- 2: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/clipboard
- 3: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText
- 4: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
- 5: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
- 6: https://w3c.github.io/clipboard-apis
- 7: https://stackoverflow.com/questions/73309629/how-to-catch-error-for-navigator-clipboard-writetext
Handle clipboard errors before showing success.
navigator.clipboard.writeText(snippet.code) returns a Promise that can reject (non-HTTPS context, permission denied, no user gesture, unfocused document). The current handler sets copied=true immediately without awaiting, showing false success even if the clipboard write fails, and leaves unhandled promise rejections.
Proposed fix
-import { useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
...
function SnippetCard({ snippet }: { snippet: Snippet }) {
const [copied, setCopied] = useState(false)
- const copy = () => {
- navigator.clipboard.writeText(snippet.code)
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
- }
+ const timeoutRef = useRef<number | null>(null)
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current)
+ }
+ }, [])
+
+ const copy = async () => {
+ try {
+ await navigator.clipboard.writeText(snippet.code)
+ setCopied(true)
+ if (timeoutRef.current !== null) window.clearTimeout(timeoutRef.current)
+ timeoutRef.current = window.setTimeout(() => setCopied(false), 1500)
+ } catch {
+ setCopied(false)
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/dashboard/app/`(dashboard)/docs/page.tsx around lines 203 - 207, The
copy function currently calls navigator.clipboard.writeText(snippet.code)
without awaiting or error handling, causing setCopied(true) to show success even
if the write fails and leaving unhandled promise rejections; modify the copy
function to await navigator.clipboard.writeText(...) inside a try/catch (or use
.then/.catch), only call setCopied(true) after a successful write, handle
rejections by logging or showing an error state (e.g., setCopied(false) and
optionally set an error flag/message), and ensure the existing setTimeout to
revert setCopied(false) only runs after success; update references to copy,
navigator.clipboard.writeText, setCopied accordingly.
| Terms | ||
| </Link> | ||
| <span aria-hidden>·</span> | ||
| <span>© 2026 Gradata</span> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Use a dynamic copyright year.
Hardcoding the year will drift over time; prefer deriving it at render time.
Proposed fix
function LegalFooter() {
+ const year = new Date().getFullYear()
return (
...
- <span>© 2026 Gradata</span>
+ <span>© {year} Gradata</span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/dashboard/src/components/layout/DashboardLayout.tsx` at line 182,
Replace the hardcoded "© 2026 Gradata" string in the DashboardLayout component
with a dynamically computed year (e.g., using new Date().getFullYear()) so the
rendered span shows the current year at runtime; locate the span in
DashboardLayout.tsx (the element currently containing "© 2026 Gradata") and
update its content to include the computed year while preserving the "©" and
"Gradata" text.
…ring Backend: - NotificationPrefs Pydantic model with validated digest_cadence - GET/PUT /users/me/notifications endpoints on users router - migrations/002_notification_prefs.sql adds JSONB column + cadence-keyed index for the future digest scheduler - 6 new tests (78/78 backend pass) Frontend: - Notifications page now loads server prefs on mount, persists via PUT, shows save errors, disables button while saving - TODO(backend) comment removed Co-Authored-By: Gradata <noreply@gradata.ai>
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
…c export Serves pre-rendered HTML to Googlebot/AEO crawlers (vs. blank shell from the previous Vite SPA), unlocking organic SEO and answer-engine discovery. - Next.js 16 App Router with output: 'export' - 5 public routes (home, how-it-works, pricing, docs, + legal stubs) - Per-route metadata, Organization JSON-LD, auto sitemap + robots - V3 Dark Cinematic theme reused from dashboard for brand consistency - GlassCard + NoiseOverlay ported, inline (no heavy shadcn dep) - Cloudflare Pages ready: wrangler.toml, _headers (CSP), _redirects Deploy: cd marketing && pnpm build && wrangler pages deploy out --project-name=gradata-website
…fault) Root .gitignore line 52 ignores all package.json files. Cherry-pick of agent commit 0bc7529 dropped it. Force-added so the marketing site can build via CI. Co-Authored-By: Gradata <noreply@gradata.ai>
There was a problem hiding this comment.
Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/loadtest-nightly.yml:
- Around line 26-27: The current job-level condition using
contains(github.event.head_commit.message, '[skip ci]') is ineffective for
schedule and workflow_dispatch events; update the workflow to add a
workflow_dispatch input (e.g., inputs.skip_ci) and change the job if: expression
to check that input (e.g., if: github.event_name != 'workflow_dispatch' ||
!github.event.inputs.skip_ci) and, if you want to control scheduled runs, add an
optional repository/org-level variable (e.g., SKIP_NIGHTLY) and include it in
the condition (e.g., && env.SKIP_NIGHTLY != 'true'); apply the same fix for the
second occurrence referenced around line 58.
In @.github/workflows/sdk-publish.yml:
- Around line 110-117: The artifact retention for the "Upload distributions"
step (uses: actions/upload-artifact@v4) is set to 7 days but sdk-test.yml uses
14 days; update the retention-days field for this step from 7 to 14 to match
sdk-test.yml (or add a clear inline comment explaining the intentional
difference) so retention periods are consistent across workflows.
- Around line 87-93: The inline Python check currently interpolates ${VERSION}
into the string passed to python -c, which allows shell/Python injection; update
the check invoked in the CI step (the python -c call that reads /tmp/pypi.json
and tests membership of VERSION in d.get('releases',{})) to pass VERSION via an
environment variable or argument instead of interpolating it into the code
string, then read that env/arg inside the Python snippet and use it for the
membership test; ensure the action still sets "already_published=true/false" to
GITHUB_OUTPUT as before.
- Around line 44-48: The current prerelease check uses VERSION with an
unanchored regex; tighten it so pre-release markers are matched only in typical
positions by changing the test on VERSION to use a anchored/position-aware
pattern (e.g., match (rc|a|b|dev|alpha|beta) when preceded by a separator or at
the start), e.g., update the VERSION =~ ... test to something like a regex that
requires the token to follow start or a separator (use VERSION and the existing
if block) so false positives like unrelated substrings are avoided.
In @.github/workflows/sdk-test.yml:
- Around line 41-46: Replace the non-deterministic version pin in the GitHub
Actions step that installs uv (the step named "Install uv" which uses
astral-sh/setup-uv@v4) by setting a fixed version string instead of "latest"
(for example "0.4.x" or the exact current release), so update the version field
in that step to a specific version to ensure reproducible CI runs.
- Around line 27-30: Update project metadata to formally declare Python 3.13
support: in pyproject.toml add the trove classifier "Programming Language ::
Python :: 3.13" to the classifiers list (alongside existing 3.11 and 3.12
entries) and update the ruff configuration's target-version from "py311" to
"py313" so the packaging metadata and static analysis settings match the CI
matrix that tests python-version ["3.11","3.12","3.13"].
In `@cloud/app/models.py`:
- Around line 122-127: The digest_email and slack_webhook fields currently
accept any string; add input validation on the model so only blank or valid
values are persisted: implement validators (e.g. methods named
validate_digest_email and validate_slack_webhook or Pydantic `@validator` hooks on
the digest_email and slack_webhook fields in the same model class) that accept
an empty string or return a normalized value, and otherwise raise a ValueError;
for digest_email validate syntactically as an email address (use Python's
email.utils.parseaddr or a regex/validator), and for slack_webhook validate as a
URL with scheme https and a non-empty netloc (raise on invalid schemes or
malformed URLs) so bad values are rejected at the API boundary.
In `@cloud/app/routes/users.py`:
- Around line 121-125: The update_notifications() handler currently calls
db.update("workspace_members", ...) and returns 200 even when zero rows are
matched; change this so that either (A) you perform an upsert/create of the
workspace_members row before or instead of the update (e.g., insert with ON
CONFLICT to set notification_prefs) or (B) detect the affected-row count
returned by db.update and return a 404/400 (failure) when no row was updated.
Modify the logic in update_notifications() around the db.update(...) call to
check the update result and either create the missing workspace_members row with
the provided notification_prefs or fail the request; keep get_notifications()
behavior unchanged.
In `@cloud/loadtest/k6/authed-read.js`:
- Around line 40-59: The setup probe in authed-read.js uses an API key but calls
/api/v1/users/me which is authorized via get_current_user_id (user JWT), so
change the script to use a consistent auth method: either (A) switch setup() and
all requests to use a user JWT (replace API_KEY usage with a USER_JWT variable
and send Authorization: Bearer <USER_JWT>), or (B) split into two scenarios (one
file or flag) where API-key-based requests target /brains (which uses
get_current_brain) and JWT-based requests target /users/me; update the probe
request in setup() and the subsequent /users/me branch to use the chosen auth
type and variable names accordingly so auth mechanism matches
get_current_user_id vs get_current_brain.
In `@cloud/loadtest/k6/baseline.js`:
- Around line 21-25: The health-check request to '/api/v1/health' is
intentionally returning 404 but k6 counts 4xx as failures (feeding into the
global http_req_failed threshold); update the health probe (the
http.get/http.batch call that requests '/api/v1/health') to include a
responseCallback that treats status 404 as expected/success (i.e., check
response.status === 404 and return true or otherwise mark the response as
successful) so that those 404s are not counted toward the http_req_failed
threshold defined by http_req_failed: ['rate<0.01'].
In `@cloud/loadtest/k6/spike.js`:
- Around line 27-32: The check() at lines 49–52 is informational only; add a k6
Rate metric to enforce "200 or 429 only" and attach a threshold so failures fail
the test: import Rate from 'k6/metrics', create a metric like
allowed_status_rate = new Rate('allowed_status_rate'), in the same place you
currently call check() call allowed_status_rate.add(res.status === 200 ||
res.status === 429), and add a threshold entry in the existing thresholds object
(e.g., 'allowed_status_rate': ['rate==1']) so any 403/404/other non-200/429
responses cause the test to fail.
In `@cloud/migrations/002_notification_prefs.sql`:
- Around line 5-11: The migration currently adds notification_prefs JSONB with
default '{}' and creates index idx_workspace_members_digest_cadence on
(notification_prefs->>'digest_cadence'), which causes users without an explicit
key to be missed; change the migration to persist the product default by (1)
setting the column default JSONB to include "digest_cadence":"weekly" instead of
'{}', (2) backfill existing rows that have NULL or empty notification_prefs to
set {"digest_cadence":"weekly"}, and (3) create the index on the expression
COALESCE(notification_prefs->>'digest_cadence','weekly') (keep the same index
name idx_workspace_members_digest_cadence) so scheduler queries for 'weekly'
match users who never set prefs.
In `@sdk/RELEASE.md`:
- Around line 27-31: The TOML example in RELEASE.md shows both the [project]
table and the name and version fields; simplify it to focus only on the field
the reader needs to change by removing the unnecessary name = "gradata" entry
and leaving the version = "0.5.1" example under the [project] context (or
showing only the version line if you prefer), so update the snippet that
currently contains [project], name and version to only include the version field
reference.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 00883630-7775-4388-b168-ec86dd20fc02
📒 Files selected for processing (15)
.github/workflows/loadtest-nightly.yml.github/workflows/sdk-publish.yml.github/workflows/sdk-test.ymlcloud/app/models.pycloud/app/routes/users.pycloud/dashboard/app/(dashboard)/notifications/page.tsxcloud/loadtest/README.mdcloud/loadtest/k6/authed-read.jscloud/loadtest/k6/baseline.jscloud/loadtest/k6/spike.jscloud/loadtest/k6/sync-write.jscloud/migrations/002_notification_prefs.sqlcloud/tests/test_notifications.pysdk/PYPI-SETUP.mdsdk/RELEASE.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cloudflare Pages
🧰 Additional context used
🪛 actionlint (1.7.12)
.github/workflows/loadtest-nightly.yml
[error] 60-60: "needs" section should not be empty
(syntax-check)
🪛 LanguageTool
sdk/RELEASE.md
[uncategorized] ~4-~4: The official name of this software platform is spelled with a capital “H”.
Context: ...pushed to GitHub. The workflow lives at [.github/workflows/sdk-publish.yml](../.github/...
(GITHUB)
[uncategorized] ~116-~116: The official name of this software platform is spelled with a capital “H”.
Context: ...tests/**, or pyproject.toml triggers [sdk-test.yml`](../.github/workflows/sdk-test.yml) on Python 3.11,...
(GITHUB)
🪛 markdownlint-cli2 (0.22.0)
sdk/PYPI-SETUP.md
[warning] 83-83: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (7)
sdk/PYPI-SETUP.md (1)
1-84: LGTM — Clear and comprehensive documentation for Trusted Publishing setup.The document accurately describes the OIDC-based publishing flow, setup steps, required permissions, and troubleshooting guidance. The permissions block at lines 59-62 correctly matches the actual workflow implementation in
.github/workflows/sdk-publish.yml(lines 149-152 forpublish-pypiand lines 126-128 forpublish-testpypi).The static analysis hint about a missing language specifier on line 83 is a false positive — that line uses inline code formatting, not a fenced code block.
.github/workflows/sdk-test.yml (1)
1-63: LGTM — Well-structured test workflow.The workflow correctly:
- Uses path filters to avoid unnecessary runs
- Employs concurrency control with
cancel-in-progress- Tests across multiple Python versions with
fail-fast: false- Excludes
test_spec_compliance.pywhich requiresgradata_cloud(as documented in the test file'spytest.importorskipguard)- Uploads wheel artifacts for downstream use
sdk/RELEASE.md (1)
1-118: LGTM — Comprehensive release documentation.The document accurately describes the release workflow, including:
- Version bump and tag matching requirements (aligned with workflow's
pyproject.tomlvalidation)- Pre-release handling with TestPyPI routing
- Idempotency behavior matching the workflow's PyPI check logic
- Local verification steps before tagging
The LanguageTool hints about "GitHub" capitalization are false positives — lowercase usage in file paths (
.github/workflows/...) is correct..github/workflows/sdk-publish.yml (2)
119-165: LGTM — Publish jobs are well-structured.Both
publish-testpypiandpublish-pypijobs:
- Correctly gate on the build job outputs
- Use job-level
permissionsfor least-privilege OIDC token access- Use the official
pypa/gh-action-pypi-publishaction with trusted publishingThe separation of pre-release → TestPyPI and final → PyPI routing is clear and correctly implemented.
166-175: Good UX for skipped publishes.The
skippedjob provides clear feedback when a version is already published, helping maintainers understand why no upload occurred and what to do next.cloud/tests/test_notifications.py (1)
20-89: Nice coverage of the new API contract.The no-row, blank-JSON, invalid-cadence, and partial-payload cases are the right regressions to pin down for this endpoint shape.
.github/workflows/loadtest-nightly.yml (1)
60-60: > Likely an incorrect or invalid review comment.
| # Skip if the triggering commit message contains [skip ci] | ||
| if: "!contains(github.event.head_commit.message, '[skip ci]')" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/loadtest-nightly.yml | head -100Repository: Gradata/gradata
Length of output: 3958
🌐 Web query:
github actions schedule trigger event payload properties documentation
💡 Result:
The GitHub Actions schedule trigger event does not have a traditional webhook payload like other events. Instead, when a workflow is triggered by on: schedule, the github.event context is minimally populated. Key properties and characteristics from official documentation: - github.event_name is "schedule" {value: "schedule", citations: [1,6,19,22], confidence: "high"}. - github.event.schedule contains the cron expression string that triggered the run (e.g., '30 5 * * 1,3'). This allows distinguishing between multiple schedules in one workflow {value: cron string, citations: [1,6,18,29], confidence: "high"}. - Prior to September 2022, github.event only included the cron; now it also includes standard repository, organization, and enterprise objects {value: includes repo/org/enterprise info, citations: [13], confidence: "high"}. - No action, client_payload, or event-specific data beyond the above, unlike workflow_dispatch (which has inputs) or repository_dispatch {value: minimal payload, citations: [1,2,19], confidence: "high"}. - Standard github context properties are available: github.sha (latest commit on default branch), github.ref (default branch), github.repository, etc. {value: standard github context, citations: [1,6,19], confidence: "high"}. - Workflows run only on the default branch's latest commit; may be delayed during high load {value: runs on default branch, citations: [1,6], confidence: "high"}. To inspect the full github.event in a workflow: steps: - env: EVENT_CONTEXT: ${{ toJSON(github.event) }} run: echo "$EVENT_CONTEXT" {value: dump example, citations: [4,19], confidence: "high"}. Official sources confirm no detailed webhook payload documentation for schedule as it's an internal timer event, not a webhook {value: no full schema in docs, citations: [1,2,19], confidence: "high"}.
Citations:
- 1: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows
- 2: https://docs.github.com/en/webhooks/webhook-events-and-payloads
- 3: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow
- 4: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
- 5: https://github.blog/changelog/2022-09-27-github-actions-additional-information-available-in-github-event-payload-for-scheduled-workflow-runs
- 6: https://docs.github.com/en/enterprise-server@3.1/actions/using-workflows/events-that-trigger-workflows
- 7: https://docs.github.com/en/actions/reference/contexts-reference
- 8: https://stackoverflow.com/questions/70281670/checking-condition-when-step-is-running-on-schedule-in-github-actions
- 9: https://github.com/Azure/azure-sdk-for-net/blob/main/.github/workflows/scheduled-event-processor.yml
🌐 Web query:
github actions workflow_dispatch trigger event payload head_commit documentation
💡 Result:
The workflow_dispatch event in GitHub Actions does not include a head_commit field in its event payload. Unlike push or pull_request events, which provide details like github.event.head_commit (commit message, author, etc.), the workflow_dispatch payload primarily contains inputs provided during manual triggering (via UI, CLI, or API). Key characteristics from official documentation: - Event payload accessible via ${{ github.event }} or ${{ inputs }} (inputs context preserves Booleans). - When triggered, GITHUB_SHA is the latest commit SHA on the specified ref (branch/tag provided in dispatch request, defaults to default branch). - GITHUB_REF is the full ref of that branch/tag (e.g., refs/heads/main). - No head_commit object; use git log -1 after actions/checkout for commit details if needed. - Full payload can be inspected by dumping ${{ toJSON(github.event) }}. Example workflow snippet to log event: steps: - run: echo '${{ toJSON(github.event) }}' Typical structure (inferred from docs and examples): github: event: name: workflow_dispatch inputs: { ...user-provided inputs... } ref: "refs/heads/main" # or specified ref Official sources confirm no head_commit for this event; it's not a push-like webhook with commit history.
Citations:
- 1: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows
- 2: https://docs.github.com/actions/using-workflows/triggering-a-workflow
- 3: https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=dismiss
- 4: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
- 5: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow
- 6: https://docs.github.com/en/actions/reference/contexts-reference
- 7: https://stackoverflow.com/questions/69925264/correct-request-with-client-payload-to-run-workflow-dispatch-in-github-action
- 8: https://stackoverflow.com/questions/70018912/how-to-send-data-payload-using-http-request-to-github-actions-workflow
- 9: https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
- 10: https://stackoverflow.com/questions/70151645/how-can-i-trigger-a-workflow-dispatch-from-the-github-api
🌐 Web query:
github actions expressions null coercion string functions contains behavior
💡 Result:
In GitHub Actions expressions, null values are coerced to an empty string ('') when used with string functions like contains, due to the explicit casting rules documented in the official GitHub documentation. Key behaviors: - Null coerces to '' for string casting (used by contains). - Empty string (search='') contains empty string (item='') returns true, since '' is a substring of ''. - Null as search coerces to '', so contains(null, '') effectively becomes contains('', '') = true. - contains(null, 'foo') becomes contains('', 'foo') = false (case-insensitive substring check). - contains('hello', null) becomes contains('hello', '') = true. - For arrays, contains coerces item to string but treats search as array if possible; null item coerces to ''. In conditionals (if), null/'' coerces to false. Examples: contains('Hello world', 'llo') -> true contains(github.event.issue.labels.*.name, 'bug') -> true if label exists contains(fromJSON('["push", "pull_request"]'), github.event_name) -> true if matches Sources confirm casting table: Null -> '' (high confidence from multiple official docs). Behavior follows standard substring rules post-coercion (medium confidence, no direct null example but logical from docs).
Citations:
- 1: https://docs.github.com/actions/reference/workflows-and-actions/expressions
- 2: https://docs.github.com/en/actions/reference/evaluate-expressions-in-workflows-and-actions
- 3: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions
- 4: https://docs.github.com/actions/reference/evaluate-expressions-in-workflows-and-actions
- 5: https://docs.github.com/en/actions/learn-github-actions/expressions
🌐 Web query:
github actions needs empty array syntax validation
💡 Result:
In GitHub Actions workflows, the needs keyword under jobs.<job_id> specifies prerequisite jobs that must complete successfully before the current job runs. It accepts either a single string (job ID) or an array of strings (multiple job IDs). Official documentation examples always show non-empty values, such as needs: job1 or needs: [job1, job2]. No official documentation explicitly states whether needs: [] (empty array) is valid syntax. However, GitHub Actions parses and executes workflows with valid YAML structure, and empty arrays are standard YAML. Linters like action-validator and actionlint focus on schema compliance and expression errors but do not flag empty needs: [] as invalid in available reports. In practice: - Omitting needs entirely means the job has no prerequisites and runs in parallel with others. - Using needs: [] semantically indicates no prerequisites, equivalent to omitting it, and should not cause validation errors or runtime failures based on schema descriptions ("string or array of strings"). If using a linter like action-validator reports an error for needs: [], it may be due to strict schema interpretation treating the array as requiring items, but this is not confirmed in GitHub's runtime behavior. Test in a repository to verify, as GitHub's parser is authoritative. Example valid syntax (non-empty): jobs: job2: needs: job1 job3: needs: [job1, job2] For no dependencies, prefer omitting needs over needs: [] for clarity.
Citations:
- 1: https://docs.github.com/actions/reference/workflows-and-actions/workflow-syntax
- 2: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
- 3: https://docs.github.com/actions/reference/workflow-syntax-for-github-actions
- 4: https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow
- 5: [plan] Fix missing
needs:declarations in 4 workflow files (22 actionlint expression errors) github/gh-aw#17669 - 6: Fix missing
needs:declarations causing 22 actionlint expression errors in 4 workflows github/gh-aw#17681
[skip ci] is a no-op for every trigger this workflow supports.
This workflow only runs on schedule and workflow_dispatch, but github.event.head_commit.message is not present for either event. GitHub's expression rules coerce null to '' for contains(), so contains(github.event.head_commit.message, '[skip ci]') is always false and the negated job condition is always true. The documented skip mechanism therefore never actually skips either job.
Use an explicit workflow_dispatch boolean input (and, if you want to pause scheduled runs, a repo/org variable) instead of a commit-message check.
Also applies to: line 58
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/loadtest-nightly.yml around lines 26 - 27, The current
job-level condition using contains(github.event.head_commit.message, '[skip
ci]') is ineffective for schedule and workflow_dispatch events; update the
workflow to add a workflow_dispatch input (e.g., inputs.skip_ci) and change the
job if: expression to check that input (e.g., if: github.event_name !=
'workflow_dispatch' || !github.event.inputs.skip_ci) and, if you want to control
scheduled runs, add an optional repository/org-level variable (e.g.,
SKIP_NIGHTLY) and include it in the condition (e.g., && env.SKIP_NIGHTLY !=
'true'); apply the same fix for the second occurrence referenced around line 58.
| if [[ "${VERSION}" =~ (rc|a|b|dev|alpha|beta) ]]; then | ||
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | ||
| fi |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Pre-release regex may have false positives.
The regex (rc|a|b|dev|alpha|beta) matches anywhere in the version string. A version like 1.0.0-database or 2.0.0a1 would match correctly, but a hypothetical version like 1.0.0+metadata wouldn't false-match. This seems reasonable for standard PEP 440 versions.
However, the pattern could be made more precise by anchoring to typical pre-release positions:
♻️ Optional: More precise pre-release detection
- if [[ "${VERSION}" =~ (rc|a|b|dev|alpha|beta) ]]; then
+ if [[ "${VERSION}" =~ (rc|a|b|dev|alpha|beta)[0-9]* ]]; thenThis is optional since the current pattern works correctly for standard PEP 440 pre-release versions (0.5.1rc1, 0.5.1a2, 0.5.1.dev1, etc.).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if [[ "${VERSION}" =~ (rc|a|b|dev|alpha|beta) ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [[ "${VERSION}" =~ (rc|a|b|dev|alpha|beta)[0-9]* ]]; then | |
| echo "is_prerelease=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_prerelease=false" >> "$GITHUB_OUTPUT" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/sdk-publish.yml around lines 44 - 48, The current
prerelease check uses VERSION with an unanchored regex; tighten it so
pre-release markers are matched only in typical positions by changing the test
on VERSION to use a anchored/position-aware pattern (e.g., match
(rc|a|b|dev|alpha|beta) when preceded by a separator or at the start), e.g.,
update the VERSION =~ ... test to something like a regex that requires the token
to follow start or a separator (use VERSION and the existing if block) so false
positives like unrelated substrings are avoided.
| if python -c "import json,sys; d=json.load(open('/tmp/pypi.json')); sys.exit(0 if '${VERSION}' in d.get('releases',{}) else 1)"; then | ||
| echo "::warning::gradata==${VERSION} is already published to ${INDEX_NAME}. Skipping publish step." | ||
| echo "already_published=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "gradata==${VERSION} not yet on ${INDEX_NAME}. Will publish." | ||
| echo "already_published=false" >> "$GITHUB_OUTPUT" | ||
| fi |
There was a problem hiding this comment.
Potential shell injection in Python inline script.
The ${VERSION} variable is interpolated directly into a Python string literal. While the attack surface is limited (requires repository write access to create tags), a maliciously crafted tag name could break out of the string context.
🛡️ Safer approach using environment variable
- if python -c "import json,sys; d=json.load(open('/tmp/pypi.json')); sys.exit(0 if '${VERSION}' in d.get('releases',{}) else 1)"; then
+ if VERSION="${VERSION}" python -c "import json,sys,os; d=json.load(open('/tmp/pypi.json')); sys.exit(0 if os.environ['VERSION'] in d.get('releases',{}) else 1)"; thenThis passes the version via environment variable rather than string interpolation, eliminating the injection vector.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if python -c "import json,sys; d=json.load(open('/tmp/pypi.json')); sys.exit(0 if '${VERSION}' in d.get('releases',{}) else 1)"; then | |
| echo "::warning::gradata==${VERSION} is already published to ${INDEX_NAME}. Skipping publish step." | |
| echo "already_published=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "gradata==${VERSION} not yet on ${INDEX_NAME}. Will publish." | |
| echo "already_published=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| if VERSION="${VERSION}" python -c "import json,sys,os; d=json.load(open('/tmp/pypi.json')); sys.exit(0 if os.environ['VERSION'] in d.get('releases',{}) else 1)"; then | |
| echo "::warning::gradata==${VERSION} is already published to ${INDEX_NAME}. Skipping publish step." | |
| echo "already_published=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "gradata==${VERSION} not yet on ${INDEX_NAME}. Will publish." | |
| echo "already_published=false" >> "$GITHUB_OUTPUT" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/sdk-publish.yml around lines 87 - 93, The inline Python
check currently interpolates ${VERSION} into the string passed to python -c,
which allows shell/Python injection; update the check invoked in the CI step
(the python -c call that reads /tmp/pypi.json and tests membership of VERSION in
d.get('releases',{})) to pass VERSION via an environment variable or argument
instead of interpolating it into the code string, then read that env/arg inside
the Python snippet and use it for the membership test; ensure the action still
sets "already_published=true/false" to GITHUB_OUTPUT as before.
| - name: Upload distributions | ||
| if: steps.check.outputs.already_published != 'true' | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: dist | ||
| path: dist/ | ||
| if-no-files-found: error | ||
| retention-days: 7 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Artifact retention is shorter than sdk-test.yml.
This workflow uses 7-day retention for artifacts (line 117) while sdk-test.yml uses 14 days (line 63). Consider aligning retention periods for consistency, or document the intentional difference.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/sdk-publish.yml around lines 110 - 117, The artifact
retention for the "Upload distributions" step (uses: actions/upload-artifact@v4)
is set to 7 days but sdk-test.yml uses 14 days; update the retention-days field
for this step from 7 to 14 to match sdk-test.yml (or add a clear inline comment
explaining the intentional difference) so retention periods are consistent
across workflows.
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| python-version: ["3.11", "3.12", "3.13"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if Python version constraints in pyproject.toml support 3.13
cat pyproject.toml | grep -A5 "requires-python"
# Check for any explicit version pins that might conflict
cat pyproject.toml | grep -E "(python_requires|Programming Language :: Python)"Repository: Gradata/gradata
Length of output: 363
🏁 Script executed:
#!/bin/bash
# Get complete classifiers section
cat pyproject.toml | grep -A 20 "classifiers = \["Repository: Gradata/gradata
Length of output: 1015
🏁 Script executed:
#!/bin/bash
# Also check if there are any Python 3.13-specific issues in dependencies
# Look for the complete dependencies section
cat pyproject.toml | grep -A 50 "dependencies = \["Repository: Gradata/gradata
Length of output: 1595
Update classifiers and ruff configuration to formally declare Python 3.13 support.
The workflow tests Python 3.13, but pyproject.toml doesn't officially declare this support:
- Classifiers list only 3.11 and 3.12 (missing
"Programming Language :: Python :: 3.13") - Ruff's
target-versionis set to"py311"instead of"py313"
While dependency constraints use flexible versioning (e.g., >=2.0.0) that should work with 3.13, the metadata inconsistency should be resolved to match the tested versions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/sdk-test.yml around lines 27 - 30, Update project metadata
to formally declare Python 3.13 support: in pyproject.toml add the trove
classifier "Programming Language :: Python :: 3.13" to the classifiers list
(alongside existing 3.11 and 3.12 entries) and update the ruff configuration's
target-version from "py311" to "py313" so the packaging metadata and static
analysis settings match the CI matrix that tests python-version
["3.11","3.12","3.13"].
| export function setup() { | ||
| if (!API_KEY) { | ||
| // Fail LOUD — don't let CI pretend this passed. | ||
| fail( | ||
| 'K6_API_KEY is unset. Set K6_API_KEY=gd_... (a real API key from a test ' + | ||
| 'workspace) to run authed-read.js. Refusing to run without auth.' | ||
| ); | ||
| } | ||
| // Sanity check: make sure the key actually works before burning the full run. | ||
| const probe = http.get(`${BASE_URL}/api/v1/users/me`, { | ||
| headers: { Authorization: `Bearer ${API_KEY}` }, | ||
| tags: { endpoint: 'setup_probe' }, | ||
| }); | ||
| if (probe.status !== 200) { | ||
| fail( | ||
| `Setup probe failed: GET /api/v1/users/me returned ${probe.status}. ` + | ||
| `Check K6_API_KEY is a valid gd_* key. Body: ${probe.body}` | ||
| ); | ||
| } | ||
| return { baseUrl: BASE_URL }; |
There was a problem hiding this comment.
/users/me cannot be exercised with the credential this script asks for.
This file requires a gd_* API key, but /users/me is wired through get_current_user_id in cloud/app/routes/users.py:18-21, while API-key auth is handled by get_current_brain in cloud/app/auth.py:93-111 and used by /brains in cloud/app/routes/brains.py:43-60. As written, the setup probe on Lines 49-57 and the /users/me branch on Lines 88-101 will 401 when the operator follows the script’s own instructions.
Use a user JWT for both endpoints, or split this into separate JWT and API-key scenarios.
Also applies to: 68-103
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/loadtest/k6/authed-read.js` around lines 40 - 59, The setup probe in
authed-read.js uses an API key but calls /api/v1/users/me which is authorized
via get_current_user_id (user JWT), so change the script to use a consistent
auth method: either (A) switch setup() and all requests to use a user JWT
(replace API_KEY usage with a USER_JWT variable and send Authorization: Bearer
<USER_JWT>), or (B) split into two scenarios (one file or flag) where
API-key-based requests target /brains (which uses get_current_brain) and
JWT-based requests target /users/me; update the probe request in setup() and the
subsequent /users/me branch to use the chosen auth type and variable names
accordingly so auth mechanism matches get_current_user_id vs get_current_brain.
| thresholds: { | ||
| // SLO: p95 latency under 500ms, error rate under 1% | ||
| http_req_duration: ['p(95)<500'], | ||
| http_req_failed: ['rate<0.01'], | ||
| errors: ['rate<0.01'], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n cloud/loadtest/k6/baseline.jsRepository: Gradata/gradata
Length of output: 3010
🌐 Web query:
k6 http_req_failed default behavior 4xx 5xx responses expected status
💡 Result:
By default, the k6 http_req_failed Rate metric measures the rate of HTTP requests that are considered "failed" according to the response callback logic. Without any custom configuration, k6 considers HTTP responses with status codes between 200 and 399 (inclusive) as "expected" (successful), and all others as failed. This includes: - Network/connection errors (e.g., timeouts, DNS failures, TLS errors), which result in response.status = 0 and contribute to http_req_failed. - HTTP 4xx client errors (e.g., 400, 401, 404, 429). - HTTP 5xx server errors (e.g., 500, 502, 503, 504). - HTTP 3xx redirects are successful by default (expected_response: true), so they do NOT count as failed unless customized. The metric emits 1 for failed requests (non-expected) and 0 for successful ones. The rate is the proportion of 1s (failed requests). In CLI output, for http_req_failed{rate=0.00%}, it shows ✓ [number of 0s/successes] ✗ [number of 1s/failures], which appears "reversed" due to the metric name but correctly indicates low failure rate. To customize what counts as failed (e.g., expect specific 4xx/5xx or exclude some), use http.setResponseCallback with http.expectedStatuses globally or per-request via Params.responseCallback. For functional validation of specific status codes, use check functions instead of or alongside http_req_failed, and set thresholds on the checks metric. Examples: import http from 'k6/http'; // Default: 200-399 OK http.setResponseCallback(http.expectedStatuses(201, 404)); // Only expect exactly 201 or 404 Threshold example: export const options = { thresholds: { http_req_failed: ['rate<0.01'] } }; This fails the test if >1% failed requests.
Citations:
- 1: http://grafana.com/docs/k6/latest/using-k6/metrics/reference/
- 2: https://k6.io/docs/javascript-api/k6-http/setresponsecallback
- 3: https://grafana.com/docs/k6/latest/using-k6/http-requests/
- 4: https://github.com/grafana/k6-learn/blob/main/Modules/II-k6-Foundations/07-Setting-test-criteria-with-thresholds.md
- 5: https://grafana.com/docs/k6/latest/using-k6/thresholds/
- 6: https://k6.io/docs/javascript-api/k6-http/
- 7: https://k6.io/docs/javascript-api/k6-http/expectedstatuses/
🌐 Web query:
k6 http.expectedStatuses syntax exact API format
💡 Result:
The exact API syntax for http.expectedStatuses in k6 is: http.expectedStatuses(statuses...) It returns a callback function to be used with http.setResponseCallback. This callback marks HTTP responses as "expected" (setting the expected_response: true tag) if their status code matches any of the provided statuses arguments. Parameters: - statuses: Variable number of arguments (zero or more), each can be: - An integer (exact status code, e.g., 200, 404) - An object with min and max properties defining a range (e.g., { min: 200, max: 299 }) Example from official documentation: import http from 'k6/http'; http.setResponseCallback( http.expectedStatuses(406, 500, { min: 200, max: 204 }, 302, { min: 305, max: 405 }) ); export default => { http.get('https://example.com/api/status/205'); // This would fail as 205 doesn't match }; Note: By default, k6 considers status codes 200-399 as expected. Use expectedStatuses to customize this globally via setResponseCallback. Status checks can also be done per-request using check from the k6 module, e.g.: import { check } from 'k6'; import http from 'k6/http'; const res = http.get('https://test.k6.io'); check(res, { 'status is 200': (r) => r.status === 200, });
Citations:
- 1: https://grafana.com/docs/k6/latest/javascript-api/k6-http/expected-statuses/
- 2: https://k6.io/docs/javascript-api/k6-http/expectedstatuses/
- 3: https://grafana.com/docs/k6/latest/using-k6/http-requests/
- 4: https://k6.io/docs/using-k6/checks
- 5: https://grafana.com/docs/k6/latest/using-k6/checks/
Mark the intentional 404 as expected, or remove this probe.
Lines 50-58 request /api/v1/health, documented as potentially returning 404. However, k6 by default counts 4xx/5xx responses as failures and feeds them into the http_req_failed metric. With the global threshold http_req_failed: ['rate<0.01'] on lines 24, a consistent 404 will exceed the 1% failure tolerance and fail the test.
Use a responseCallback to mark 404 as an expected response:
Suggested fix
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const BASE_URL = __ENV.BASE_URL || 'https://gradata-production.up.railway.app';
+const okOr404 = http.expectedStatuses({ min: 200, max: 399 }, 404);
const errorRate = new Rate('errors');
@@
const apiHealthRes = http.get(`${BASE_URL}/api/v1/health`, {
tags: { endpoint: 'api_health' },
+ responseCallback: okOr404,
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/loadtest/k6/baseline.js` around lines 21 - 25, The health-check request
to '/api/v1/health' is intentionally returning 404 but k6 counts 4xx as failures
(feeding into the global http_req_failed threshold); update the health probe
(the http.get/http.batch call that requests '/api/v1/health') to include a
responseCallback that treats status 404 as expected/success (i.e., check
response.status === 404 and return true or otherwise mark the response as
successful) so that those 404s are not counted toward the http_req_failed
threshold defined by http_req_failed: ['rate<0.01'].
| thresholds: { | ||
| // The ONLY hard SLO: zero 5xx. SlowAPI should return 429, never 500. | ||
| five_xx_rate: ['rate==0'], | ||
| // Keep an eye on p99 but don't fail on it — spike tests expect slow tails. | ||
| http_req_duration: ['p(99)<3000'], | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "spike.js" -type fRepository: Gradata/gradata
Length of output: 87
🏁 Script executed:
cat -n cloud/loadtest/k6/spike.js | head -60Repository: Gradata/gradata
Length of output: 2424
🌐 Web query:
k6 grafana check failure threshold behavior documentation
💡 Result:
In k6 (integrated with Grafana), checks validate conditions like response status or body content but do not affect test pass/fail status by themselves. Each check failure contributes to a 'checks' rate metric tracking the pass rate across all checks. To fail the test based on check failures, define a threshold on the 'checks' metric in options.thresholds, e.g.: export const options = { thresholds: { checks: ['rate>0.9'], // Overall check pass rate must exceed 90% }, }; Threshold behavior: - Evaluated at test end (or periodically with abortOnFail: true). - 'rate>0.9' passes if >=90% of all check invocations succeeded; fails otherwise, causing non-zero exit code. - Thresholds specify pass conditions; false evaluation fails the test. - For tagged checks (e.g., check(res, {...}, {tag: 'value'})), use 'checks{tag:value}': ['rate>0.95'] for specific subsets. - Aggregate across all checks unless tagged; individual check names create separate rate metrics but untagged 'checks' threshold applies to global aggregate. Example output on pass: checks.........................: 98.84% ✓ 515 ✗ 6 On fail (e.g., GitHub issue #5316): threshold false despite 98.8% due to one specific check below threshold (per-check grouping). Use for CI/CD: exit code 0=pass, non-0=fail (often 99 for thresholds).
Citations:
- 1: https://grafana.com/docs/k6/latest/using-k6/thresholds/
- 2: https://grafana.com/docs/k6/next/using-k6/thresholds/
- 3: https://grafana.com/docs/k6/latest/using-k6/checks/
- 4: Threshold check fails but value is ok? grafana/k6#5316
- 5: https://k6.io/docs/using-k6/checks
- 6: http://grafana.com/docs/k6/latest/using-k6/thresholds/
- 7: https://github.com/grafana/k6-learn/blob/main/Modules/II-k6-Foundations/07-Setting-test-criteria-with-thresholds.md
- 8: https://grafana.com/docs/k6/latest/using-k6/checks
Add a threshold to enforce "200 or 429 only" assertion.
The check() call at lines 49–52 is informational only: in k6, check failures do not cause the test to fail unless you attach a threshold to them. This means the test can pass on sustained 403/404 responses as long as no 5xx status is returned and latency stays under the threshold. Add a Rate metric and corresponding threshold to enforce that only 200 and 429 responses are acceptable.
Suggested fix
import http from 'k6/http';
import { check } from 'k6';
import { Rate, Counter } from 'k6/metrics';
const fiveXX = new Rate('five_xx_rate');
+const unexpectedStatus = new Rate('unexpected_status_rate');
const rateLimited = new Counter('rate_limited_429');
const successes = new Counter('success_200');
thresholds: {
// The ONLY hard SLO: zero 5xx. SlowAPI should return 429, never 500.
five_xx_rate: ['rate==0'],
+ unexpected_status_rate: ['rate==0'],
// Keep an eye on p99 but don't fail on it — spike tests expect slow tails.
http_req_duration: ['p(99)<3000'],
},
- check(res, {
+ const ok = check(res, {
'no 5xx response': (r) => r.status < 500,
'status is 200 or 429': (r) => r.status === 200 || r.status === 429,
});
+ unexpectedStatus.add(!ok);
}Also applies to: lines 49–52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/loadtest/k6/spike.js` around lines 27 - 32, The check() at lines 49–52
is informational only; add a k6 Rate metric to enforce "200 or 429 only" and
attach a threshold so failures fail the test: import Rate from 'k6/metrics',
create a metric like allowed_status_rate = new Rate('allowed_status_rate'), in
the same place you currently call check() call
allowed_status_rate.add(res.status === 200 || res.status === 429), and add a
threshold entry in the existing thresholds object (e.g., 'allowed_status_rate':
['rate==1']) so any 403/404/other non-200/429 responses cause the test to fail.
| ALTER TABLE workspace_members | ||
| ADD COLUMN IF NOT EXISTS notification_prefs JSONB NOT NULL DEFAULT '{}'::jsonb; | ||
|
|
||
| -- Index on the cadence key — used by the future digest scheduler to fetch | ||
| -- "all users with digest_cadence = 'weekly'" without a full table scan. | ||
| CREATE INDEX IF NOT EXISTS idx_workspace_members_digest_cadence | ||
| ON workspace_members ((notification_prefs->>'digest_cadence')); |
There was a problem hiding this comment.
Persist the “weekly by default” behavior in storage too.
The API treats missing prefs as digest_cadence="weekly", but this migration stores {} and indexes the raw JSON key. Any scheduler query like notification_prefs->>'digest_cadence' = 'weekly' will skip users who never opened this page, even though the product says they should receive the weekly digest by default.
Suggested migration adjustment
ALTER TABLE workspace_members
- ADD COLUMN IF NOT EXISTS notification_prefs JSONB NOT NULL DEFAULT '{}'::jsonb;
+ ADD COLUMN IF NOT EXISTS notification_prefs JSONB NOT NULL DEFAULT
+ '{"alert_correction_spike":true,"alert_rule_regression":true,"alert_meta_rule_emerged":false,"digest_cadence":"weekly","digest_email":"","slack_webhook":""}'::jsonb;
CREATE INDEX IF NOT EXISTS idx_workspace_members_digest_cadence
- ON workspace_members ((notification_prefs->>'digest_cadence'));
+ ON workspace_members ((coalesce(notification_prefs->>'digest_cadence', 'weekly')));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ALTER TABLE workspace_members | |
| ADD COLUMN IF NOT EXISTS notification_prefs JSONB NOT NULL DEFAULT '{}'::jsonb; | |
| -- Index on the cadence key — used by the future digest scheduler to fetch | |
| -- "all users with digest_cadence = 'weekly'" without a full table scan. | |
| CREATE INDEX IF NOT EXISTS idx_workspace_members_digest_cadence | |
| ON workspace_members ((notification_prefs->>'digest_cadence')); | |
| ALTER TABLE workspace_members | |
| ADD COLUMN IF NOT EXISTS notification_prefs JSONB NOT NULL DEFAULT | |
| '{"alert_correction_spike":true,"alert_rule_regression":true,"alert_meta_rule_emerged":false,"digest_cadence":"weekly","digest_email":"","slack_webhook":""}'::jsonb; | |
| -- Index on the cadence key — used by the future digest scheduler to fetch | |
| -- "all users with digest_cadence = 'weekly'" without a full table scan. | |
| CREATE INDEX IF NOT EXISTS idx_workspace_members_digest_cadence | |
| ON workspace_members ((coalesce(notification_prefs->>'digest_cadence', 'weekly'))); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cloud/migrations/002_notification_prefs.sql` around lines 5 - 11, The
migration currently adds notification_prefs JSONB with default '{}' and creates
index idx_workspace_members_digest_cadence on
(notification_prefs->>'digest_cadence'), which causes users without an explicit
key to be missed; change the migration to persist the product default by (1)
setting the column default JSONB to include "digest_cadence":"weekly" instead of
'{}', (2) backfill existing rows that have NULL or empty notification_prefs to
set {"digest_cadence":"weekly"}, and (3) create the index on the expression
COALESCE(notification_prefs->>'digest_cadence','weekly') (keep the same index
name idx_workspace_members_digest_cadence) so scheduler queries for 'weekly'
match users who never set prefs.
| ```toml | ||
| [project] | ||
| name = "gradata" | ||
| version = "0.5.1" | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Simplify the TOML example to show only the relevant field.
The example includes the name field which doesn't need to be changed. Consider showing only the version field to focus attention on what actually needs updating.
📝 Suggested simplification
```toml
- [project]
- name = "gradata"
version = "0.5.1"
```🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sdk/RELEASE.md` around lines 27 - 31, The TOML example in RELEASE.md shows
both the [project] table and the name and version fields; simplify it to focus
only on the field the reader needs to change by removing the unnecessary name =
"gradata" entry and leaving the version = "0.5.1" example under the [project]
context (or showing only the version line if you prefer), so update the snippet
that currently contains [project], name and version to only include the version
field reference.
Summary
Three follow-ups landed on the dashboard branch after PR #25 merged. Building on the production-deployed state.
1. Branded Supabase email templates (
f2cc259)6 inline-CSS table-layout HTML templates for Supabase's transactional emails —
cloud/supabase/email-templates/:confirm-signup.html— Confirm your Gradata accountmagic-link.html— Sign in to Gradatachange-email.html— Confirm your new emailreset-password.html— Reset your Gradata passwordinvite-user.html— You've been invited to a Gradata workspacereauthentication.html— Confirm it's you (with prominent OTP)V3 Dark Cinematic styling (gradient CTA,
#0C1120bg, system-ui fonts), email-client safe (no<style>, no flexbox, no external images), ~96-117 lines each. Uses Supabase template variables ({{ .ConfirmationURL }},{{ .Token }},{{ .Email }}, etc.). CompanionREADME.mddocuments subjects, variables, and 5-step install in Supabase dashboard.2. Privacy Policy + Terms of Service (
5a08cf5)Two public pages at
/legal/privacyand/legal/terms(no auth required). Tied into the dashboard via:<AuthLegalLinks />shown on login/signup/forgot-password<LegalFooter />after children inside DashboardLayoutV3-themed using GlassCard. Plain-language, Gradata-specific copy:
24 static routes total now (was 22).
3. Auto-deploy CI workflow (
18323ec).github/workflows/dashboard-source-maps.yml— replaces the broken Cloudflare-native git-builder with a GitHub Action:maintouchingcloud/dashboard/**(andworkflow_dispatch)[skip ci]in commit messageout/via wranglerPlus
cloud/dashboard/SOURCE-MAPS.md(operations doc).Required GitHub Actions secrets
For the workflow to actually run successfully, set these in GitHub repo Settings → Secrets:
SENTRY_AUTH_TOKENproject:write+project:releasesSENTRY_ORGgradataSENTRY_PROJECTgradata-dashboardCLOUDFLARE_API_TOKENCloudflare Pages: EditpermissionCLOUDFLARE_ACCOUNT_IDd568e4421afe0100d09df9e4d29bef81NEXT_PUBLIC_SUPABASE_URLhttps://miqwilxheuxwafvmoajs.supabase.coNEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_API_URLhttps://gradata-production.up.railway.app/api/v1(orapi.gradata.aionce SSL fixed)NEXT_PUBLIC_SENTRY_DSNTest plan
pnpm buildsucceeds, 24 static routescloud/dashboard/app/legal/privacy+termsrender with V3 theme/legal/privacyto verifyGenerated with Gradata