chore: import upstream snapshot with attribution
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:40:13 +08:00
commit e30e75b5d4
3893 changed files with 533074 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# CI and repository policy changes require maintainer review.
/.github/CODEOWNERS @Yonom @okisdev
/.github/actions/ @assistant-ui/maintainers
/.github/workflows/ @assistant-ui/maintainers
# Public API surface snapshots require maintainer review.
/api-surface/ @Yonom @okisdev
+108
View File
@@ -0,0 +1,108 @@
name: Bug report
description: Report an issue to help us improve assistant-ui
labels: ["bug"]
body:
- type: checkboxes
attributes:
label: Checklist
description: Please confirm before submitting.
options:
- label: I have searched existing issues and this is not a duplicate
required: true
- label: This is a reproducible bug, not a usage question (use [Discord](https://discord.gg/S9dwgCNEFs) for questions)
required: true
- type: textarea
attributes:
label: Environment info
description: Run `npx assistant-ui info` in your project and paste the output here.
placeholder: |
Environment:
OS: macOS 15.3 (arm64)
Node.js: v22.14.0
Package Manager: pnpm 10.32.1
Framework: Next.js 15.3.1
Packages:
@assistant-ui/react 0.12.15
...
validations:
required: true
- type: dropdown
attributes:
label: Affected area(s)
multiple: true
options:
- Runtime / Core
- Thread Management
- Streaming (assistant-stream)
- Attachments / Files
- Cloud (assistant-cloud)
- React / UI Components
- React Native
- Store / Tap
- Content Rendering (Markdown, Syntax Highlighting)
- Rich Text / Composer (Lexical)
- Provider Adapters
- Performance
- CLI / Scaffolding
- DevTools / Observability
- Documentation
- Unsure
- type: dropdown
attributes:
label: Runtime adapter in use
description: Which runtime hook are you using?
options:
- useLocalRuntime
- useExternalStoreRuntime
- useCloudRuntime
- useAISDKRuntime / useChatRuntime
- useLangGraphRuntime
- useAgUiRuntime
- useA2ARuntime
- useAdkRuntime (Google ADK)
- useDataStreamRuntime
- useAssistantTransportRuntime
- Other / Custom
- Not applicable
- Unsure
- type: textarea
attributes:
label: Steps to reproduce
description: Minimal steps or a link to a reproduction repo.
placeholder: |
1. Set up runtime with `useXxxRuntime(...)`
2. Send a message with ...
3. Observe ...
validations:
required: true
- type: textarea
attributes:
label: Expected vs actual behavior
placeholder: |
Expected: ...
Actual: ...
validations:
required: true
- type: input
attributes:
label: Browser (if applicable)
placeholder: "e.g. Chrome 131, Firefox 135, Safari 18"
- type: textarea
attributes:
label: Code snippet or error log
render: tsx
description: Relevant code, stack traces, or console output. Please remove any secrets.
- type: textarea
attributes:
label: Additional context
description: Screenshots, related issues, or anything else that might help.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question or help
url: https://discord.gg/S9dwgCNEFs
about: Ask questions and get help from the community on Discord.
- name: Documentation
url: https://www.assistant-ui.com/docs
about: Check the docs — your question might already be answered.
@@ -0,0 +1,48 @@
name: Feature request
description: Suggest an improvement or new capability for assistant-ui
labels: ["enhancement"]
body:
- type: textarea
attributes:
label: Problem statement
description: What problem are you trying to solve?
placeholder: I'm trying to ... but currently ...
validations:
required: true
- type: textarea
attributes:
label: Proposed solution
description: If you have an idea for an API, component, or behavior change, describe it here.
- type: dropdown
attributes:
label: Area
options:
- Runtime / Core
- Thread Management
- Streaming (assistant-stream)
- Attachments / Files
- Cloud (assistant-cloud)
- React / UI Components
- React Native
- Store / Tap
- Content Rendering (Markdown, Syntax Highlighting)
- Rich Text / Composer (Lexical)
- Provider Adapters
- Performance
- CLI / Scaffolding
- DevTools / Observability
- Documentation
- Unsure
- type: textarea
attributes:
label: Alternatives considered
description: Other approaches or workarounds you've tried.
- type: textarea
attributes:
label: Additional context
description: Any extra details, links, mockups, or examples.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" fill="none" viewBox="0 0 224 39"><path fill="#666" d="M35.512 14.185c-5.541-2.124-.97-10.255-8.59-10.049-9.313.252-23.906 14.92-22.415 24.45.74 4.718 4.42 2.737 6.149 3.425 1.147.454 2.505 2.504 3.972 2.83-6.491 3.738-14.83.179-14.624-7.833C.3 15.208 17.32-1.91 29.454.173c6.775 1.165 7.617 8.38 6.057 14.012"/><path fill="#666" d="m9 20.981 5.368 5.073c3.42 3.077 12.499 8.448 16.613 4.806 4.85-4.293-5.408-19.12-10.13-21.486l4.146-1.867c6.46 4.728 16.28 20.097 8.28 26.788-5.162 4.316-13.483.66-18.374-2.554C11.177 29.29 5.792 25.779 9 20.977zm5.633-19.253c-1.737.046-3.095 2.312-4.37 2.71-1.134.354-2.702-.614-4.408.666-1.847 1.385-1.216 7.998-4.759 9.645C-1.084 8.332.273.683 8.15.027c.608-.05 7.37.399 6.483 1.697z"/><path fill="#666" d="M27.607 20.981c2.908 4.825-2.363 10.338-6.752 5.94zM51.3 29.93H42V8.63h8.28q3.96 0 6.06 1.4 2.1 1.39 2.1 4.39 0 1.32-.54 2.29-.54.98-1.58 1.55-1.03.57-2.5.69v-.06q2.67.18 4.09 1.54 1.43 1.37 1.43 3.62 0 2.97-2.09 4.43-2.08 1.45-5.95 1.45m-4.74-9.18v5.46h4.53q1.65 0 2.66-.68 1-.67 1-2.02t-1-2.06q-1.01-.7-2.66-.7zm0-8.4v5.1h3.6q1.68 0 2.68-.66 1.01-.66 1.01-1.89 0-1.32-.97-1.94-.98-.61-2.72-.61zm20.49 17.58h-4.5V13.85h4.26l.15 4.68-.33-.09q.39-2.43 1.38-3.51t2.73-1.08h1.47v3.72h-1.47q-1.26 0-2.07.33t-1.22 1.05q-.4.72-.4 1.95zm14.34.36q-2.46 0-4.29-1.04-1.83-1.03-2.85-2.92t-1.02-4.44 1.02-4.43q1.02-1.87 2.85-2.92t4.29-1.05 4.29 1.05 2.85 2.92q1.02 1.88 1.02 4.43t-1.02 4.44-2.85 2.92q-1.83 1.04-4.29 1.04m0-3.45q1.68 0 2.61-1.31.93-1.3.93-3.64T84 18.24q-.93-1.3-2.61-1.3t-2.61 1.3q-.93 1.31-.93 3.65t.93 3.64q.93 1.31 2.61 1.31m18.15 3.09h-4.35l-4.77-16.08h4.53l2.76 10.89 2.97-10.89h3.66l2.97 10.89 2.79-10.89h4.53l-4.77 16.08h-4.38l-2.97-10.05zm23.73.36q-2.49 0-4.17-.71-1.68-.7-2.57-1.92-.88-1.21-1-2.8l4.56-.15q.21 1.14.96 1.77t2.22.63q1.2 0 1.87-.39.68-.39.68-1.2 0-.51-.24-.86-.24-.34-.95-.6-.7-.25-2.08-.46-2.55-.45-3.99-1.05t-2.04-1.56-.6-2.34q0-2.31 1.78-3.74 1.79-1.42 5.36-1.42 2.28 0 3.82.72 1.55.72 2.4 1.96.86 1.25 1.07 2.84l-4.53.18q-.09-.78-.47-1.34-.37-.55-.94-.87-.57-.31-1.38-.31-1.2 0-1.85.49-.64.5-.64 1.31 0 .6.28.97.29.38.95.62t1.77.39q2.58.36 4.09 1 1.52.65 2.18 1.61t.66 2.37q0 1.56-.9 2.64t-2.51 1.65q-1.6.57-3.79.57m17.19 0q-2.46 0-4.28-1.04-1.81-1.03-2.82-2.92-1-1.89-1-4.44t1-4.43q1.01-1.87 2.81-2.92t4.26-1.05q2.4 0 4.17 1.05t2.74 3q.98 1.95.98 4.65v.9h-11.31q.09 1.92 1.03 2.85.95.93 2.51.93 1.14 0 1.9-.48.77-.48 1.1-1.47l4.53.27q-.63 2.4-2.67 3.75t-4.95 1.35m-3.45-9.96h6.69q-.09-1.77-.98-2.64-.88-.87-2.29-.87t-2.33.91q-.91.92-1.09 2.6m18.57 9.6h-4.5V13.85h4.26l.15 4.68-.33-.09q.39-2.43 1.38-3.51t2.73-1.08h1.47v3.72h-1.47q-1.26 0-2.07.33t-1.22 1.05q-.4.72-.4 1.95zm23.67.48q-2.7 0-4.67-.99-1.96-.99-3.04-2.84-1.08-1.84-1.08-4.33V8.6h4.56v13.65q0 2.07 1.09 3.19 1.1 1.13 3.14 1.13t3.12-1.13q1.08-1.12 1.08-3.19V8.6h4.56v13.65q0 2.49-1.07 4.33-1.06 1.85-3.04 2.84t-4.65.99m19.38-.12q-2.49 0-4.17-.71-1.68-.7-2.57-1.92-.88-1.21-1-2.8l4.56-.15q.21 1.14.96 1.77t2.22.63q1.2 0 1.87-.39.68-.39.68-1.2 0-.51-.24-.86-.24-.34-.95-.6-.7-.25-2.08-.46-2.55-.45-3.99-1.05t-2.04-1.56-.6-2.34q0-2.31 1.78-3.74 1.79-1.42 5.36-1.42 2.28 0 3.82.72 1.55.72 2.4 1.96.86 1.25 1.07 2.84l-4.53.18q-.09-.78-.47-1.34-.37-.55-.94-.87-.57-.31-1.38-.31-1.2 0-1.85.49-.64.5-.64 1.31 0 .6.28.97.29.38.95.62t1.77.39q2.58.36 4.09 1 1.52.65 2.18 1.61t.66 2.37q0 1.56-.9 2.64t-2.51 1.65q-1.6.57-3.79.57m17.19 0q-2.46 0-4.28-1.04-1.81-1.03-2.82-2.92-1-1.89-1-4.44t1-4.43q1.01-1.87 2.81-2.92t4.26-1.05q2.4 0 4.17 1.05t2.74 3q.98 1.95.98 4.65v.9h-11.31q.09 1.92 1.03 2.85.95.93 2.51.93 1.14 0 1.9-.48.77-.48 1.1-1.47l4.53.27q-.63 2.4-2.67 3.75t-4.95 1.35m-3.45-9.96h6.69q-.09-1.77-.98-2.64-.88-.87-2.29-.87t-2.33.91q-.91.92-1.09 2.6"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 309 93"><g fill="#666"><path fill-rule="evenodd" d="M31.527 4.343 12.677 15.12A22.55 22.55 0 0 0 1.343 34.693v23.164a22.55 22.55 0 0 0 10.733 19.21l19.256 11.83a22.55 22.55 0 0 0 23.374.151l20.083-12.01a22.55 22.55 0 0 0 10.988-19.362v-21.51a22.55 22.55 0 0 0-10.973-19.362L54.3 4.554a22.55 22.55 0 0 0-22.773-.211m12.687 17.151-7.651 4.42a12.03 12.03 0 0 0-5.998 10.417v9.861a12.03 12.03 0 0 0 5.682 10.207l7.862 4.885a12.03 12.03 0 0 0 12.567.075l8.237-4.975a12.03 12.03 0 0 0 5.817-10.297v-9.065a12.03 12.03 0 0 0-5.817-10.296l-8.463-5.111a12.03 12.03 0 0 0-12.236-.12" clip-rule="evenodd"/><path d="M120.18 70.71h-7.63q-5.53 0-8.54-2.94T101 59.3V37.11q0-5.53 3.01-8.47t8.54-2.94h7.63q5.46 0 8.51 2.98 3.04 2.97 3.04 8.43v3.29q0 1.54-1.54 1.54h-5.88q-1.47 0-1.47-1.54v-2.87q0-2.24-.95-3.18-.94-.95-3.18-.95h-4.76q-2.17 0-3.11.95-.95.94-.95 3.18v21.35q0 2.24.95 3.19.94.94 3.11.94h4.76q2.24 0 3.18-.94.95-.95.95-3.19v-2.87q0-1.54 1.47-1.54h5.88q1.54 0 1.54 1.54v3.29q0 5.46-3.04 8.44-3.05 2.97-8.51 2.97m35.56 0h-6.23q-5.53 0-8.54-2.94t-3.01-8.47V46.42q0-5.53 3.01-8.47t8.54-2.94h6.23q5.46 0 8.51 2.98 3.04 2.97 3.04 8.43V59.3q0 5.46-3.04 8.44-3.05 2.97-8.51 2.97m-4.9-7.63h3.57q2.24 0 3.19-.91.94-.91.94-3.15V46.7q0-2.24-.94-3.15-.95-.91-3.19-.91h-3.57q-2.24 0-3.15.91t-.91 3.15v12.32q0 2.24.91 3.15t3.15.91m30.87 7.63h-5.95q-1.47 0-1.47-1.47V36.48q0-1.47 1.47-1.47h5.95q1.47 0 1.47 1.47v2.38h.28q.77-1.89 2.73-2.87t4.13-.98h2.52q1.4 0 1.4 1.47v4.83q0 .84-.32 1.16-.31.31-1.08.24h-4.83q-4.83 0-4.83 5.46v21.07q0 1.47-1.47 1.47m43.33 0h-15.47q-5.53 0-8.54-2.94t-3.01-8.47V46.42q0-5.53 3.01-8.47t8.54-2.94h5.53q5.53 0 8.54 2.94t3.01 8.47v8.89q0 1.47-1.47 1.47h-17.57q-.84 0-.84.77v1.54q0 2.17.95 3.12.94.94 3.18.94h14.14q1.47 0 1.47 1.54v4.62q0 .77-.31 1.09-.32.31-1.16.31m-17.43-19.88h9.52q.77 0 .77-.77V46.7q0-2.24-.91-3.15t-3.15-.91h-2.94q-2.24 0-3.15.91t-.91 3.15v3.36q0 .77.77.77m24.15-15.82h6.16q1.4 0 1.68 1.47l6.02 26.32h.49l5.88-26.32q.28-1.47 1.75-1.47h6.16q1.47 0 1.05 1.47l-8.82 32.69q-.28 1.54-1.89 1.54h-8.89q-1.47 0-1.89-1.54l-8.75-32.69q-.42-1.47 1.05-1.47m34.65-6.02V22.9q0-1.4 1.4-1.4h6.09q1.4 0 1.4 1.33v6.16q0 1.4-1.4 1.4h-6.09q-1.4 0-1.4-1.4m0 40.25V36.48q0-1.47 1.4-1.47h6.02q1.47 0 1.47 1.47v32.76q0 .91-.31 1.19-.32.28-1.09.28h-6.02q-1.47 0-1.47-1.47m15.26 0v-2.31q0-2.38 1.33-4.06l14.84-19.95v-.28h-14.07q-1.47 0-1.47-1.54v-4.62q0-1.47 1.47-1.47h22.82q1.47 0 1.47 1.47v4.2q0 2.24-1.12 3.64L293.22 62.8v.28h14.14q1.47 0 1.47 1.54v4.62q0 1.47-1.47 1.47h-24.22q-1.47 0-1.47-1.47"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 3179 713"><path fill="#666" d="M874.8 143.68h102.18v425.23H874.8zm482.12 220.26V568.9h-102.18V384.13c0-34.88-23.86-55.06-54.45-55.06s-55.68 22.03-55.68 51.39V568.9h-102.17V246.47h96.05v25.7c22.03-20.19 53.84-33.65 93-33.65 75.87 0 125.42 51.4 125.42 125.43Zm41.62 44.66c0-94.83 72.81-169.48 171.32-169.48 91.16 0 145 59.96 157.85 107.07l-90.55 36.1c-7.96-30.59-30.6-52.62-64.85-52.62-39.77 0-70.98 29.98-70.98 78.32s31.21 78.32 70.98 78.32c34.87 0 58.74-22.03 66.69-51.39l90.55 36.1c-12.85 45.28-67.3 105.85-159.08 105.85-97.89 0-171.93-72.81-171.93-168.26Zm352.41-.61c0-96.67 73.42-168.87 176.21-168.87s176.82 72.2 176.82 168.87-73.42 168.87-176.82 168.87-176.21-72.81-176.21-168.87m250.24-.61c0-45.89-28.76-77.71-73.42-77.71s-74.04 31.82-74.04 78.32 29.37 78.32 74.04 78.32 73.42-32.43 73.42-78.92Zm461.95-43.44V568.9h-102.18V384.13c0-34.88-23.86-55.06-54.45-55.06s-55.68 22.03-55.68 51.39V568.9h-102.17V246.47h96.05v25.7c22.03-20.19 53.84-33.65 93-33.65 75.87 0 125.42 51.4 125.42 125.43Zm362.23-117.47-114.41 322.44h-119.31l-114.41-322.44h105.24l68.52 222.1 69.14-222.1h105.24Zm0 161.52c0-96.67 73.42-168.87 176.21-168.87s176.82 72.2 176.82 168.87-73.42 168.87-176.82 168.87-176.21-72.81-176.21-168.87m250.24-.61c0-45.89-28.76-77.71-73.42-77.71s-74.04 31.82-74.04 78.32 29.37 78.32 74.04 78.32 73.42-32.43 73.42-78.92ZM356.04 0C159.41 0 0 159.41 0 356.04s159.41 356.04 356.04 356.04 356.04-159.41 356.04-356.04S552.68 0 356.04 0m-165 424.56h-32.16V287.52h32.16zm140.27 68.76h-62.76V218.77h62.76zm175.19 73.52h-98.1V145.25h98.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" fill="none" viewBox="0 0 219 48"><path fill="#666" fill-rule="evenodd" d="m39.873 24.108-4.215 4.261-4.087-4.132-8.504 8.598L27.1 36.91l-4.016 4.06 4.397 4.445 16.734-16.918z" clip-rule="evenodd"/><path fill="#666" fill-rule="evenodd" d="m6.624 23.774-4.687 4.739L18.67 45.43l4.412-4.461-4.031-4.076 4.016-4.06-8.556-8.649-3.74 3.782zM27.353 2.93l-3.939 3.982 4.092 4.137-4.49 4.538 8.556 8.65 4.214-4.26 4.087 4.131 4.214-4.26z" clip-rule="evenodd"/><path fill="#666" fill-rule="evenodd" d="M19.077 2.528 2.343 19.446l4.28 4.328 3.741-3.782 4.147 4.193 8.505-8.598-4.092-4.136 4.49-4.539z" clip-rule="evenodd"/><path fill="#999" fill-rule="evenodd" d="m23.068 32.834-4.016 4.06 4.03 4.075 4.017-4.06zm12.718-12.857-4.215 4.26 4.087 4.132 4.215-4.26zm-25.421.015-3.74 3.782 4.147 4.193 3.74-3.782zM23.414 6.913l-4.49 4.538 4.092 4.137 4.49-4.538z" clip-rule="evenodd"/><path fill="#aaa" fill-rule="evenodd" d="M18.8 11.35c-2.341-2.368-2.341-6.207 0-8.574a5.95 5.95 0 0 1 8.48 0c2.343 2.367 2.343 6.206 0 8.573a5.95 5.95 0 0 1-8.48 0m25.436 17.186a5.95 5.95 0 0 1-8.48 0c-2.342-2.368-2.342-6.206 0-8.574a5.95 5.95 0 0 1 8.48 0c2.342 2.368 2.342 6.206 0 8.574M18.802 45.678c-2.341-2.367-2.341-6.206 0-8.573a5.95 5.95 0 0 1 8.48 0c2.343 2.367 2.343 6.206 0 8.573a5.95 5.95 0 0 1-8.48 0M1.85 28.48c-2.343-2.367-2.343-6.206 0-8.573a5.95 5.95 0 0 1 8.48 0c2.341 2.367 2.341 6.206 0 8.573a5.95 5.95 0 0 1-8.48 0" clip-rule="evenodd"/><path fill="#666" fill-rule="evenodd" d="M76.282 29.994h-3.187a.705.705 0 0 1-.698-.705h-.001v-11.16c0-.388.312-.705.697-.705h3.21c.2 0 .363-.165.363-.367v-3.11a.366.366 0 0 0-.364-.367H64.366a.366.366 0 0 0-.364.367v3.11c0 .202.163.367.364.367h3.383c.383 0 .697.317.697.705V29.31a.7.7 0 0 1-.696.684h-3.375a.37.37 0 0 0-.363.368v3.109c0 .203.165.368.365.368h4.335c.032.009.064.02.098.02h3.221c.035 0 .066-.011.098-.02h4.153a.365.365 0 0 0 .363-.368V30.36a.366.366 0 0 0-.364-.367M96.769 13.58h-12.63a.366.366 0 0 0-.364.367v3.11c0 .202.161.367.362.367h3.718c.383 0 .697.317.697.705v15.362c0 .202.164.368.365.368h3.22a.367.367 0 0 0 .365-.369V18.13c0-.39.313-.706.697-.706h3.57a.367.367 0 0 0 .365-.367v-3.11a.37.37 0 0 0-.365-.367m18.676 0h-10.987a.375.375 0 0 0-.369.373v19.532c0 .207.165.375.369.375h10.987a.375.375 0 0 0 .371-.375V30.39a.37.37 0 0 0-.37-.375h-6.706a.705.705 0 0 1-.703-.711V26.09c0-.392.315-.711.705-.711h6.144a.375.375 0 0 0 .37-.375V22.2a.373.373 0 0 0-.37-.375h-6.145a.705.705 0 0 1-.704-.71v-2.98c0-.392.315-.711.705-.711h6.705a.375.375 0 0 0 .369-.375v-3.096a.373.373 0 0 0-.37-.375m16.794 8.706c-.563.516-1.301.78-2.195.78h-1.594a.705.705 0 0 1-.7-.708v-4.225a.705.705 0 0 1 .7-.705h1.594c.893 0 1.632.267 2.196.795.567.532.855 1.213.855 2.023 0 .831-.288 1.518-.857 2.04m1.637 4.559a.705.705 0 0 1 .24-.959 6 6 0 0 0 2.107-2.146 6.9 6.9 0 0 0 .91-3.495 6.75 6.75 0 0 0-.839-3.261 6.45 6.45 0 0 0-2.4-2.468c-1.038-.621-2.255-.934-3.617-.934h-6.109a.37.37 0 0 0-.366.369v19.54c0 .204.165.37.366.37h3.217a.367.367 0 0 0 .366-.37v-5.902a.705.705 0 0 1 .7-.707h.95c.261 0 .5.144.62.378l3.315 6.404a.36.36 0 0 0 .324.198h3.643a.37.37 0 0 0 .315-.555zm20.523-.179a.7.7 0 0 1-.574.297H150.8a.705.705 0 0 1-.665-.939l1.512-4.269a.7.7 0 0 1 .665-.468c.301 0 .562.183.663.468l1.512 4.269a.7.7 0 0 1-.089.642m6.621 7.195c.125 0 .236-.06.306-.163a.36.36 0 0 0 .038-.345l-7.731-19.539a.38.38 0 0 0-.345-.234h-1.98a.36.36 0 0 0-.344.234l-7.731 19.54a.36.36 0 0 0 .038.345.36.36 0 0 0 .306.162h3.522a.375.375 0 0 0 .348-.246l.969-2.7a.705.705 0 0 1 .663-.465h6.495a.705.705 0 0 1 .664.471l.942 2.691a.375.375 0 0 0 .35.25zm10.681-8.083c0-.333.269-.606.599-.606h2.656c.797 0 1.452.234 1.947.693a2.25 2.25 0 0 1 .753 1.715 2.36 2.36 0 0 1-.735 1.756c-.488.471-1.147.71-1.965.71h-2.62a.64.64 0 0 1-.635-.642zm0-7.74c0-.337.27-.612.603-.612h1.749c.774 0 1.395.203 1.844.603q.57.51.664 1.292a2.2 2.2 0 0 1-.661 1.875c-.47.439-1.101.661-1.877.661h-1.723a.6.6 0 0 1-.599-.606zm-3.558 15.824h7.57c1.304 0 2.438-.277 3.37-.818a5.55 5.55 0 0 0 2.12-2.13 5.76 5.76 0 0 0 .724-2.805c0-1.139-.276-2.154-.82-3.015a5.55 5.55 0 0 0-2.216-1.994.21.21 0 0 1-.11-.159l-.01-.09.079-.088c1.152-.998 1.737-2.289 1.737-3.843 0-.966-.223-1.865-.664-2.672-.44-.802-1.118-1.452-2.016-1.935-.903-.486-2.024-.732-3.33-.732h-6.423a.405.405 0 0 0-.402.405v19.481c0 .217.175.396.391.396m31.154-.001a.375.375 0 0 0 .37-.376V30.39a.375.375 0 0 0-.37-.375h-5.714a.705.705 0 0 1-.703-.711V13.953a.375.375 0 0 0-.371-.375H189.3a.375.375 0 0 0-.371.375v19.532c0 .206.167.374.37.374zm18.696-3.846h-6.707a.705.705 0 0 1-.702-.711v-3.218a.705.705 0 0 1 .702-.71h6.147a.37.37 0 0 0 .369-.373V22.2a.37.37 0 0 0-.369-.372h-6.147a.705.705 0 0 1-.702-.711v-2.982a.705.705 0 0 1 .702-.71h6.707a.37.37 0 0 0 .369-.372V13.95a.37.37 0 0 0-.37-.372h-10.989a.37.37 0 0 0-.369.372v19.535a.37.37 0 0 0 .37.372h10.989a.37.37 0 0 0 .369-.372v-3.1a.37.37 0 0 0-.37-.373" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" fill="none" viewBox="0 0 130 22"><path fill="#666" d="M5 12.3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9M10.9.7a4.5 4.5 0 0 1 4.4 5.4c-.3 1.4-.6 3 .2 4.2l1.3 1.9.3.2.3-.2 1.3-1.9c.8-1.2.5-2.7.2-4.1a4.5 4.5 0 1 1 8.8.1c-.3 1.3-.6 2.7 0 3.9l1.3 2v.1a4.5 4.5 0 1 1-4.3 3.4c.3-1.3.6-2.7 0-3.9l-1.2-2h-.2L22 11.5c-.8 1.2-.5 2.8-.2 4.2a4.5 4.5 0 1 1-8.8.3q.5-2-.4-3.8l-.9-1.3q-.9-1.2-2.4-1.6A4.5 4.5 0 0 1 10.9.6M56.6 17v-6.9q0-1-.7-1.7t-1.7-.7q-1.3 0-2.1 1T51 11v6h-2.8V5.6h2.9v2.2q.6-1 1.6-1.8 1-.6 2.5-.7 1.2 0 2.3.7T59 7.8q.6-1.2 1.7-1.8a5 5 0 0 1 2.5-.7q2 0 3.1 1.2 1.3 1.2 1.3 3.2V17h-3v-6.6q0-1.4-.6-2t-1.7-.7q-1.2 0-2.1.9t-.9 2.3V17zm18.6.3q-1.5 0-3-.7a6 6 0 0 1-2-2.2q-.7-1.5-.7-3 0-1.8.7-3.2a6 6 0 0 1 5-3q1.4 0 2.6.7t1.8 1.6V5.6h3V17h-3v-2q-.6 1.2-1.8 1.8-1.2.5-2.6.5M76 15q1.6 0 2.6-1t1-2.7-1-2.7-2.6-1-2.6 1-1 2.7 1 2.7 2.6 1m14 2.3a7 7 0 0 1-4.1-1q-1.5-1.2-1.6-3L87 13q0 1 .8 1.6t2.2.7a3 3 0 0 0 1.7-.5q.7-.4.7-1a1 1 0 0 0-.6-1l-1.4-.6-3.8-.9q-.8-.3-1.4-.9t-.6-1.7q0-1.6 1.4-2.6t3.8-1 3.6 1A3 3 0 0 1 95 8.6l-2.8.1q0-.6-.6-1.2T89.8 7q-1 0-1.7.4t-.6 1 .6 1l1.5.4 3.7.9q.8.3 1.5 1 .5.6.5 1.7 0 1.8-1.4 2.8-1.5 1-4 1m12.6 0q-1.9 0-3-1a4 4 0 0 1-1.2-2.8V7.7h-2.5V5.5h2.5V2.2h2.9v3.4h3.7v2.2h-3.7V13q0 1 .4 1.4.5.5 1.3.5l1-.2.9-.6.4 2.4q-.3.3-1.1.5-.7.2-1.6.2m4.3-.3V5.6h2.9V8q.5-1.2 1.5-2a4 4 0 0 1 4.2-.4l-.3 2.8-.9-.4-1-.2-1 .2q-.7.2-1.2.6t-1 1.2q-.3.7-.3 2V17zm14.7.3q-1.6 0-3-.7a6 6 0 0 1-2-2.2q-.8-1.5-.8-3 0-1.8.7-3.2a6 6 0 0 1 5-3q1.5 0 2.6.7t1.9 1.6V5.5h2.9V17h-3v-2q-.6 1.2-1.8 1.8-1.1.5-2.5.5m.7-2.3q1.6 0 2.6-1t1-2.7-1-2.7-2.6-1-2.6 1-1 2.7 1 2.7 2.6 1"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 164 37"><path fill="#666" d="M14.42 2.582c-2.267 1.207-4.118 2.22-4.107 2.242.01.036 8.141 4.306 8.259 4.337.047.016 8.21-4.938 8.21-4.98 0-.047-8.093-3.815-8.178-3.81-.032.005-1.921.998-4.183 2.21m59.629-.543-2.907 1.61-.016 1.311-.01 1.312h-4.482v5.017h4.476l.021 5.733c.016 6.114.016 6.015.283 7.175.688 2.99 2.673 4.97 5.623 5.613 1.665.36 3.996.277 5.874-.204.395-.104 1.67-.523 1.745-.575.021-.02-1.595-4.86-1.643-4.907-.027-.026-.235.031-.491.136-1.84.768-3.66.648-4.615-.308-.363-.366-.688-1.004-.79-1.558-.064-.345-.074-1.374-.08-5.759v-5.346l3.324-.01 3.319-.016.016-2.498.01-2.493h-6.615V3.345c0-2.31-.016-2.927-.064-2.922-.038.005-1.377.732-2.977 1.615m70.439.005-2.951 1.626v26.067h5.976l.01-5.3.016-5.304 4.674 5.289 4.668 5.289 3.558.015 3.559.01v-.5l-5.522-5.959c-3.036-3.271-5.516-5.978-5.506-6.015.006-.031 2.204-2.466 4.882-5.403 4.071-4.468 4.866-5.362 4.866-5.488v-.15h-7.144l-4.017 4.693-4.018 4.698-.016-7.599c-.005-4.175-.026-7.593-.053-7.593-.021 0-1.366.732-2.982 1.625M23.048 7.52l-4.349 2.707-.032 2.948c-.016 1.62-.037 6.02-.042 9.772l-.006 6.826 4.375 2.466a736 736 0 0 0 4.402 2.462c.016 0 .026-2.232.026-4.955v-4.954l-4.375-2.32c-2.406-1.28-4.374-2.336-4.374-2.352s1.974-1.113 4.39-2.44l4.386-2.41.016-5.23c.005-2.88-.005-5.232-.027-5.227-.026 0-2 1.218-4.39 2.707m30.169-1.95c-.987.085-2.491.372-3.387.654-.46.146-1.356.528-1.804.763-2.118 1.129-3.393 2.827-3.697 4.918-.09.632-.037 1.902.107 2.508.256 1.082.715 1.934 1.467 2.713a7.2 7.2 0 0 0 1.873 1.41c1.995 1.025 3.547 1.401 7.122 1.715 2.95.261 4.06.92 4.06 2.404 0 .261-.032.527-.09.684-.465 1.349-2.412 2.268-4.53 2.138-2.465-.152-5.522-1.25-7.219-2.592a7 7 0 0 1-.56-.502c-.133-.146-.17-.167-.23-.11-.037.037-.703.972-1.488 2.08l-1.419 2.012.427.397c2.177 2.038 4.743 3.235 7.773 3.627.934.12 3.153.194 4.023.13 3.243-.24 5.602-1.16 7.234-2.832q2.312-2.361 1.942-5.874c-.117-1.15-.565-2.357-1.179-3.161-.341-.455-1.051-1.119-1.547-1.443-1.66-1.097-3.708-1.636-7.33-1.923-1.66-.136-2.535-.313-3.335-.69-1.771-.83-1.755-2.937.032-3.726 1.232-.543 3.33-.674 5.196-.313 1.286.245 2.364.747 3.41 1.583.389.313.394.313.48.204.048-.058.779-.904 1.627-1.882.843-.977 1.542-1.797 1.542-1.818.005-.068-.843-.727-1.409-1.092a11.5 11.5 0 0 0-3.617-1.563c-1.2-.298-2.15-.402-3.793-.423a49 49 0 0 0-1.68.005m75.09.021c-4.47.413-8.024 2.587-9.811 6.004-.805 1.537-1.227 3.058-1.398 5.017-.08.915-.037 2.755.086 3.611.768 5.467 4.444 9.14 10.051 10.03 2.086.334 4.444.234 6.263-.257 1.062-.287 2.246-.8 3.132-1.348.736-.46 2.401-1.777 2.401-1.902 0-.027-.56-.586-1.238-1.234-.683-.648-1.574-1.5-1.974-1.886l-.731-.7-.192.15c-.667.534-.976.738-1.547 1.015-.742.36-1.11.48-1.921.627-1.798.33-3.788.063-5.148-.695-1.937-1.072-3.062-2.906-3.254-5.315-.272-3.36 1.392-6.161 4.294-7.238 1.163-.428 2.7-.554 3.9-.314 1.158.236 2.364.79 3.228 1.5.133.11.256.199.272.199.064 0 3.697-3.773 3.697-3.836 0-.037-.165-.22-.373-.402-1.862-1.673-3.799-2.603-6.162-2.948-.715-.104-2.785-.151-3.575-.078m-30.601.11c-5.832.548-9.828 4.19-10.687 9.725a16.6 16.6 0 0 0 .075 5.608c.88 4.672 3.815 7.849 8.301 8.983 1.852.465 4.018.56 5.57.24 1.601-.329 3.004-.935 4.194-1.813.672-.491 1.387-1.29 1.707-1.897.096-.178.128-.209.139-.13.016.125.282 3.203.282 3.27 0 .027 1.174.048 2.801.048h2.801V6.219h-5.708v.115c0 .125-.155 2.744-.176 2.968-.011.11-.048.073-.273-.261-.352-.523-1.221-1.359-1.835-1.766-1.264-.847-2.838-1.411-4.396-1.573-.587-.063-2.14-.063-2.795 0m3.531 5.252c1.483.22 2.945 1.013 3.868 2.095.406.47.72.92.918 1.307 1.456 2.817 1.04 6.402-.992 8.623-1.276 1.395-2.983 2.106-5.09 2.111-2.091.005-3.804-.638-5.116-1.928-1.355-1.328-2.05-3.1-2.05-5.242 0-3.533 2.055-6.177 5.357-6.898.934-.204 2.044-.23 3.105-.068M5.72 17.93a2785 2785 0 0 0-4.312 2.619c-.016.015 1.409.81 3.164 1.766 1.755.961 3.756 2.054 4.45 2.43l1.259.69L14.41 22.8c2.273-1.448 4.135-2.65 4.145-2.676.01-.036-2.32-1.343-8.045-4.51l-.507-.282zm27.053 4.296-4.39 2.67v4.934c0 2.712.02 4.923.042 4.912.027-.005 2.006-1.165 4.402-2.57l4.358-2.556v-5.033a628 628 0 0 0-.01-5.033c-.01 0-1.99 1.202-4.402 2.676M1.227 26.694v4.954l4.353 2.347a567 567 0 0 0 4.402 2.367c.027.01.043-2.137.037-4.954l-.016-4.975-4.348-2.346c-2.39-1.291-4.364-2.347-4.385-2.347-.027 0-.043 2.226-.043 4.954"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" fill="none" viewBox="0 0 200 57"><path fill="#666" d="M157.076 36.609V8.354h3.934l.609 3.767c1.33-1.994 3.934-4.1 8.255-4.1 3.934 0 7.313 1.773 8.92 5.375 1.717-3.048 4.764-5.374 9.806-5.374 5.873 0 10.692 3.213 10.692 11.523V36.61h-4.542V19.822c0-4.93-2.438-7.645-6.704-7.645-4.709 0-7.59 3.324-7.59 8.532v15.9h-4.543V19.822c0-4.93-2.493-7.645-6.814-7.645-4.654 0-7.48 3.656-7.48 8.753v15.68z"/><path fill="#666" d="M139.069 36.941c-6.205 0-10.416-3.102-10.416-8.255 0-5.65 3.934-8.808 11.358-8.808h8.31v-1.94c0-3.822-2.604-5.872-7.036-5.872-4.155 0-6.648 1.884-7.202 4.764h-4.543c.665-5.54 5.152-8.808 11.966-8.808 7.203 0 11.358 3.6 11.358 10.193V30.57c0 1.496.554 1.884 1.828 1.884h5.945v4.155h-6.998c-3.601 0-5.097-1.662-5.097-4.266v-.11c-1.662 2.548-4.598 4.708-9.473 4.708m.332-3.878c5.651 0 8.92-3.269 8.92-7.978V23.7h-8.754c-4.044 0-6.315 1.496-6.315 4.71 0 2.991 2.382 4.653 6.149 4.653m-24.233 3.547V8.355h3.934l.609 4.266c1.33-2.161 3.49-4.266 8.532-4.266h1.274v4.432h-2.271c-5.928 0-7.535 4.93-7.535 9.418V36.61zM97.295 48.022c-7.202 0-12.244-3.38-12.964-9.585h4.654c.775 3.546 3.933 5.54 8.642 5.54 5.928 0 9.142-2.77 9.142-8.92v-2.991c-1.884 2.604-4.931 4.875-9.696 4.875-7.811 0-13.573-5.263-13.573-14.46 0-8.642 5.762-14.46 13.573-14.46 4.765 0 7.978 1.94 9.696 4.654l.609-4.321h3.934v26.87c0 8.144-4.322 12.798-14.017 12.798m.222-15.125c5.54 0 9.307-4.266 9.307-10.305 0-6.205-3.767-10.526-9.307-10.526-5.596 0-9.363 4.266-9.363 10.415 0 6.15 3.767 10.416 9.363 10.416"/><path fill="#666" fill-rule="evenodd" d="M26.467.431a3.08 3.08 0 0 1 3.075.005L54.367 13.48l.028.014.01.007.021.013a3.29 3.29 0 0 1 1.572 2.813v2.751c0 1.133-.58 2.218-1.56 2.808l-.03.018-.009.005-.03.016-1.834.935 1.846.996.023.012.01.006.017.012A3.3 3.3 0 0 1 56 26.697v3.437c0 1.132-.578 2.213-1.558 2.804l.002.002-.022.014-.016.01-.025.012-.002-.002-1.737.913 1.732.91.025.013.01.007.024.014A3.28 3.28 0 0 1 56 37.643v2.063c0 1.133-.579 2.218-1.56 2.808l-.023.014-.009.007-.025.01-24.85 13.06-.113.061-.005-.004a3 3 0 0 1-.645.246l-.005.012-.047.102h-.716c-.527 0-1.06-.138-1.542-.42L1.626 42.558l-.025-.014-.01-.004-.022-.014A3.29 3.29 0 0 1 0 39.711V16.328c0-1.133.579-2.216 1.56-2.806l.023-.016.009-.004.025-.014zm1.381 2.229a.5.5 0 0 0-.113.052l-.027.015-.007.003-.025.013-24.802 13.03a.67.67 0 0 0-.283.56v23.382c0 .249.13.46.294.564L27.667 53.3l.002.003.023.011.007.005.025.013a.51.51 0 0 0 .53.007l.022-.016.037-.018L53.11 40.272a.67.67 0 0 0 .287-.557V37.65a.67.67 0 0 0-.285-.56l-3.28-1.723L29.527 46.04l-.002-.002a3.08 3.08 0 0 1-3.062.002L7.384 36.312l-.031-.016-.012-.007-.027-.018a2.3 2.3 0 0 1-1.105-1.966v-.516c0-.792.405-1.543 1.09-1.96l.022-.015.014-.009.027-.013 6.868-3.612-6.857-3.62-.025-.012-.016-.01-.022-.015a2.3 2.3 0 0 1-1.1-1.963v-.517c0-.791.405-1.542 1.09-1.96l.034-.021.013-.01.036-.015 19.094-9.579a3.07 3.07 0 0 1 3.069.014L49.75 21.366l3.379-1.73a.67.67 0 0 0 .276-.55v-2.75a.68.68 0 0 0-.292-.567L28.33 2.75l-.028-.013-.01-.007-.026-.016a.53.53 0 0 0-.27-.075h-.033l-.116.02ZM8.802 33.975v.127l18.852 9.62h.004l.03.015.006.004.03.019c.178.108.379.1.534.006l.025-.013.007-.005.027-.013 18.716-9.837-7.921-4.162-9.597 4.911-.002-.002a3.07 3.07 0 0 1-3.062-.014l-9.425-4.979zm19.472-9.941a.52.52 0 0 0-.528-.01l-.022.017-.016.009-.03.013-7.846 4.122 7.837 4.139.041.02.023.014a.5.5 0 0 0 .527.005l.028-.019.01-.006.033-.014 7.961-4.076-7.961-4.183-.003-.002-.025-.011-.004-.002zm13.68 4.246 7.88 4.141 3.292-1.73a.67.67 0 0 0 .277-.55v-3.437c0-.25-.13-.463-.295-.567l-3.394-1.831zM28.27 12.74a.51.51 0 0 0-.53-.01l.002.003-.032.02-.018.01-.034.013-18.852 9.459v.127l8.227 4.345 9.432-4.957a3.07 3.07 0 0 1 1.537-.41c.54.001 1.068.148 1.531.415l9.597 5.04 7.8-3.99-18.613-10.038-.002-.002-.02-.011-.005-.003z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 2952 592"><g fill="#666"><path d="M291.5 4.5a33.7 33.7 0 0 1 11 1.5c93.33 41 186.67 82 280 123a30 30 0 0 1 2.5 4.5q1.005 161.5 0 323c-2.46 9.131-8.3 14.965-17.5 17.5A34503 34503 0 0 1 295 587.5 24719 24719 0 0 1 12.5 469q-5.822-4.392-7.5-11.5-1-162 0-324l2-4a35144 35144 0 0 1 284.5-125m272 235a5686 5686 0 0 1 0 213 10199 10199 0 0 1-179 74.5c59.8-95.777 119.47-191.611 179-287.5m-330 4a2293 2293 0 0 0 50 22 43.2 43.2 0 0 1 .5 16 51403 51403 0 0 1-246 176 83 83 0 0 1-11.5-5 561 561 0 0 1-.5-58 85298 85298 0 0 1 207.5-151m50 64a8010 8010 0 0 1 0 253 32889 32889 0 0 1-222-93.5 38522 38522 0 0 0 222-159.5"/><path d="M292.5 25.5a39.9 39.9 0 0 1 14 4.5 39680 39680 0 0 1 243 107 42686 42686 0 0 0-253 111 21215 21215 0 0 1-256-111 106681 106681 0 0 0 252-111.5m596 50h75c.17 36.002 0 72.002-.5 108a838 838 0 0 1-2.5 34q2.835.305 4.5-2c15.06-21.794 35.9-34.294 62.5-37.5 28.82-3.904 55.49 1.763 80 17 21.17 18.17 32.33 41.337 33.5 69.5.5 63.666.67 127.332.5 191h-75c.17-54.668 0-109.334-.5-164-.41-43.09-21.57-60.257-63.5-51.5-18.34 7.01-29.5 20.177-33.5 39.5a201.2 201.2 0 0 0-5 42c-.5 44.665-.67 89.332-.5 134h-75zm624 0h75v380h-75zm179 0q22.65-3.284 42 8.5c5.12 4.74 8.29 10.573 9.5 17.5q1.005 10 0 20c-1 8.834-5.16 15.668-12.5 20.5-15.13 7.038-30.8 8.371-47 4-15.6-5.851-22.94-17.185-22-34q.03-30.893 30-36.5m-1666 77q-.499 108.254 1 216-.09.814-1 1a5893 5893 0 0 1 0-217"/><path d="M25.5 152.5a23074 23074 0 0 1 186 81.5 35133 35133 0 0 1-185 134.5 11663 11663 0 0 1-1-216m537 0h3c.17 15.67 0 31.337-.5 47l-214.5 343a1880 1880 0 0 0-45 19 938 938 0 0 1-.5-75 47420 47420 0 0 0 201-308 6746 6746 0 0 1 56.5-26m749 25c26.53-2.476 52.19 1.024 77 10.5 31.88 15.226 52.05 40.06 60.5 74.5a173 173 0 0 1 4 34c.5 13.663.67 27.329.5 41h-176q2.85 67.555 70.5 67a200 200 0 0 0 50.5-6.5 490 490 0 0 0 39-14.5c.17 19.336 0 38.67-.5 58q-33.375 16.158-70.5 18.5c-36.4 4.441-71.07-1.226-104-17-33.65-20.425-53.49-50.258-59.5-89.5-5.49-36.652-1.82-72.319 11-107q29.715-61.246 97.5-69m17 53c27.12-.04 44.28 12.96 51.5 39a166 166 0 0 1 2.5 18h-104c.98-15.107 5.81-28.774 14.5-41 9.88-9.86 21.71-15.193 35.5-16m587-53c31.63-2.713 62.3 1.454 92 12.5 4.68 1.672 9.01 3.839 13 6.5a1382 1382 0 0 1-22 56.5 810 810 0 0 0-43-13c-14.54-3.633-28.87-2.966-43 2-15.21 6.88-25.04 18.38-29.5 34.5-7.61 29.436-7.27 58.769 1 88 7.52 19.679 21.69 31.179 42.5 34.5 16.63 1.263 32.97-.404 49-5 12.93-4.821 25.27-10.654 37-17.5.17 21.336 0 42.669-.5 64-17.57 11.078-36.73 17.578-57.5 19.5-102.05 9.666-153.38-36.667-154-139-.31-24.967 3.86-49.133 12.5-72.5 16.34-36.601 44.17-59.101 83.5-67.5 6.47-1.187 12.81-2.354 19-3.5m256 0c34.93-3.963 67.26 3.204 97 21.5 28.21 20.889 45.37 48.723 51.5 83.5 7.09 39.841 2.43 78.174-14 115q-26.76 49.722-82.5 60.5a235.4 235.4 0 0 1-60 1c-47.05-8.441-79.22-34.941-96.5-79.5-13.18-42.891-12.52-85.558 2-128 20.04-44.203 54.21-68.869 102.5-74m16 60q47.55.145 55.5 47 8.505 44.84-6 88-14.4 28.336-46.5 28c-27.35-.255-44.52-13.588-51.5-40q-10.215-46.428 3-92c8.71-19.93 23.88-30.264 45.5-31m336-60c25.75-2.821 50.08 1.679 73 13.5 25.45 16.718 38.62 40.551 39.5 71.5.5 64.332.67 128.666.5 193h-75c.17-55.668 0-111.334-.5-167-1.78-41.439-22.95-57.606-63.5-48.5-17.82 5.824-28.65 17.991-32.5 36.5a159 159 0 0 0-5 28c-.5 50.332-.67 100.666-.5 151h-76v-273c19.34-.167 38.67 0 58 .5a786 786 0 0 1 9.5 32.5c.39.928 1.05 1.595 2 2q26.85-34.558 70.5-40m283 0c26.55-2.612 52.22.888 77 10.5 31.89 15.234 52.06 40.067 60.5 74.5 2.27 9.875 3.6 19.875 4 30 .5 14.996.67 29.996.5 45h-176q3.495 66.876 70.5 67a200 200 0 0 0 50.5-6.5 490 490 0 0 0 39-14.5c.17 19.336 0 38.67-.5 58-18.78 9.258-38.61 15.091-59.5 17.5a303.4 303.4 0 0 1-76-2c-51.54-11.208-83.71-42.375-96.5-93.5q-10.755-55.28 6-109 27.6-67.938 100.5-77m17 53c25.46-.537 42.29 11.129 50.5 35a100.6 100.6 0 0 1 3.5 22h-104c.77-19.037 8.11-34.87 22-47.5 8.66-5.498 18-8.665 28-9.5m-1159-48h76v273h-76zm-1358 266a8376 8376 0 0 1-1-183 7723 7723 0 0 1 168-72.5 61878 61878 0 0 0-167 255.5"/><path d="M305.5 265.5a8376 8376 0 0 0 1 183q-.425 1.184-1.5 2a5711 5711 0 0 1 .5-185"/></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

+78
View File
@@ -0,0 +1,78 @@
# Simplify Review
You are a code simplification specialist. Your job is to analyze a PR diff for simplification opportunities, then return a structured summary of findings. You do NOT post any GitHub comments — just return your findings to the caller.
## Phase 1: Get Context
1. Run `gh pr diff <PR_NUMBER>` to get the full diff (replace `<PR_NUMBER>` with the PR number from your prompt)
2. Read `AGENTS.md` and `CLAUDE.md` for project conventions
3. Store the diff text for analysis
## Phase 2: Analyze for Simplification
Review the changed code for three categories. For each, search the codebase for context — read surrounding files, check for existing patterns, and verify your findings against the actual code.
### Code Reuse
For each change:
1. **Search for existing utilities and helpers** that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
2. **Flag any new function that duplicates existing functionality.** Suggest the existing function to use instead.
3. **Flag any inline logic that could use an existing utility** — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
This is a monorepo. Search across `packages/*/src/` for existing utilities.
### Code Quality
Review the same changes for hacky patterns:
1. **Redundant state**: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
2. **Parameter sprawl**: adding new parameters to a function instead of generalizing or restructuring existing ones
3. **Copy-paste with slight variation**: near-duplicate code blocks that should be unified with a shared abstraction
4. **Leaky abstractions**: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
5. **Unclear naming**: vague variable/function names that obscure intent — look for names that don't clearly convey purpose or type
6. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
7. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior
8. **Project convention deviations**: check AGENTS.md and CLAUDE.md for project conventions. Do not flag issues that oxlint/oxfmt already enforce (formatting, Tailwind class order).
### Efficiency
Review the same changes for efficiency:
1. **Unnecessary work**: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
2. **Missed concurrency**: independent operations run sequentially when they could run in parallel
3. **Hot-path bloat**: new blocking work added to startup or per-request/per-render hot paths
4. **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
5. **Memory**: unbounded data structures, missing cleanup, event listener leaks
6. **Overly broad operations**: reading entire files when only a portion is needed, loading all items when filtering for one
7. **Missing subscription cleanup**: `subscribe()` without corresponding `unsubscribe` in effect or cleanup patterns
## Phase 3: Filter
For each finding:
- **Keep** if it's genuinely worth changing — a real improvement, not nitpicking
- **Filter out** if it's a false positive, too minor, or stylistic preference already handled by the linter
Maintain balance — do not suggest over-simplification that would:
- Create overly clever solutions harder to understand
- Combine too many concerns into single functions
- Remove helpful abstractions that improve organization
- Prioritize "fewer lines" over readability
## Return Format
Return your findings in this format:
```
### Reuse (N issues)
- `file:line` — description of what exists and what it could replace
### Quality (N issues)
- `file:line` — description of the pattern and suggested fix
### Efficiency (N issues)
- `file:line` — description of the inefficiency and improvement
```
If a category has no issues, omit it. If no issues found at all, return: "No simplification opportunities found. Code looks clean."
+39
View File
@@ -0,0 +1,39 @@
name: autofix.ci
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
autofix:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --ignore-scripts
- name: Lint fix
run: pnpm lint:fix
- name: Update API surface snapshots
run: pnpm api-surface
- uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
@@ -0,0 +1,302 @@
name: Changeset Semver Check
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
analyze:
name: Semver Check
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 2
steps:
- name: Checkout PR head
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Check changeset bump types against package versions
id: check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
with:
script: |
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
// --- Helpers ---
function bumpVersion(version, bumpType) {
const [major, minor, patch] = version.split(".").map(Number);
if (bumpType === "major") return `${major + 1}.0.0`;
if (bumpType === "minor") return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
}
function isOutsideCaretRange(rangeVersion, newVersion) {
const [rMajor, rMinor, rPatch] = rangeVersion.split(".").map(Number);
const [nMajor, nMinor, nPatch] = newVersion.split(".").map(Number);
// ^0.0.Z — exact match only
if (rMajor === 0 && rMinor === 0) {
return nMajor !== 0 || nMinor !== 0 || nPatch !== rPatch;
}
// ^0.X.Z (X > 0) — same major and minor
if (rMajor === 0) {
return nMajor !== 0 || nMinor !== rMinor;
}
// ^X.Y.Z (X > 0) — same major
return nMajor !== rMajor;
}
function computeCascade(bumps, pkgMap, revDeps) {
const cascade = [];
const visited = new Set(bumps.map((v) => v.pkg));
const queue = bumps.map((v) => ({
pkg: v.pkg,
newVersion: bumpVersion(v.version, v.bumpType),
}));
let qi = 0;
while (qi < queue.length) {
const { pkg, newVersion } = queue[qi++];
const dependents = revDeps.get(pkg) || [];
for (const dep of dependents) {
if (visited.has(dep.name)) continue;
// Extract version from the ^ range (e.g., "^0.12.15" → "0.12.15")
const rangeVersion = dep.range.replace(/^\^/, "");
if (!isOutsideCaretRange(rangeVersion, newVersion)) continue;
visited.add(dep.name);
// This dependent gets a cascade patch
const depInfo = pkgMap.get(dep.name);
const depVersion = depInfo?.version ?? dep.version;
const cascadeNewVersion = bumpVersion(depVersion, "patch");
const isBreaking = isOutsideCaretRange(depVersion, cascadeNewVersion);
cascade.push({
name: dep.name,
version: depVersion,
newVersion: cascadeNewVersion,
isBreaking,
});
// If the cascade patch is itself range-breaking (0.0.x),
// recurse to check further downstream
if (isBreaking) {
queue.push({ pkg: dep.name, newVersion: cascadeNewVersion });
}
}
}
return cascade;
}
// --- Scope to PR-changed changeset files ---
const { BASE_SHA, HEAD_SHA } = process.env;
let prChangedFiles;
try {
const diff = execFileSync(
"git",
["diff", "--name-only", "--diff-filter=ACM", BASE_SHA, HEAD_SHA, "--", ".changeset/*.md"],
{ encoding: "utf8" },
).trim();
prChangedFiles = new Set(
diff ? diff.split("\n").map((f) => path.basename(f)) : [],
);
} catch {
// If git diff fails (e.g., shallow clone), fall back to all files
core.warning("Could not diff against base — checking all changeset files");
prChangedFiles = null;
}
const changesetDir = ".changeset";
const allFiles = fs.readdirSync(changesetDir).filter(
(f) => f.endsWith(".md") && f !== "README.md",
);
const files = prChangedFiles
? allFiles.filter((f) => prChangedFiles.has(f))
: allFiles;
if (files.length === 0) {
core.info("No changeset files changed in this PR.");
return;
}
// --- Build package map and reverse dependency graph ---
const pkgMap = new Map();
const revDeps = new Map();
const packageManifests = [];
const packagesDir = "packages";
for (const entry of fs.readdirSync(packagesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const dir = entry.name;
const pkgJsonPath = path.join(packagesDir, dir, "package.json");
let pkgJson;
try {
pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
} catch {
continue;
}
if (pkgJson.private) continue;
packageManifests.push(pkgJson);
const major = parseInt(pkgJson.version.split(".")[0], 10);
pkgMap.set(pkgJson.name, { major, version: pkgJson.version });
}
for (const pkgJson of packageManifests) {
const allDeps = {
...pkgJson.dependencies,
...pkgJson.peerDependencies,
};
for (const [depName, range] of Object.entries(allDeps)) {
if (typeof range !== "string" || !range.startsWith("^")) continue;
if (!pkgMap.has(depName)) continue;
if (!revDeps.has(depName)) revDeps.set(depName, []);
revDeps.get(depName).push({
name: pkgJson.name,
version: pkgJson.version,
range,
});
}
}
// --- Parse changeset files ---
const allBumps = [];
const violations = [];
for (const file of files) {
const content = fs.readFileSync(
path.join(changesetDir, file),
"utf8",
);
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) continue;
const frontmatter = match[1];
for (const line of frontmatter.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const bumpMatch = trimmed.match(
/^"([^"]+)":\s*(patch|minor|major)$/,
);
if (!bumpMatch) continue;
const [, pkg, bumpType] = bumpMatch;
const pkgInfo = pkgMap.get(pkg);
// Skip unknown or private packages (not in pkgMap)
if (!pkgInfo) continue;
const { major, version } = pkgInfo;
allBumps.push({ file, pkg, bumpType, version });
// Rules:
// 0.0.x → patch technically breaks ^0.0.x (exact match), but not
// flagged because these packages are pre-stable with no
// internal dependents. Only minor/major are flagged.
// 0.x → only patch is safe (minor/major break ^)
// 1.x+ → patch and minor are safe (only major breaks ^)
const needsConfirmation =
(major === 0 && bumpType !== "patch") ||
(major >= 1 && bumpType === "major");
if (needsConfirmation) {
const reason =
major === 0
? `0.x package — ${bumpType} bump breaks \`^\` caret range`
: `major bump breaks \`^\` caret range`;
violations.push({ file, pkg, bumpType, version, reason });
}
}
}
if (allBumps.length === 0) {
core.info("No package bumps found in changesets.");
return;
}
// --- Compute cascade impact for ALL bumps ---
const cascade = computeCascade(allBumps, pkgMap, revDeps);
const breakingCascade = cascade.filter((c) => c.isBreaking);
// --- Build cascade table (shared between both summary paths) ---
function renderCascadeSection() {
if (cascade.length === 0) return "";
let section = `### Cascade Impact (${cascade.length} packages)\n\n`;
section += "| Package | Version | Cascade Bump | Breaking |\n";
section += "| --- | --- | --- | --- |\n";
for (const c of cascade) {
const indicator = c.isBreaking
? `⛔ YES — \`^${c.version}\` breaks`
: "✅ no";
section += `| \`${c.name}\` | ${c.version} → ${c.newVersion} | patch | ${indicator} |\n`;
}
section += "\n";
if (breakingCascade.length > 0) {
section += `> **${breakingCascade.length} downstream package(s)** will also break their consumers' \`^\` ranges.\n\n`;
}
return section;
}
// --- Build summary ---
let summary;
if (violations.length === 0) {
// Informational summary for safe PRs
summary = "## Changeset Impact Summary\n\n";
summary += "| File | Package | Version | Bump |\n";
summary += "| --- | --- | --- | --- |\n";
for (const b of allBumps) {
summary += `| \`${b.file}\` | \`${b.pkg}\` | ${b.version} | ${b.bumpType} |\n`;
}
summary += "\n";
summary += renderCascadeSection();
await core.summary.addRaw(summary).write();
core.info("All changeset bump types are safe for current package versions. ✓");
} else {
// Violation summary
summary = "## ⚠️ Semver-Breaking Changeset Detected\n\n";
summary += "| File | Package | Version | Bump | Why |\n";
summary += "| --- | --- | --- | --- | --- |\n";
for (const v of violations) {
summary += `| \`${v.file}\` | \`${v.pkg}\` | ${v.version} | **${v.bumpType}** | ${v.reason} |\n`;
}
summary += "\n";
summary += renderCascadeSection();
summary += "### What this means\n\n";
summary += "- **0.x packages**: `^0.12.15` only matches `>=0.12.15 <0.13.0` — ";
summary += "a minor bump is effectively a breaking change for all consumers.\n";
summary += "- **1.x+ packages**: `^1.3.12` matches `>=1.3.12 <2.0.0` — ";
summary += "minor/patch are safe, but a major bump breaks all consumers.\n\n";
summary += "This check is informational — the PR can still be merged if this is intentional.\n";
await core.summary.addRaw(summary).write();
// Emit warnings for breaking cascade entries (visible in PR checks tab)
for (const c of breakingCascade) {
core.warning(
`Cascade breaks ${c.name} (${c.version} → ${c.newVersion}, ^${c.version} consumers affected)`,
);
}
core.setFailed(`Semver-breaking bumps detected: ${violations.map((v) => `${v.pkg}@${v.bumpType}`).join(", ")}`);
}
+139
View File
@@ -0,0 +1,139 @@
name: Changesets
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
CI: true
jobs:
version:
name: Create Version PR
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create version PR
id: changesets
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
commit: "chore: update versions"
title: "chore: update versions"
version: pnpm ci:version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Detect release packages
id: release_plan
if: steps.changesets.outputs.hasChangesets == 'false'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { execFileSync } = require("node:child_process");
const { existsSync, readFileSync } = require("node:fs");
const before = context.payload.before;
const after = context.sha;
const isZeroBefore = typeof before === "string" && /^0+$/.test(before);
if (isZeroBefore) {
core.warning(
"Skipping release dispatch on zero before SHA (no baseline to diff)",
);
core.setOutput("hasReleasePackages", "false");
return;
}
const runGit = (args) =>
execFileSync("git", args, { encoding: "utf8" }).trim();
try {
runGit(["cat-file", "-e", `${before}^{commit}`]);
} catch {
core.warning(
`Skipping release dispatch because before SHA is missing: ${before}`,
);
core.setOutput("hasReleasePackages", "false");
return;
}
const changedFilesOutput = runGit([
"diff",
"--name-only",
before,
after,
"--",
"packages/*/package.json",
]);
const changedFiles = changedFilesOutput
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
let hasReleasePackages = false;
for (const filePath of changedFiles) {
if (!existsSync(filePath)) continue;
const current = JSON.parse(readFileSync(filePath, "utf8"));
if (current.private === true) continue;
let previousVersion = null;
try {
const previousRaw = runGit(["show", `${before}:${filePath}`]);
previousVersion = JSON.parse(previousRaw).version ?? null;
} catch {
previousVersion = null;
}
if (previousVersion !== current.version) {
hasReleasePackages = true;
core.info(`Release candidate: ${current.name}@${current.version}`);
}
}
if (!hasReleasePackages) {
core.info("Release candidates: none");
}
core.setOutput("hasReleasePackages", hasReleasePackages ? "true" : "false");
- name: Trigger npm publish
if: steps.changesets.outputs.hasChangesets == 'false' && steps.release_plan.outputs.hasReleasePackages == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
await github.rest.repos.createDispatchEvent({
owner: context.repo.owner,
repo: context.repo.repo,
event_type: 'npm-publish',
client_payload: {
releaseSha: context.sha,
},
});
+159
View File
@@ -0,0 +1,159 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
concurrency:
group: claude-code-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
# Shared action configuration
TRACK_PROGRESS: 'true'
USE_STICKY_COMMENT: 'true'
ADDITIONAL_PERMISSIONS: |
actions: read
REVIEW_PROMPT: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
TITLE: ${{ github.event.pull_request.title }}
BODY: ${{ github.event.pull_request.body }}
AUTHOR: ${{ github.event.pull_request.user.login }}
COMMIT: ${{ github.event.pull_request.head.sha }}
Note: The PR branch is already checked out in the current working directory.
FIRST: Immediately launch a simplification subagent in the background using the Agent tool (with run_in_background: true) with this prompt:
"Read the file .github/prompts/simplify-review.md for guidance on what to look for. The PR number is ${{ github.event.pull_request.number }}. Wherever the file says <PR_NUMBER>, use ${{ github.event.pull_request.number }}. The repository is checked out in the current working directory. Return your findings as a structured summary — do NOT post any GitHub comments yourself."
THEN: While the simplify agent runs in the background, proceed with the review:
Gather Context (complete these steps silently - don't report on this process):
1. Check CI status: Use mcp__github_ci__get_ci_status to see if tests passed/failed
2. Get previous reviews: Run gh pr view ${{ github.event.pull_request.number }} --comments
3. Identify previous bot reviews and track which issues were raised before
4. If CI failed: Use mcp__github_ci__get_workflow_run_details for failure details
Review Approach:
- If first review: Comprehensive analysis of all changes
- If previous reviews exist: Focus on changes since last review, track issue status, understand how changes impact pr
- Incorporate CI/test failure context into your feedback if relevant
- Use inline comments (mcp__github_inline_comment__create_inline_comment) for specific code issues
Do not include in your review:
- Task completion statements ("I completed the review", "Review finished")
- Process descriptions ("First I checked X, then I analyzed Y")
- Generic meta-commentary about reviewing
- Progress tracking
Review this pull request by individually analysing each of these thoroughly:
- Code quality and best practices
- Potential bugs or issues
- Security implications
- Performance considerations
- Test coverage
- assistant-ui specific requirements:
- The PR has a changeset if updating source code of published packages (packages/*)
- The PR includes documentation if API surface changes
Provide a comprehensive review build on previous reviews including:
- Summary of changes since last review
- Critical issues found (be thorough)
- Suggested improvements (be thorough)
- Good practices observed (be concise - list only the most notable items without elaboration)
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability
When reviewing subsequent commits:
- Track status of previously identified issues
- Identify new problems introduced since last review
- Note if fixes introduced new issues
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively. NO meta-commentary about the review process itself.
Use `mcp__github_inline_comment__create_inline_comment` only for critical code issues or relevant suggestions. Check existing inline comments first to avoid duplicates. Do NOT use inline comments for good practices or positive observations. Each inline comment must be well thought out, precise, and actionable.
You have been provided with the mcp__github_comment__update_claude_comment tool to update your comment. This tool automatically handles PR comments. Only the body parameter is required - the tool automatically knows which comment to update.
FINALLY: Once your review is complete, wait for the background simplify agent to finish. Collect its structured findings, then:
1. For each simplify finding that you agree is worth flagging, post an inline comment using mcp__github_inline_comment__create_inline_comment. Skip findings that overlap with issues you already commented on during your review.
2. Submit one final comment using mcp__github_comment__update_claude_comment that includes your full review followed by a `## Simplify` section with the agent's summary. If no simplification issues were found, note that the code looks clean.
# Allowed tools:
# - mcp__github_inline_comment__create_inline_comment: Create inline code comments
# - Bash(gh ...): Read-only GitHub CLI commands for PR/issue info
# Note: gh pr comment is NOT allowed - Claude must use update_claude_comment only
# Temporarily on Opus 4.8 while Fable usage credits are exhausted; swap back to
# `--model fable` when access returns. The Fable-specific machinery below
# (Opus-fallback flagging) stays in place for that switch-back.
CLAUDE_ARGS: '--model claude-opus-4-8 --effort high --allowedTools "Agent,mcp__github_inline_comment__create_inline_comment,Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
jobs:
claude-review:
if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
ref: refs/pull/${{ github.event.pull_request.number }}/head
fetch-depth: 1
- name: Review
id: review
uses: anthropics/claude-code-action@536f2c32a39763739000b0e1ac69ca2647d97ce9 # ratchet:anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ env.REVIEW_PROMPT }}
use_sticky_comment: ${{ fromJSON(env.USE_STICKY_COMMENT) }}
track_progress: ${{ fromJSON(env.TRACK_PROGRESS) }}
claude_args: ${{ env.CLAUDE_ARGS }}
additional_permissions: ${{ env.ADDITIONAL_PERMISSIONS }}
# The action reports success even when the Claude execution errors out
# (e.g. expired OAuth token or exhausted usage limit), leaving PRs with a
# green check and no review. Fail loudly and surface the error text,
# which the action redacts from its own logs.
- name: Fail on Claude execution error
if: always()
run: |
FILE='${{ steps.review.outputs.execution_file }}'
[ -f "$FILE" ] || exit 0
jq -e 'last(.[] | select(.type == "result")) | .is_error == true' "$FILE" >/dev/null || exit 0
echo "::error::Claude review failed: $(jq -r 'last(.[] | select(.type == "result")) | .result // "no error text"' "$FILE" | tr '\n' ' ' | head -c 500)"
exit 1
# Fable's cybersecurity classifier can silently swap the session to Opus
# mid-run; the model's own context still claims Fable, so the transcript
# is the only reliable signal of the downgrade.
- name: Flag Opus fallback
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
FILE='${{ steps.review.outputs.execution_file }}'
[ -f "$FILE" ] && grep -q '"model_refusal_fallback"' "$FILE" || exit 0
echo "::warning::Fable safeguard triggered — review served by Opus fallback"
COMMENT=$(gh api --paginate "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
--jq '.[] | select(.user.login == "claude[bot]")' | jq -s 'last')
[ -n "$COMMENT" ] && [ "$COMMENT" != "null" ] || exit 0
gh api --method PATCH "repos/${{ github.repository }}/issues/comments/$(jq -r .id <<<"$COMMENT")" \
-f body="> [!WARNING]
> **Fable's cybersecurity safeguard triggered — this review was served by Opus 4.8.** Ignore it and re-review with Fable locally.
$(jq -r .body <<<"$COMMENT")"
+182
View File
@@ -0,0 +1,182 @@
name: Code Quality
on:
push:
branches: [main]
paths:
- "packages/**"
- "api-surface/**"
- "apps/**"
- "examples/**"
- "templates/**"
- "scripts/generate-api-surface.mjs"
- "scripts/check-api-surface.mjs"
- "scripts/lib/script-options.mjs"
- "scripts/sync-templates.sh"
- "turbo.json"
- "package.json"
- "pnpm-lock.yaml"
- ".github/workflows/code-quality.yaml"
pull_request:
branches: [main]
paths:
- "packages/**"
- "api-surface/**"
- "apps/**"
- "examples/**"
- "templates/**"
- "scripts/generate-api-surface.mjs"
- "scripts/check-api-surface.mjs"
- "scripts/lib/script-options.mjs"
- "scripts/sync-templates.sh"
- "turbo.json"
- "package.json"
- "pnpm-lock.yaml"
- ".github/workflows/code-quality.yaml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Oxlint + Oxfmt
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run oxlint + oxfmt
run: pnpm lint
- name: Check resource memoization (React Compiler)
run: pnpm check:resource-memo
template-sync:
name: Template Sync
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 2
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 1
- name: Verify templates match packages/ui source
run: bash scripts/sync-templates.sh
build:
name: Build Changed Packages
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Cache turbo build setup
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # ratchet:actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-build-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-build-${{ github.ref }}-
${{ runner.os }}-turbo-build-
${{ runner.os }}-turbo-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PR: Build packages changed compared to base branch
pnpm turbo build --filter="...[origin/${{ github.base_ref }}]"
else
# Push to main: Build packages changed in last commit
pnpm turbo build --filter="...[HEAD^1]"
fi
- name: Check API surface
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PR: Check API surface for packages changed compared to base branch
pnpm run api-surface:check -- --skip-build --filter="...[origin/${{ github.base_ref }}]"
else
# Push to main: Check API surface for packages changed in last commit
pnpm run api-surface:check -- --skip-build --filter="...[HEAD^1]"
fi
- name: Test API surface generator
run: pnpm run api-surface:test
test:
name: Test Changed Packages
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
# Note: No dependency on 'build' job - Turbo handles test->build dependencies internally via dependsOn
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Cache turbo build setup
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # ratchet:actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-test-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-test-${{ github.ref }}-
${{ runner.os }}-turbo-test-
${{ runner.os }}-turbo-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PR: Test packages changed compared to base branch
pnpm turbo test --filter="...[origin/${{ github.base_ref }}]"
else
# Push to main: Test packages changed in last commit
pnpm turbo test --filter="...[HEAD^1]"
fi
+70
View File
@@ -0,0 +1,70 @@
name: Deploy Expo Example
on:
push:
branches:
- main
paths:
- examples/with-expo/**
- packages/react-native/**
- packages/react-ai-sdk/**
- packages/core/**
- .github/workflows/expo.yaml
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_EXPO_PROJECT_ID }}
EXPO_PUBLIC_CHAT_ENDPOINT_URL: https://www.assistant-ui.com/api/chat
jobs:
deploy-production:
name: Deploy Production
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Cache turbo build setup
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # ratchet:actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref }}-
${{ runner.os }}-turbo-
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Pull Vercel Environment Information
working-directory: examples/with-expo
run: vercel pull --yes --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
working-directory: examples/with-expo
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
working-directory: examples/with-expo
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
+70
View File
@@ -0,0 +1,70 @@
name: Deploy Ink Example
on:
push:
branches:
- main
paths:
- examples/with-react-ink-web/**
- packages/react-ink/**
- packages/react-ink-markdown/**
- packages/react-ai-sdk/**
- packages/core/**
- .github/workflows/ink.yaml
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_INK_PROJECT_ID }}
jobs:
deploy-production:
name: Deploy Production
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Cache turbo build setup
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # ratchet:actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref }}-
${{ runner.os }}-turbo-
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Pull Vercel Environment Information
working-directory: examples/with-react-ink-web
run: vercel pull --yes --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
working-directory: examples/with-react-ink-web
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
working-directory: examples/with-react-ink-web
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
+816
View File
@@ -0,0 +1,816 @@
name: npm Publish
on:
repository_dispatch:
types: [npm-publish]
workflow_dispatch:
inputs:
releaseSha:
description: "Release commit SHA to publish (defaults to latest default-branch commit)"
required: false
type: string
permissions:
contents: read
env:
CI: true
jobs:
approve:
name: Prepare npm publish
runs-on: ubuntu-24.04
concurrency:
group: npm-publish-approval
cancel-in-progress: true
outputs:
release_sha: ${{ steps.release-sha.outputs.value }}
default_branch: ${{ steps.release-sha.outputs.default_branch }}
steps:
- name: Resolve release commit SHA
id: release-sha
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const dispatchReleaseShaRaw = context.payload?.client_payload?.releaseSha;
const manualReleaseShaRaw = context.payload?.inputs?.releaseSha;
const payloadDefaultBranchRaw =
context.payload?.repository?.default_branch;
const dispatchReleaseSha =
typeof dispatchReleaseShaRaw === "string"
? dispatchReleaseShaRaw.trim()
: "";
const manualReleaseSha =
typeof manualReleaseShaRaw === "string"
? manualReleaseShaRaw.trim()
: "";
const payloadDefaultBranch =
typeof payloadDefaultBranchRaw === "string"
? payloadDefaultBranchRaw.trim()
: "";
let releaseSha = dispatchReleaseSha || manualReleaseSha;
let defaultBranch = payloadDefaultBranch;
if (!defaultBranch) {
const repoInfo = await github.rest.repos.get(context.repo);
defaultBranch = repoInfo.data.default_branch;
}
if (!defaultBranch) {
core.setFailed("Unable to determine repository default branch");
return;
}
if (!releaseSha) {
if (context.eventName === "repository_dispatch") {
core.setFailed("repository_dispatch payload is missing releaseSha");
return;
}
const ref = await github.rest.git.getRef({
...context.repo,
ref: `heads/${defaultBranch}`,
});
releaseSha = ref.data.object.sha;
core.info(
`No release SHA provided; using ${defaultBranch} HEAD ${releaseSha}`,
);
}
if (!/^[0-9a-f]{40}$/i.test(releaseSha)) {
core.setFailed(
`releaseSha must be a full 40-character commit SHA: ${releaseSha}`,
);
return;
}
core.setOutput("value", releaseSha);
core.setOutput("default_branch", defaultBranch);
cancel_pending:
name: Cancel stale pending publishes
needs: approve
runs-on: ubuntu-24.04
permissions:
actions: write
contents: read
env:
DEFAULT_BRANCH: ${{ needs.approve.outputs.default_branch }}
steps:
- name: Cancel older queued/waiting runs
continue-on-error: true
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const currentRunId = context.runId;
const currentRunNumber = context.runNumber;
const defaultBranch = (process.env.DEFAULT_BRANCH || "").trim();
const workflowRef = process.env.GITHUB_WORKFLOW_REF || "";
const workflowPathMatch = workflowRef.match(
/^[^/]+\/[^/]+\/(.+)@/,
);
const workflowPath = workflowPathMatch?.[1] ?? "";
const workflowId = workflowPath.split("/").pop() || "";
let runs = [];
if (workflowId) {
runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id: workflowId,
per_page: 100,
});
} else {
core.warning(
"Unable to resolve workflow filename; using repository-wide workflow run list",
);
runs = await github.paginate(
github.rest.actions.listWorkflowRunsForRepo,
{
owner,
repo,
per_page: 100,
},
);
}
const candidates = runs.filter((run) => {
if (run.id === currentRunId) return false;
if (run.run_number >= currentRunNumber) return false;
if (run.status === "completed") return false;
if (workflowPath && run.path !== workflowPath) return false;
if (defaultBranch && run.head_branch && run.head_branch !== defaultBranch) {
return false;
}
if (
run.event !== "repository_dispatch" &&
run.event !== "workflow_dispatch"
) {
return false;
}
return true;
});
let canceledCount = 0;
for (const run of candidates) {
const jobs = await github.paginate(
github.rest.actions.listJobsForWorkflowRun,
{
owner,
repo,
run_id: run.id,
per_page: 100,
},
);
const publishJob = jobs.find((job) => job.name === "Publish to npm");
if (publishJob?.status === "in_progress") {
core.info(
`Keeping run ${run.id} because Publish to npm is in progress`,
);
continue;
}
try {
await github.rest.actions.cancelWorkflowRun({
owner,
repo,
run_id: run.id,
});
canceledCount += 1;
core.info(`Canceled stale pending run ${run.id}`);
} catch (error) {
core.warning(
`Unable to cancel run ${run.id}: ${error.message}`,
);
}
}
core.info(`Canceled ${canceledCount} stale pending run(s)`);
summary:
name: Release summary
needs:
- approve
runs-on: ubuntu-24.04
outputs:
target_count: ${{ steps.targets.outputs.targetCount }}
targets_json: ${{ steps.targets.outputs.targetsJson }}
env:
RELEASE_SHA: ${{ needs.approve.outputs.release_sha }}
DEFAULT_BRANCH: ${{ needs.approve.outputs.default_branch }}
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ env.RELEASE_SHA }}
- name: Validate release commit is on default branch
run: |
if [ -z "$RELEASE_SHA" ]; then
echo "Missing releaseSha" >&2
exit 1
fi
if [ -z "$DEFAULT_BRANCH" ]; then
echo "Missing default branch" >&2
exit 1
fi
head_sha=$(git rev-parse HEAD)
if [ "$head_sha" != "$RELEASE_SHA" ]; then
echo "Checked out commit does not match releaseSha payload" >&2
echo "HEAD=$head_sha" >&2
echo "releaseSha=$RELEASE_SHA" >&2
exit 1
fi
git fetch origin "$DEFAULT_BRANCH"
origin_default_sha=$(git rev-parse "origin/$DEFAULT_BRANCH")
if ! git merge-base --is-ancestor "$RELEASE_SHA" "origin/$DEFAULT_BRANCH"; then
echo "releaseSha is not an ancestor of origin/$DEFAULT_BRANCH; refusing to publish" >&2
echo "releaseSha=$RELEASE_SHA" >&2
echo "origin/$DEFAULT_BRANCH SHA=$origin_default_sha" >&2
exit 1
fi
- name: Validate no pending changesets
run: |
pending=$(find .changeset -name '*.md' ! -name 'README.md' | sort)
if [ -n "$pending" ]; then
echo "::error::Release SHA has pending changesets, so changesets/action would update the version PR instead of publishing."
echo "::error::Merge the version PR first, then dispatch with its merge commit as releaseSha."
echo "Pending changesets:"
echo "$pending"
exit 1
fi
- name: Resolve publish targets
id: targets
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require("node:fs");
const path = require("node:path");
const fetchNpmMetadata = async (packageName) => {
const encodedName = encodeURIComponent(packageName);
const url = `https://registry.npmjs.org/${encodedName}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
try {
const response = await fetch(url, {
signal: controller.signal,
});
if (response.ok) {
return await response.json();
}
if (response.status === 404) {
return null;
}
throw new Error(`HTTP ${response.status}`);
} finally {
clearTimeout(timeout);
}
};
const parseStableSemver = (value) => {
if (typeof value !== "string") return null;
const match = value.match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) return null;
return match.slice(1).map((part) => Number(part));
};
const compareStableSemver = (a, b) => {
const parsedA = parseStableSemver(a);
const parsedB = parseStableSemver(b);
if (!parsedA || !parsedB) return null;
for (let index = 0; index < 3; index += 1) {
if (parsedA[index] > parsedB[index]) return 1;
if (parsedA[index] < parsedB[index]) return -1;
}
return 0;
};
const packagesDir = path.join(process.cwd(), "packages");
const entries = fs.readdirSync(packagesDir, { withFileTypes: true });
const prereleasePackages = [];
const downgradePackages = [];
const targets = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const manifestPath = path.join(packagesDir, entry.name, "package.json");
if (!fs.existsSync(manifestPath)) continue;
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
if (manifest.private === true) continue;
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
core.setFailed(`Package ${manifest.name} is missing a version`);
return;
}
if (manifest.version.includes("-")) {
prereleasePackages.push(`${manifest.name}@${manifest.version}`);
continue;
}
const pkgName = manifest.name;
const pkgVersion = manifest.version;
let metadata = null;
try {
metadata = await fetchNpmMetadata(pkgName);
} catch (error) {
core.setFailed(
`Failed to query npm metadata for ${pkgName}: ${error.message}`,
);
return;
}
const latestTag = metadata?.["dist-tags"]?.latest ?? null;
const hasVersion = Boolean(metadata?.versions?.[pkgVersion]);
if (latestTag !== null) {
const comparison = compareStableSemver(pkgVersion, latestTag);
if (comparison === null) {
core.setFailed(
`Unsupported semver for ${pkgName}: local=${pkgVersion} latest=${latestTag}`,
);
return;
}
if (comparison < 0) {
downgradePackages.push(
`${pkgName} local=${pkgVersion} latest=${latestTag}`,
);
continue;
}
}
if (hasVersion) {
if (latestTag !== pkgVersion) {
core.warning(
`${pkgName}@${pkgVersion} is on npm but latest is ${latestTag ?? "unset"}; run: npm dist-tag add ${pkgName}@${pkgVersion} latest --registry https://registry.npmjs.org`,
);
}
} else {
targets.push({
name: pkgName,
version: pkgVersion,
latestVersion: latestTag,
dir: entry.name,
});
}
}
if (prereleasePackages.length > 0) {
core.setFailed(
`Prerelease versions are not allowed on default-branch releases: ${prereleasePackages.join(", ")}`,
);
return;
}
if (downgradePackages.length > 0) {
core.setFailed(
`Refusing to retag older versions as latest: ${downgradePackages.join(", ")}`,
);
return;
}
targets.sort((a, b) => a.name.localeCompare(b.name));
core.info(
`Targets: ${targets.length === 0 ? "none" : targets.map((target) => `${target.name}@${target.version}`).join(", ")}`,
);
core.setOutput("targetCount", String(targets.length));
core.setOutput("targetsJson", JSON.stringify(targets));
- name: Write release summary
if: steps.targets.outputs.targetCount != '0'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
TARGETS_JSON: ${{ steps.targets.outputs.targetsJson }}
with:
script: |
const fs = require("node:fs");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const targets = JSON.parse(process.env.TARGETS_JSON || "[]");
const releaseSha = process.env.RELEASE_SHA;
const runGit = (args) => {
try {
return execFileSync("git", args, { encoding: "utf8" }).trim();
} catch {
return "";
}
};
const readJsonFromGit = (gitRef, filePath) => {
try {
return JSON.parse(
execFileSync(
"git",
["show", `${gitRef}:${filePath}`],
{ encoding: "utf8" },
),
);
} catch {
return null;
}
};
function classifyVersionBump(previousVersion, newVersion) {
const [pMajor, pMinor, pPatch] = previousVersion.split(".").map(Number);
const [nMajor, nMinor, nPatch] = newVersion.split(".").map(Number);
if (nMajor !== pMajor) return "major";
if (nMinor !== pMinor) return "minor";
if (nPatch !== pPatch) return "patch";
return "none";
}
function isOutsideCaretRange(rangeVersion, newVersion) {
const [rMajor, rMinor, rPatch] = rangeVersion.split(".").map(Number);
const [nMajor, nMinor, nPatch] = newVersion.split(".").map(Number);
if (rMajor === 0 && rMinor === 0) {
return nMajor !== 0 || nMinor !== 0 || nPatch !== rPatch;
}
if (rMajor === 0) return nMajor !== 0 || nMinor !== rMinor;
return nMajor !== rMajor;
}
// --- 1. Version changes table ---
let summary = "## Release Summary\n\n";
summary += `**Release SHA:** \`${releaseSha.slice(0, 12)}\`\n`;
summary += `**Packages:** ${targets.length}\n\n`;
summary += "### Version Changes\n\n";
summary += "| Package | Previous | New |\n";
summary += "| --- | --- | --- |\n";
for (const t of targets) {
const prev = t.latestVersion ?? "_new_";
summary += `| \`${t.name}\` | ${prev} | **${t.version}** |\n`;
}
summary += "\n";
const breakingTargets = targets
.filter((target) => target.latestVersion)
.map((target) => ({
...target,
previousVersion: target.latestVersion,
bumpType: classifyVersionBump(target.latestVersion, target.version),
}))
.filter((target) => isOutsideCaretRange(target.previousVersion, target.version))
.sort((a, b) => a.name.localeCompare(b.name));
summary += "### Breaking Version Changes\n\n";
if (breakingTargets.length > 0) {
summary += "These published versions move outside the previous `^` range and would be breaking for caret consumers if this release is published:\n\n";
summary += "| Package | Previous | New | Bump |\n";
summary += "| --- | --- | --- | --- |\n";
for (const target of breakingTargets) {
summary += `| \`${target.name}\` | ${target.previousVersion} | **${target.version}** | ${target.bumpType} |\n`;
}
} else {
summary += "No published package versions move outside their previous `^` range.\n";
}
summary += "\n";
// --- 2. Cascade impact ---
const packagesDir = path.join(process.cwd(), "packages");
const currentPackages = new Map();
const previousPackages = new Map();
for (const entry of fs.readdirSync(packagesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const dir = entry.name;
const pkgJsonPath = path.join(packagesDir, dir, "package.json");
let pkgJson;
try {
pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
} catch {
continue;
}
if (pkgJson.private) continue;
currentPackages.set(pkgJson.name, {
dir,
...pkgJson,
});
const previousPkgJson = readJsonFromGit(
`${releaseSha}^`,
path.join("packages", dir, "package.json"),
);
if (!previousPkgJson || previousPkgJson.private) continue;
previousPackages.set(previousPkgJson.name, {
dir,
...previousPkgJson,
});
}
const targetVersions = new Map(
targets.map((target) => [target.name, target.version]),
);
const cascadeByConsumer = new Map();
for (const previousPkg of previousPackages.values()) {
const allDeps = {
...previousPkg.dependencies,
...previousPkg.peerDependencies,
};
for (const [depName, range] of Object.entries(allDeps)) {
if (typeof range !== "string" || !range.startsWith("^")) continue;
const targetVersion = targetVersions.get(depName);
if (!targetVersion) continue;
const rangeVersion = range.replace(/^\^/, "");
if (!isOutsideCaretRange(rangeVersion, targetVersion)) continue;
const currentPkg = currentPackages.get(previousPkg.name);
const existing = cascadeByConsumer.get(previousPkg.name);
if (existing) {
existing.triggers.push({
name: depName,
range,
version: targetVersion,
});
continue;
}
cascadeByConsumer.set(previousPkg.name, {
name: previousPkg.name,
previousVersion: previousPkg.version,
newVersion: currentPkg?.version ?? previousPkg.version,
triggers: [
{
name: depName,
range,
version: targetVersion,
},
],
});
}
}
const cascadeEntries = Array.from(cascadeByConsumer.values()).sort(
(a, b) => a.name.localeCompare(b.name),
);
if (cascadeEntries.length > 0) {
summary += "### Downstream Impact\n\n";
summary += `${cascadeEntries.length} internal consumer(s) have \`^\` ranges that will be broken by these version changes:\n\n`;
summary += "| Consumer | Version | Broken Dependencies |\n";
summary += "| --- | --- | --- |\n";
for (const c of cascadeEntries) {
const brokenDeps = c.triggers
.map((trigger) => `\`${trigger.name}\` (${trigger.range} -> ${trigger.version})`)
.join(", ");
summary += `| \`${c.name}\` | ${c.previousVersion} -> ${c.newVersion} | ${brokenDeps} |\n`;
}
summary += "\n";
}
// --- 3. Changelog entries ---
summary += "### Changelog\n\n";
for (const t of targets) {
const changelogPath = path.join(packagesDir, t.dir, "CHANGELOG.md");
let changelogEntry = "";
try {
const content = fs.readFileSync(changelogPath, "utf8");
const versionEscaped = t.version.replace(/\./g, "\\.");
const headingRe = new RegExp("^## " + versionEscaped + "\\b", "m");
const headingMatch = content.match(headingRe);
if (headingMatch) {
const headingEnd = content.indexOf("\n", headingMatch.index);
const afterHeading = headingEnd === -1 ? content.length : headingEnd + 1;
const rest = content.slice(afterHeading);
const nextIdx = rest.search(/^## /m);
changelogEntry = (nextIdx === -1 ? rest : rest.slice(0, nextIdx)).trim();
}
} catch {
// No changelog file
}
if (changelogEntry) {
summary += `<details><summary><code>${t.name}@${t.version}</code></summary>\n\n`;
summary += changelogEntry + "\n";
summary += "</details>\n\n";
} else {
summary += `- \`${t.name}@${t.version}\` — _no changelog entry_\n`;
}
}
summary += "\n";
// --- 4. Commit history ---
const prevVersionCommit = runGit([
"log", "--format=%H", "--fixed-strings", "--grep=chore: update versions",
`${releaseSha}^`, "--max-count=1",
]);
if (prevVersionCommit) {
const commitLog = runGit([
"log", "--oneline", "--no-merges",
`${prevVersionCommit}..${releaseSha}`,
]);
if (commitLog) {
summary += "### Commits in This Release\n\n";
summary += "```\n" + commitLog + "\n```\n\n";
}
}
await core.summary.addRaw(summary).write();
core.info("Release summary written to job summary.");
- name: No targets
if: steps.targets.outputs.targetCount == '0'
run: echo "No packages require publishing for this release SHA."
publish:
name: Publish to npm
needs:
- approve
- cancel_pending
- summary
if: needs.summary.outputs.target_count != '0'
environment: npm Publish
runs-on: ubuntu-24.04
permissions:
contents: write
# Required for npm trusted publishing (OIDC).
id-token: write
concurrency:
group: npm-publish-exec
cancel-in-progress: false
env:
RELEASE_SHA: ${{ needs.approve.outputs.release_sha }}
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ env.RELEASE_SHA }}
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
# No cache - hardened against cache poisoning for sensitive publish operations
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Verify npm trusted publishing
# Temporarily disabled: npm appears to reject the subsequent CLI OIDC
# exchange after this pre-flight call within the same job, causing
# "OIDC publish authorize: Invalid token" on every package.
if: false
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
TARGETS_JSON: ${{ needs.summary.outputs.targets_json }}
with:
script: |
const targets = JSON.parse(process.env.TARGETS_JSON || "[]");
const oidcRequestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const oidcRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!oidcRequestUrl || !oidcRequestToken) {
core.setFailed("OIDC token request context is unavailable");
return;
}
const audience = "npm:registry.npmjs.org";
const oidcUrl = `${oidcRequestUrl}${oidcRequestUrl.includes("?") ? "&" : "?"}audience=${encodeURIComponent(audience)}`;
const fetchOidcToken = async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
try {
const response = await fetch(oidcUrl, {
headers: { Authorization: `Bearer ${oidcRequestToken}` },
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
if (!payload?.value) {
throw new Error("OIDC response is missing token value");
}
return payload.value;
} finally {
clearTimeout(timeout);
}
};
const failures = [];
for (const target of targets) {
let githubOidcToken;
try {
githubOidcToken = await fetchOidcToken();
} catch (error) {
core.setFailed(
`Failed to fetch GitHub OIDC token: ${error.message}`,
);
return;
}
const encodedName = encodeURIComponent(target.name);
const exchangeUrl =
`https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/${encodedName}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
let response;
try {
response = await fetch(exchangeUrl, {
method: "POST",
headers: { Authorization: `Bearer ${githubOidcToken}` },
signal: controller.signal,
});
} catch (error) {
failures.push(`${target.name}: network error (${error.message})`);
continue;
} finally {
clearTimeout(timeout);
}
if (!response.ok) {
const hint =
response.status === 404
? "not configured for npm trusted publishing"
: `HTTP ${response.status}`;
failures.push(`${target.name}: ${hint}`);
}
}
if (failures.length > 0) {
core.setFailed(
[
`Pre-flight OIDC verification failed for ${failures.length} package(s):`,
...failures.map((f) => ` - ${f}`),
"",
"Configure trusted publishing for each failed package at:",
" https://docs.npmjs.com/trusted-publishers",
].join("\n"),
);
return;
}
core.info(
`All ${targets.length} package(s) verified for npm trusted publishing.`,
);
- name: Publish to npm and create GitHub releases
id: changesets-publish
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
publish: pnpm ci:publish
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The action's fallback version path commits without ci:version's lockfile refresh, so the pre-commit hook's pnpm run fails on the stale lockfile.
HUSKY: 0
- name: Recovery guidance
if: failure()
run: |
echo "::error::Publish failed for release SHA ${RELEASE_SHA}."
echo "::error::Rerun this workflow run, or run workflow_dispatch with releaseSha=${RELEASE_SHA}."
+309
View File
@@ -0,0 +1,309 @@
name: PyPI Publish
on:
repository_dispatch:
types: [pypi-publish]
workflow_dispatch:
inputs:
releaseSha:
description: "Release commit SHA to publish (defaults to latest default-branch commit)"
required: false
type: string
permissions:
contents: read
env:
CI: true
PYTHON_PACKAGE_DIR: python/assistant-stream
PYTHON_PACKAGE_NAME: assistant-stream
jobs:
approve:
name: Resolve release SHA
runs-on: ubuntu-24.04
concurrency:
group: pypi-publish-approval
cancel-in-progress: true
outputs:
release_sha: ${{ steps.release-sha.outputs.value }}
default_branch: ${{ steps.release-sha.outputs.default_branch }}
steps:
- name: Resolve release commit SHA
id: release-sha
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const dispatchReleaseShaRaw = context.payload?.client_payload?.releaseSha;
const manualReleaseShaRaw = context.payload?.inputs?.releaseSha;
const payloadDefaultBranchRaw =
context.payload?.repository?.default_branch;
const dispatchReleaseSha =
typeof dispatchReleaseShaRaw === "string"
? dispatchReleaseShaRaw.trim()
: "";
const manualReleaseSha =
typeof manualReleaseShaRaw === "string"
? manualReleaseShaRaw.trim()
: "";
const payloadDefaultBranch =
typeof payloadDefaultBranchRaw === "string"
? payloadDefaultBranchRaw.trim()
: "";
let releaseSha = dispatchReleaseSha || manualReleaseSha;
let defaultBranch = payloadDefaultBranch;
if (!defaultBranch) {
const repoInfo = await github.rest.repos.get(context.repo);
defaultBranch = repoInfo.data.default_branch;
}
if (!defaultBranch) {
core.setFailed("Unable to determine repository default branch");
return;
}
if (!releaseSha) {
if (context.eventName === "repository_dispatch") {
core.setFailed("repository_dispatch payload is missing releaseSha");
return;
}
const ref = await github.rest.git.getRef({
...context.repo,
ref: `heads/${defaultBranch}`,
});
releaseSha = ref.data.object.sha;
core.info(
`No release SHA provided; using ${defaultBranch} HEAD ${releaseSha}`,
);
}
if (!/^[0-9a-f]{40}$/i.test(releaseSha)) {
core.setFailed(
`releaseSha must be a full 40-character commit SHA: ${releaseSha}`,
);
return;
}
core.setOutput("value", releaseSha);
core.setOutput("default_branch", defaultBranch);
summary:
name: Resolve publish target
needs: approve
runs-on: ubuntu-24.04
outputs:
should_publish: ${{ steps.target.outputs.shouldPublish }}
version: ${{ steps.target.outputs.version }}
env:
RELEASE_SHA: ${{ needs.approve.outputs.release_sha }}
DEFAULT_BRANCH: ${{ needs.approve.outputs.default_branch }}
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ env.RELEASE_SHA }}
- name: Validate release commit is on default branch
run: |
if [ -z "$RELEASE_SHA" ]; then
echo "Missing releaseSha" >&2
exit 1
fi
if [ -z "$DEFAULT_BRANCH" ]; then
echo "Missing default branch" >&2
exit 1
fi
head_sha=$(git rev-parse HEAD)
if [ "$head_sha" != "$RELEASE_SHA" ]; then
echo "Checked out commit does not match releaseSha payload" >&2
echo "HEAD=$head_sha" >&2
echo "releaseSha=$RELEASE_SHA" >&2
exit 1
fi
git fetch origin "$DEFAULT_BRANCH"
origin_default_sha=$(git rev-parse "origin/$DEFAULT_BRANCH")
if ! git merge-base --is-ancestor "$RELEASE_SHA" "origin/$DEFAULT_BRANCH"; then
echo "releaseSha is not an ancestor of origin/$DEFAULT_BRANCH; refusing to publish" >&2
echo "releaseSha=$RELEASE_SHA" >&2
echo "origin/$DEFAULT_BRANCH SHA=$origin_default_sha" >&2
exit 1
fi
- name: Resolve target version + PyPI status
id: target
run: |
set -euo pipefail
version=$(python3 - <<'PY' "$PYTHON_PACKAGE_DIR/pyproject.toml"
import sys, tomllib
with open(sys.argv[1], "rb") as f:
data = tomllib.load(f)
v = data.get("project", {}).get("version")
if not v:
sys.exit("[project].version missing from pyproject.toml")
print(v)
PY
)
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Refusing to publish non-stable version: $version" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
pypi_body=$(mktemp)
trap 'rm -f "$pypi_body"' EXIT
status=$(curl -s --max-time 30 -o "$pypi_body" -w "%{http_code}" \
"https://pypi.org/pypi/$PYTHON_PACKAGE_NAME/json")
case "$status" in
200)
latest=$(python3 -c 'import sys, json; print(json.load(sys.stdin).get("info", {}).get("version") or "")' < "$pypi_body")
exists=$(python3 -c 'import sys, json; d=json.load(sys.stdin); print("true" if sys.argv[1] in d.get("releases", {}) else "false")' "$version" < "$pypi_body")
;;
404)
latest=""
exists="false"
;;
*)
echo "Unexpected PyPI status $status for $PYTHON_PACKAGE_NAME" >&2
exit 1
;;
esac
if [ -n "$latest" ] && [ "$version" != "$latest" ]; then
highest=$(printf '%s\n%s\n' "$latest" "$version" | sort -V | tail -n1)
if [ "$highest" != "$version" ]; then
echo "Refusing to publish $PYTHON_PACKAGE_NAME@$version: lower than current PyPI latest ($latest)" >&2
exit 1
fi
fi
if [ "$exists" = "true" ]; then
echo "$PYTHON_PACKAGE_NAME@$version already on PyPI; skipping publish."
echo "shouldPublish=false" >> "$GITHUB_OUTPUT"
else
echo "$PYTHON_PACKAGE_NAME@$version not yet on PyPI; will publish."
echo "shouldPublish=true" >> "$GITHUB_OUTPUT"
fi
- name: Write release summary
if: steps.target.outputs.shouldPublish == 'true'
run: |
{
echo "## PyPI Release Summary"
echo
echo "**Release SHA:** \`${RELEASE_SHA:0:12}\`"
echo "**Package:** \`$PYTHON_PACKAGE_NAME\`"
echo "**Version:** **${{ steps.target.outputs.version }}**"
} >> "$GITHUB_STEP_SUMMARY"
- name: No targets
if: steps.target.outputs.shouldPublish == 'false'
run: echo "$PYTHON_PACKAGE_NAME@${{ steps.target.outputs.version }} is already published; nothing to do."
publish:
name: Publish to PyPI
needs:
- approve
- summary
if: needs.summary.outputs.should_publish == 'true'
environment: pypi publish
runs-on: ubuntu-24.04
permissions:
# Required to create GitHub releases.
contents: write
# Required for PyPI trusted publishing (OIDC).
id-token: write
concurrency:
group: pypi-publish-exec
cancel-in-progress: false
env:
RELEASE_SHA: ${{ needs.approve.outputs.release_sha }}
VERSION: ${{ needs.summary.outputs.version }}
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
fetch-tags: true
ref: ${{ env.RELEASE_SHA }}
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
# No cache - hardened against cache poisoning for sensitive publish operations
enable-cache: false
- name: Build sdist + wheel
working-directory: ${{ env.PYTHON_PACKAGE_DIR }}
run: |
set -euo pipefail
rm -rf dist
uv build
ls -la dist
- name: Verify built version matches target
working-directory: ${{ env.PYTHON_PACKAGE_DIR }}
run: |
set -euo pipefail
dist_name="${PYTHON_PACKAGE_NAME//-/_}-${VERSION}"
shopt -s nullglob
wheels=( "dist/${dist_name}"-*.whl )
if [ "${#wheels[@]}" -eq 0 ] || [ ! -f "dist/${dist_name}.tar.gz" ]; then
echo "Expected build artifacts for ${dist_name} are missing." >&2
ls dist >&2 || true
exit 1
fi
# PyPI trusted publishing requires this workflow filename and the
# `pypi publish` environment to match the project's trusted-publisher config.
- name: Publish to PyPI via OIDC
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: ${{ env.PYTHON_PACKAGE_DIR }}/dist
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="pypi/${PYTHON_PACKAGE_NAME}@${VERSION}"
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag already exists; skipping."
exit 0
fi
prev=$(git tag --list "pypi/${PYTHON_PACKAGE_NAME}@*" \
| sort -V \
| awk -v t="$tag" '$0 != t' \
| tail -n1)
if [ -n "$prev" ]; then
gh release create "$tag" \
--target "$RELEASE_SHA" \
--title "$tag" \
--notes-start-tag "$prev" \
--generate-notes
else
gh release create "$tag" \
--target "$RELEASE_SHA" \
--title "$tag" \
--notes "First $PYTHON_PACKAGE_NAME release published via this workflow."
fi
- name: Recovery guidance
if: failure()
run: |
echo "::error::Publish failed for $PYTHON_PACKAGE_NAME@${VERSION} on release SHA ${RELEASE_SHA}."
echo "::error::Rerun this workflow run, or workflow_dispatch with releaseSha=${RELEASE_SHA}."
+51
View File
@@ -0,0 +1,51 @@
name: Python Tests
on:
push:
branches: [main]
paths:
- "python/**"
- ".github/workflows/python-tests.yaml"
pull_request:
branches: [main]
paths:
- "python/**"
- ".github/workflows/python-tests.yaml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: pytest (${{ matrix.package }}, ${{ matrix.python-version }})
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
package:
- assistant-stream
- assistant-ui-sync-server-api
python-version: ["3.10", "3.12"]
defaults:
run:
working-directory: python/${{ matrix.package }}
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install dependencies
run: uv sync --all-extras
- name: Run tests
run: uv run pytest
+67
View File
@@ -0,0 +1,67 @@
name: Deploy Shadcn Registry
on:
push:
branches:
- main
paths:
- apps/registry/**
- packages/ui/**
- .github/workflows/registry.yaml
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_REGISTRY_PROJECT_ID }}
jobs:
deploy-production:
name: Deploy Production
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v7
with:
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # ratchet:pnpm/action-setup@v6.0.9
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Cache turbo build setup
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # ratchet:actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref }}-
${{ runner.os }}-turbo-
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Pull Vercel Environment Information
working-directory: apps/registry
run: vercel pull --yes --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
working-directory: apps/registry
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
working-directory: apps/registry
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
+157
View File
@@ -0,0 +1,157 @@
name: Template Metrics
on:
push:
branches: [main]
paths:
- "templates/**"
- "packages/ui/**"
pull_request:
branches: [main]
paths:
- "templates/**"
- "packages/ui/**"
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# Templates to build for bundle-size measurement. LOC is measured for all
# templates regardless; only these are built (next build is the expensive part).
BUNDLE_TEMPLATES: "default minimal"
# Per-template regression limits. A template that already exists on the main
# baseline fails the gate if it grows past either limit.
MAX_LOC_INCREASE: "50"
MAX_BUNDLE_INCREASE_KB: "10"
jobs:
metrics:
name: LOC + Bundle Size
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 20
steps:
- name: Checkout code repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # ratchet:actions/checkout@v6
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # ratchet:pnpm/action-setup@v4
- name: Setup node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # ratchet:actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Cache turbo build setup
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # ratchet:actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref }}-
${{ runner.os }}-turbo-
# Baseline is main's metrics, cached by the last run on main. PRs restore
# the newest one via the restore-keys prefix to compute deltas against main.
- name: Restore main baseline
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # ratchet:actions/cache/restore@v4
with:
path: install-footprint/branch/main/data.json
key: install-footprint-main-${{ github.sha }}
restore-keys: install-footprint-main-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build templates (bundle subset)
env:
# next build needs these public vars defined; values are placeholders.
NEXT_PUBLIC_ASSISTANT_BASE_URL: "https://example.com"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "pk_test_placeholder"
NEXT_TELEMETRY_DISABLED: "1"
run: |
filters=""
for t in $BUNDLE_TEMPLATES; do
filters="$filters --filter=./templates/$t"
done
# turbo pulls in the @assistant-ui/* workspace deps each template needs.
pnpm turbo build $filters
- name: Measure metrics
run: node scripts/template-metrics.mjs measure . metrics-head.json
- name: Build report
run: |
node scripts/template-metrics.mjs report install-footprint/branch/main/data.json metrics-head.json metrics-report.md metrics-gate.txt metrics-comment.txt
cat metrics-report.md
# continue-on-error: a fork PR's read-only token makes commenting 403; that
# must not fail the check or skip the gate below.
- name: Post / update PR comment
if: github.event_name == 'pull_request'
continue-on-error: true
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # ratchet:actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('metrics-report.md', 'utf8');
const marker = '<!-- template-metrics -->';
// paginate: a long PR thread can push the sticky comment past the
// first 30, and missing it would post a duplicate on every re-run.
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find((c) => c.body?.includes(marker));
const shouldCreate = fs.readFileSync('metrics-comment.txt', 'utf8').trim() === 'post';
if (!existing && !shouldCreate) {
console.log('No LOC changes; skipping template metrics comment.');
return;
}
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
# always(): run even if the comment step failed (e.g. fork 403), so the
# gate verdict is never lost. Only blocks PRs; pushes refresh the baseline.
- name: Enforce regression gate
if: always() && github.event_name == 'pull_request'
run: |
if [ "$(cat metrics-gate.txt 2>/dev/null)" = "fail" ]; then
echo "::error::Template metrics regressed past the configured limits. See the PR comment."
exit 1
fi
echo "Template metrics within limits."
- name: Persist baseline (main only)
if: github.event_name == 'push'
run: |
mkdir -p install-footprint/branch/main
cp metrics-head.json install-footprint/branch/main/data.json
- name: Save baseline cache (main only)
if: github.event_name == 'push'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # ratchet:actions/cache/save@v4
with:
path: install-footprint/branch/main/data.json
key: install-footprint-main-${{ github.sha }}
+399
View File
@@ -0,0 +1,399 @@
name: Update README Traction Chart
on:
schedule:
- cron: "0 14 * * 1" # Monday 8 AM Central
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
generate-chart:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Install dependencies
run: |
mkdir -p /tmp/sharp-deps
cd /tmp/sharp-deps
npm init -y > /dev/null
npm install sharp
- name: Generate chart
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_PATH: /tmp/sharp-deps/node_modules
run: |
node << 'SCRIPT'
const sharp = require('sharp');
const fs = require('fs');
const PACKAGE_NAME = '@assistant-ui/react';
const GITHUB_REPO = 'assistant-ui/assistant-ui';
const OUTPUT_PATH = '.github/assets/traction.png';
const COLORS = { primary: '#ffffff', text: '#ffffff', textMuted: '#888888', grid: '#333333' };
const ASSISTANT_UI_LOGO = `<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z" fill="none" stroke="${COLORS.text}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" fill="none" stroke="${COLORS.text}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`;
const GITHUB_LOGO = `<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" fill="${COLORS.textMuted}"/>`;
const NPM_LOGO = `<path d="M0,0 H24 V24 H0 Z M4.5,4.5 V19.5 H12 V7.5 H16.5 V19.5 H19.5 V4.5 Z" fill="${COLORS.textMuted}" fill-rule="evenodd"/>`;
function getStartDate() {
const d = new Date();
d.setMonth(d.getMonth() - 18);
return d.toISOString().split('T')[0];
}
const START_DATE = getStartDate();
async function fetchNpmStats(packageName) {
const end = new Date().toISOString().split('T')[0];
const url = `https://npm-stat.com/api/download-counts?package=${encodeURIComponent(packageName)}&from=${START_DATE}&until=${end}`;
const response = await fetch(url);
if (!response.ok) throw new Error(`npm stats fetch failed: ${response.status}`);
const data = await response.json();
return data[packageName] || {};
}
async function fetchCurrentStars(repo) {
const [owner, name] = repo.split('/');
const headers = { 'Accept': 'application/vnd.github.v3+json' };
if (process.env.GITHUB_TOKEN) {
headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
}
const response = await fetch(`https://api.github.com/repos/${owner}/${name}`, { headers });
if (!response.ok) throw new Error(`GitHub repo fetch failed: ${response.status}`);
const data = await response.json();
return data.stargazers_count;
}
async function fetchStarHistory(repo, currentStars) {
const [owner, name] = repo.split('/');
const stars = [];
let page = 1;
const headers = { 'Accept': 'application/vnd.github.star+json' };
if (process.env.GITHUB_TOKEN) {
headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
}
// Paginate through all stars (no hard limit) with delays to avoid rate limits
while (true) {
if (page > 1) await new Promise(r => setTimeout(r, 100));
let response = await fetch(
`https://api.github.com/repos/${owner}/${name}/stargazers?per_page=100&page=${page}`,
{ headers }
);
if (response.status === 403 || response.status === 429) {
console.warn(`Rate limited on page ${page}, waiting 3 minutes before retry...`);
await new Promise(r => setTimeout(r, 180000));
response = await fetch(
`https://api.github.com/repos/${owner}/${name}/stargazers?per_page=100&page=${page}`,
{ headers }
);
}
if (!response.ok) {
console.warn(`Stopped at page ${page}: HTTP ${response.status} (fetched ${stars.length} stars)`);
break;
}
const data = await response.json();
if (data.length === 0) break;
stars.push(...data.map(s => s.starred_at));
console.log(`Fetched page ${page}, ${stars.length} stars total`);
if (data.length < 100) break;
page++;
}
const startDate = new Date(START_DATE);
const starsBeforeStart = stars.filter(d => new Date(d) < startDate).length;
const weeks = {};
for (const date of stars) {
const d = new Date(date);
if (d < startDate) continue;
const weekStart = getWeekStart(d.toISOString().split('T')[0]);
weeks[weekStart] = (weeks[weekStart] || 0) + 1;
}
const result = [];
const now = new Date();
const currentWeekStart = getWeekStart(now.toISOString().split('T')[0]);
let d = new Date(START_DATE);
const dayOfWeek = d.getDay();
const daysToMonday = dayOfWeek === 0 ? 1 : (dayOfWeek === 1 ? 0 : 8 - dayOfWeek);
d.setDate(d.getDate() + daysToMonday);
let cumulative = starsBeforeStart;
while (d <= now) {
const key = d.toISOString().split('T')[0];
if (key >= currentWeekStart) break;
cumulative += (weeks[key] || 0);
result.push({ date: key, value: cumulative });
d.setDate(d.getDate() + 7);
}
result.push({ date: now.toISOString().split('T')[0], value: currentStars });
return result;
}
function getWeekStart(date) {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
d.setDate(diff);
return d.toISOString().split('T')[0];
}
function aggregateByWeek(dailyData) {
const sorted = Object.entries(dailyData)
.filter(([, count]) => count > 0)
.sort(([a], [b]) => a.localeCompare(b));
if (sorted.length < 7) return sorted.map(([date, value]) => ({ date, value }));
const result = [];
for (let i = 3; i < sorted.length - 4; i += 3) {
let sum = 0;
for (let j = i - 3; j <= i + 3; j++) {
sum += sorted[j][1];
}
result.push({ date: sorted[i][0], value: sum });
}
return result;
}
function smoothData(data) {
if (data.length < 3) return data;
const avg = [];
for (let i = 0; i < data.length; i++) {
const s = Math.max(0, i - 2), e = Math.min(data.length, i + 3);
const w = data.slice(s, e).map(d => d.value);
avg.push({ date: data[i].date, value: w.reduce((a, b) => a + b, 0) / w.length });
}
const out = [avg[0]];
let h = avg[0].value;
for (let i = 1; i < avg.length; i++) {
const p = out[i - 1].value, c = avg[i].value;
h = c > h ? c : h * 0.99;
const v = c > p ? p * 0.3 + c * 0.7 : c < p * 0.8 ? p * 0.85 + h * 0.1 + c * 0.05 : p * 0.95 + c * 0.05;
out.push({ date: avg[i].date, value: Math.round(v) });
}
return out;
}
function getMonthlyRate(dailyData) {
const entries = Object.entries(dailyData).sort(([a], [b]) => a.localeCompare(b));
const recent = entries.slice(-60);
if (recent.length < 30) return Math.round(recent.reduce((s, [, v]) => s + v, 0) / recent.length * 30);
let max = 0;
for (let i = 0; i <= recent.length - 30; i++) {
const sum = recent.slice(i, i + 30).reduce((s, [, v]) => s + v, 0);
if (sum > max) max = sum;
}
return max;
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(0) + 'K';
return num.toString();
}
function smoothPath(pts) {
if (pts.length < 2) return '';
if (pts.length === 2) return `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)} L ${pts[1].x.toFixed(1)} ${pts[1].y.toFixed(1)}`;
let path = `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)}`;
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[Math.max(0, i - 1)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(pts.length - 1, i + 2)];
const cp1x = p1.x + (p2.x - p0.x) / 6;
const cp1y = p1.y + (p2.y - p0.y) / 6;
const cp2x = p2.x - (p3.x - p1.x) / 6;
const cp2y = p2.y - (p3.y - p1.y) / 6;
path += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)}, ${cp2x.toFixed(1)} ${cp2y.toFixed(1)}, ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`;
}
return path;
}
function generateDualSVG(starsData, downloadsData, monthlyDownloads, currentStars, totalDownloads) {
const width = 800;
const height = 380;
const margin = 20;
const gap = 24;
const cardWidth = (width - margin * 2 - gap) / 2;
const cardHeight = height - margin * 2;
const chartPadding = { top: 50, right: 20, bottom: 35, left: 45 };
const innerW = cardWidth - chartPadding.left - chartPadding.right;
const innerH = cardHeight - chartPadding.top - chartPadding.bottom;
function chartCard(data, cardX, cardY, packageName, metricName, subtitle, gradId, clipId, logoSvg, subtitle2 = null, yAxisLabel = null) {
if (!data.length) return '';
const maxVal = Math.max(...data.map(d => d.value));
const chartLeft = cardX + chartPadding.left;
const chartRight = cardX + cardWidth - chartPadding.right;
const chartTop = cardY + chartPadding.top;
const chartBottom = cardY + chartPadding.top + innerH;
const niceStep = (max) => {
const rough = max / 6;
const magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
const normalized = rough / magnitude;
let nice;
if (normalized <= 1) nice = 1;
else if (normalized <= 2) nice = 2;
else if (normalized <= 5) nice = 5;
else nice = 10;
return nice * magnitude;
};
const step = niceStep(maxVal);
const niceMax = Math.ceil(maxVal / step) * step;
const yVals = [];
for (let v = 0; v <= niceMax; v += step) {
yVals.push(v);
}
const pts = data.map((d, i) => ({
x: chartLeft + (data.length > 1 ? (i / (data.length - 1)) * innerW : innerW / 2),
y: chartTop + innerH - (d.value / niceMax) * innerH
}));
const line = smoothPath(pts);
const areaPath = line + ` L ${pts[pts.length - 1].x.toFixed(1)} ${chartBottom} L ${chartLeft} ${chartBottom} Z`;
const xLabels = [];
const numLabels = 8;
const labelStep = Math.max(1, Math.floor(data.length / (numLabels - 1)));
let lastYear = null;
for (let labelNum = 0; labelNum < numLabels - 1; labelNum++) {
const i = Math.min(labelNum * labelStep, data.length - 2);
const x = pts[i].x;
const d = new Date(data[i].date);
const year = d.getFullYear();
const month = d.toLocaleDateString('en-US', { month: 'short' });
let label;
if (year !== lastYear && lastYear !== null) {
label = String(year);
} else {
label = month;
}
lastYear = year;
xLabels.push({ x, label });
}
xLabels.push({ x: pts[data.length - 1].x, label: 'Today' });
return `
<clipPath id="${clipId}">
<rect x="${chartLeft}" y="${chartTop}" width="${innerW}" height="${innerH + 1}"/>
</clipPath>
<rect x="${cardX}" y="${cardY}" width="${cardWidth}" height="${cardHeight}" fill="#111111" rx="12"/>
<g transform="translate(${cardX + 12}, ${cardY + 10}) scale(0.6)">
${ASSISTANT_UI_LOGO}
</g>
<text x="${cardX + 38}" y="${cardY + 24}" fill="${COLORS.text}" font-family="system-ui,-apple-system,sans-serif" font-size="14" font-weight="600">${metricName}</text>
<text x="${cardX + cardWidth - chartPadding.right}" y="${cardY + 26}" fill="${COLORS.text}" font-family="system-ui,sans-serif" font-size="13" font-weight="600" text-anchor="end">${subtitle}</text>
${subtitle2 ? `<text x="${cardX + cardWidth - chartPadding.right}" y="${cardY + 40}" fill="${COLORS.textMuted}" font-family="system-ui,sans-serif" font-size="10" text-anchor="end">${subtitle2}</text>` : ''}
<rect x="${chartLeft + 4}" y="${chartTop + 4}" width="${packageName.length * 6 + 32}" height="20" fill="#222222" rx="4" opacity="0.9"/>
<g transform="translate(${chartLeft + 8}, ${chartTop + 6}) scale(0.65)">
${logoSvg}
</g>
<text x="${chartLeft + 28}" y="${chartTop + 18}" fill="${COLORS.textMuted}" font-family="system-ui,-apple-system,sans-serif" font-size="10">${packageName}</text>
${yVals.slice(1).map(v => {
const y = chartTop + innerH - (v / niceMax) * innerH;
return `<line x1="${chartLeft}" y1="${y.toFixed(1)}" x2="${chartRight}" y2="${y.toFixed(1)}" stroke="${COLORS.grid}" stroke-width="1" stroke-dasharray="2,4"/>`;
}).join('')}
<g clip-path="url(#${clipId})">
<path d="${areaPath}" fill="url(#${gradId})"/>
<path d="${line}" fill="none" stroke="${COLORS.primary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.3" filter="blur(3px)"/>
<path d="${line}" fill="none" stroke="${COLORS.primary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
${yVals.map(v => {
const y = chartTop + innerH - (v / niceMax) * innerH;
return `<text x="${chartLeft - 6}" y="${(y + 3).toFixed(1)}" fill="${COLORS.textMuted}" font-family="system-ui,sans-serif" font-size="9" text-anchor="end">${formatNumber(v)}</text>`;
}).join('')}
${yAxisLabel ? `<text x="${chartLeft - 35}" y="${chartTop + innerH / 2}" fill="${COLORS.textMuted}" font-family="system-ui,-apple-system,sans-serif" font-size="11" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90, ${chartLeft - 35}, ${chartTop + innerH / 2})">${yAxisLabel}</text>` : ''}
${xLabels.map(({ x, label }) => {
return `<text x="${x.toFixed(1)}" y="${cardY + cardHeight - 10}" fill="${COLORS.textMuted}" font-family="system-ui,sans-serif" font-size="9" text-anchor="middle">${label}</text>`;
}).join('')}
`;
}
const cardY = margin;
const card1X = margin;
const card2X = margin + cardWidth + gap;
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="${COLORS.primary}" stop-opacity="0.2"/>
<stop offset="50%" stop-color="${COLORS.primary}" stop-opacity="0.08"/>
<stop offset="100%" stop-color="${COLORS.primary}" stop-opacity="0"/>
</linearGradient>
</defs>
${chartCard(starsData, card1X, cardY, 'assistant-ui/assistant-ui', ' GitHub Stars', formatNumber(currentStars) + ' total', 'grad', 'clip1', GITHUB_LOGO, null, 'Total')}
${chartCard(downloadsData, card2X, cardY, '@assistant-ui/react', ' npm Downloads', formatNumber(monthlyDownloads) + '/month', 'grad', 'clip2', NPM_LOGO, formatNumber(totalDownloads) + ' total', 'Weekly')}
</svg>`;
}
async function main() {
const [dailyData, stars] = await Promise.all([fetchNpmStats(PACKAGE_NAME), fetchCurrentStars(GITHUB_REPO)]);
const monthlyDownloads = getMonthlyRate(dailyData);
const totalDownloads = Object.values(dailyData).reduce((sum, v) => sum + v, 0);
const downloadsData = smoothData(aggregateByWeek(dailyData)).slice(0, -1);
const currentStars = stars || 0;
const starsData = currentStars ? await fetchStarHistory(GITHUB_REPO, currentStars) : [];
const svg = generateDualSVG(starsData, downloadsData, monthlyDownloads, currentStars, totalDownloads);
const png = await sharp(Buffer.from(svg), { density: 300 }).png().toBuffer();
fs.mkdirSync('.github/assets', { recursive: true });
fs.writeFileSync(OUTPUT_PATH, png);
console.log('Done');
}
main().catch(err => { console.error(err.message); process.exit(1); });
SCRIPT
- name: Commit and push changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .github/assets/traction.png
if git diff --staged --quiet; then
echo "No changes to commit"
exit 0
fi
git checkout -B update-traction-chart
git commit -m "Update traction chart [skip ci]"
git push -f origin update-traction-chart
- name: Create or update Pull Request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PREVIEW_URL="https://raw.githubusercontent.com/${{ github.repository }}/update-traction-chart/.github/assets/traction.png"
BODY="Automated weekly update of README traction chart.
![Traction Chart](${PREVIEW_URL})"
existing_pr=$(gh pr list --head update-traction-chart --json number --jq '.[0].number' || echo "")
if [ -n "$existing_pr" ]; then
gh pr edit "$existing_pr" --body "$BODY"
echo "Updated PR #$existing_pr"
else
gh pr create \
--title "Update README traction chart" \
--body "$BODY" \
--head update-traction-chart \
--base main
fi