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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 271 KiB |
@@ -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."
|
||||
@@ -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(", ")}`);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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")"
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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}."
|
||||
@@ -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}."
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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.
|
||||
"
|
||||
|
||||
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
|
||||