-
Notifications
You must be signed in to change notification settings - Fork 892
Publish eclipse #7590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Publish eclipse #7590
Changes from all commits
7506bd3
e13fa29
292f85d
dbc1fc6
7f5907a
efbb8a9
d2152c3
4bd252d
680fa9e
242bc14
7fda4ea
dfb32d7
69aef89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
|
|
||
| permissions: | ||
| contents: write | ||
| id-token: write | ||
|
|
||
| concurrency: | ||
| group: release-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| release: | ||
| name: Release to npm | ||
| if: "startsWith(github.event.head_commit.message, 'chore(eclipse): release ')" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Extract version from commit message | ||
| id: version | ||
| run: | | ||
| VERSION=$(echo "${{ github.event.head_commit.message }}" | sed -E 's/^chore\(eclipse\): release ([0-9]+\.[0-9]+\.[0-9]+).*/\1/') | ||
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | ||
| echo "Releasing version: $VERSION" | ||
|
|
||
| - name: Validate version format | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "Error: Invalid version format '$VERSION'. Expected x.y.z" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Verify package version matches commit | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| PACKAGE_VERSION=$(node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('packages/eclipse/package.json','utf8'));process.stdout.write(p.version)") | ||
| if [[ "$PACKAGE_VERSION" != "$VERSION" ]]; then | ||
| echo "Error: packages/eclipse/package.json version is '$PACKAGE_VERSION' but commit version is '$VERSION'" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Check if version already exists on npm | ||
| id: check-version | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| if npm view @prisma/eclipse@$VERSION version >/dev/null 2>&1; then | ||
| echo "Version $VERSION already exists on npm" | ||
| echo "exists=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "Version $VERSION is new" | ||
| echo "exists=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Setup pnpm | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| uses: pnpm/action-setup@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "20" | ||
| cache: "pnpm" | ||
| registry-url: "https://registry.npmjs.org" | ||
|
|
||
| - name: Ensure npm version for trusted publishing | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| run: | | ||
| npm install -g npm@11.5.1 | ||
| echo "Node: $(node --version)" | ||
| echo "npm: $(npm --version)" | ||
|
|
||
| - name: Install dependencies | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Typecheck | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| run: pnpm --filter @prisma/eclipse run types:check | ||
|
|
||
| - name: Verify publish dry run | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| working-directory: packages/eclipse | ||
| run: npm publish --dry-run --access public | ||
|
|
||
| - name: Publish to npm | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| working-directory: packages/eclipse | ||
| run: npm publish --access public | ||
|
|
||
| - name: Create and push git tag | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| TAG="eclipse-v$VERSION" | ||
| if ! git rev-parse "$TAG" >/dev/null 2>&1; then | ||
| git tag "$TAG" | ||
| git push origin "$TAG" | ||
| else | ||
| echo "Tag $TAG already exists, skipping" | ||
| fi | ||
|
|
||
| - name: Create GitHub Release | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| TAG="eclipse-v$VERSION" | ||
| if ! gh release view "$TAG" >/dev/null 2>&1; then | ||
| gh release create "$TAG" --title "@prisma/eclipse v$VERSION" --generate-notes | ||
| else | ||
| echo "Release $TAG already exists, skipping" | ||
| fi | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Release summary | ||
| if: steps.check-version.outputs.exists == 'false' | ||
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| TAG="eclipse-v$VERSION" | ||
| echo "## Release @prisma/eclipse v$VERSION complete" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "- Tag: $TAG" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "- GitHub release: https://github.com/${{ github.repository }}/releases/tag/$TAG" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "- https://www.npmjs.com/package/@prisma/eclipse/v/$VERSION" >> "$GITHUB_STEP_SUMMARY" | ||
| Original file line number | Diff line number | Diff line change | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,16 @@ | |||||||||||||||||
| import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread'; | |||||||||||||||||
| import Mixedbread from '@mixedbread/sdk'; | |||||||||||||||||
| import { SortedResult } from 'fumadocs-core/search'; | |||||||||||||||||
| import { createMixedbreadSearchAPI } from "fumadocs-core/search/mixedbread"; | |||||||||||||||||
| import Mixedbread from "@mixedbread/sdk"; | |||||||||||||||||
| import { SortedResult } from "fumadocs-core/search"; | |||||||||||||||||
|
|
|||||||||||||||||
| export const dynamic = "force-dynamic"; | |||||||||||||||||
|
|
|||||||||||||||||
| /** Derive breadcrumbs from URL path segments (e.g. /docs/console/concepts → ['Docs', 'Console']) */ | |||||||||||||||||
| function getBreadcrumbsFromUrl(url: string): string[] { | |||||||||||||||||
| const path = url.replace(/#.*$/, '').trim().replace(/\/$/, '') || '/'; | |||||||||||||||||
| const segments = path.split('/').filter(Boolean); | |||||||||||||||||
| const path = url.replace(/#.*$/, "").trim().replace(/\/$/, "") || "/"; | |||||||||||||||||
| const segments = path.split("/").filter(Boolean); | |||||||||||||||||
| if (segments.length === 0) return []; | |||||||||||||||||
| // Strip version prefix (e.g. v6) | |||||||||||||||||
| const normalized = segments[0] === 'v6' ? segments.slice(1) : segments; | |||||||||||||||||
| const normalized = segments[0] === "v6" ? segments.slice(1) : segments; | |||||||||||||||||
| if (normalized.length === 0) return []; | |||||||||||||||||
| // Ancestors only (exclude last = current page), or full path for section roots | |||||||||||||||||
| const breadcrumbSegments = | |||||||||||||||||
|
|
@@ -17,82 +19,88 @@ function getBreadcrumbsFromUrl(url: string): string[] { | ||||||||||||||||
| s | |||||||||||||||||
| .split(/[-_]/) | |||||||||||||||||
| .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) | |||||||||||||||||
| .join(' ') | |||||||||||||||||
| .join(" "), | |||||||||||||||||
| ); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| function slugger(value: string): string { | |||||||||||||||||
| if (typeof value !== 'string') return ''; | |||||||||||||||||
| if (typeof value !== "string") return ""; | |||||||||||||||||
| return value | |||||||||||||||||
| .toLowerCase() | |||||||||||||||||
| .trim() | |||||||||||||||||
| .replace(/[^a-z0-9_\s-]/g, '') | |||||||||||||||||
| .replace(/\s+/g, '-'); | |||||||||||||||||
| .replace(/[^a-z0-9_\s-]/g, "") | |||||||||||||||||
| .replace(/\s+/g, "-"); | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| function removeMd(md: string): string { | |||||||||||||||||
| if (typeof md !== 'string') return ''; | |||||||||||||||||
| if (typeof md !== "string") return ""; | |||||||||||||||||
| try { | |||||||||||||||||
| return md | |||||||||||||||||
| .replace( | |||||||||||||||||
| /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/gm, | |||||||||||||||||
| '', | |||||||||||||||||
| "", | |||||||||||||||||
| ) | |||||||||||||||||
| .replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, '$1') | |||||||||||||||||
| .replace(/\n={2,}/g, '\n') | |||||||||||||||||
| .replace(/^[=\-]{2,}\s*$/gm, '') | |||||||||||||||||
| .replace(/~{3}.*\n/g, '') | |||||||||||||||||
| .replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, "$1") | |||||||||||||||||
| .replace(/\n={2,}/g, "\n") | |||||||||||||||||
| .replace(/^[=\-]{2,}\s*$/gm, "") | |||||||||||||||||
| .replace(/~{3}.*\n/g, "") | |||||||||||||||||
| .replace(/```[^\n]*\n([\s\S]*?)```/g, (_: string, c: string) => c.trim()) | |||||||||||||||||
| .replace(/~~/g, '') | |||||||||||||||||
| .replace(/<[^>]*>/g, '') | |||||||||||||||||
| .replace(/\[\^.+?\](\: .*?$)?/g, '') | |||||||||||||||||
| .replace(/\s{0,2}\[.*?\]: .*?$/g, '') | |||||||||||||||||
| .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/gm, '') | |||||||||||||||||
| .replace(/!\[(.*?)\][\[\(].*?[\]\)]/g, '') | |||||||||||||||||
| .replace(/\[([\s\S]*?)\]\s*[\(\[].*?[\)\]]/g, '$1') | |||||||||||||||||
| .replace(/^(\n)?\s{0,3}>\s?/gm, '$1') | |||||||||||||||||
| .replace(/~~/g, "") | |||||||||||||||||
| .replace(/<[^>]*>/g, "") | |||||||||||||||||
| .replace(/\[\^.+?\](\: .*?$)?/g, "") | |||||||||||||||||
| .replace(/\s{0,2}\[.*?\]: .*?$/g, "") | |||||||||||||||||
| .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/gm, "") | |||||||||||||||||
| .replace(/!\[(.*?)\][\[\(].*?[\]\)]/g, "") | |||||||||||||||||
| .replace(/\[([\s\S]*?)\]\s*[\(\[].*?[\)\]]/g, "$1") | |||||||||||||||||
| .replace(/^(\n)?\s{0,3}>\s?/gm, "$1") | |||||||||||||||||
| .replace( | |||||||||||||||||
| /^(\n)?\s{0,}#{1,6}\s*( (.+))? +#+$|^(\n)?\s{0,}#{1,6}\s*( (.+))?$/gm, | |||||||||||||||||
| '$1$3$4$6', | |||||||||||||||||
| "$1$3$4$6", | |||||||||||||||||
| ) | |||||||||||||||||
| .replace(/([\*]+)(\S)(.*?\S)??\1/g, '$2$3') | |||||||||||||||||
| .replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g, '$1$3$4$5') | |||||||||||||||||
| .replace(/(`{3,})(.*?)\1/gm, '$2') | |||||||||||||||||
| .replace(/`(.+?)`/g, '$1') | |||||||||||||||||
| .replace(/~(.*?)~/g, '$1'); | |||||||||||||||||
| .replace(/([\*]+)(\S)(.*?\S)??\1/g, "$2$3") | |||||||||||||||||
| .replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g, "$1$3$4$5") | |||||||||||||||||
| .replace(/(`{3,})(.*?)\1/gm, "$2") | |||||||||||||||||
| .replace(/`(.+?)`/g, "$1") | |||||||||||||||||
| .replace(/~(.*?)~/g, "$1"); | |||||||||||||||||
| } catch { | |||||||||||||||||
| return md; | |||||||||||||||||
| } | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| function extractHeadingTitle(text: string): string { | |||||||||||||||||
| const t = text.trim(); | |||||||||||||||||
| return t.startsWith('#') ? removeMd(t.split('\n')[0]?.trim() ?? '') : ''; | |||||||||||||||||
| return t.startsWith("#") ? removeMd(t.split("\n")[0]?.trim() ?? "") : ""; | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| const client = new Mixedbread({ apiKey: process.env.MIXEDBREAD_API_KEY! }); | |||||||||||||||||
|
|
|||||||||||||||||
| export const { GET } = createMixedbreadSearchAPI({ | |||||||||||||||||
| client, | |||||||||||||||||
| storeIdentifier: 'web-search', | |||||||||||||||||
| storeIdentifier: "web-search", | |||||||||||||||||
| topK: 20, | |||||||||||||||||
| rerank: true, | |||||||||||||||||
| transform: (results, _query) => { | |||||||||||||||||
| return results.flatMap((item) => { | |||||||||||||||||
| const { url = '#', title = 'Untitled' } = item.generated_metadata ?? {}; | |||||||||||||||||
| const { url = "#", title = "Untitled" } = item.generated_metadata ?? {}; | |||||||||||||||||
|
|
|||||||||||||||||
| const formattedUrl = url.startsWith("/docs") ? url.slice(5) : url; | |||||||||||||||||
| const base = `${item.file_id}-${item.chunk_index}`; | |||||||||||||||||
| const breadcrumbs = getBreadcrumbsFromUrl(formattedUrl); | |||||||||||||||||
| const chunkResults: SortedResult[] = [ | |||||||||||||||||
| { id: `${base}-page`, type: 'page', content: title, url: formattedUrl, breadcrumbs }, | |||||||||||||||||
| { | |||||||||||||||||
| id: `${base}-page`, | |||||||||||||||||
| type: "page", | |||||||||||||||||
| content: title, | |||||||||||||||||
| url: formattedUrl, | |||||||||||||||||
| breadcrumbs, | |||||||||||||||||
| }, | |||||||||||||||||
|
Comment on lines
+84
to
+96
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle the
Proposed fix- const formattedUrl = url.startsWith("/docs") ? url.slice(5) : url;
+ const formattedUrl =
+ url === "/docs" || url === "/docs/"
+ ? "/"
+ : url.startsWith("/docs/")
+ ? url.slice(5)
+ : url;🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify each finding against the current code and only fix it if needed. In
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
const formattedUrl = url.startsWith("/docs") ? url.slice(5) : url;has two concrete issues:
Here is the corrected logic: const formattedUrl =
url === "/docs" || url === "/docs/"
? "/"
: url.startsWith("/docs/")
? url.slice(5)
: url;Why each case is handled correctly:
The key insight is gating the |
|||||||||||||||||
| ]; | |||||||||||||||||
| const heading = | |||||||||||||||||
| item.type === 'text' ? extractHeadingTitle(item.text) : ''; | |||||||||||||||||
| item.type === "text" ? extractHeadingTitle(item.text) : ""; | |||||||||||||||||
| if (heading) | |||||||||||||||||
| chunkResults.push({ | |||||||||||||||||
| id: `${base}-heading`, | |||||||||||||||||
| type: 'heading', | |||||||||||||||||
| type: "heading", | |||||||||||||||||
| content: heading, | |||||||||||||||||
| url: `${formattedUrl}#${slugger(heading)}`, | |||||||||||||||||
| }); | |||||||||||||||||
|
|
|||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script injection vulnerability via untrusted commit message.
The commit message is interpolated directly into the shell script using
${{ github.event.head_commit.message }}. Since commit messages are user-controlled, a malicious actor with push access could craft a commit message containing shell metacharacters to execute arbitrary commands in the workflow context.For example, a commit message like:
would execute the injected command.
The fix is to pass the value through an environment variable, which prevents shell interpretation of the content.
🔧 Proposed fix using environment variable
- name: Extract version from commit message id: version + env: + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | - VERSION=$(echo "${{ github.event.head_commit.message }}" | sed -E 's/^chore\(eclipse\): release ([0-9]+\.[0-9]+\.[0-9]+).*/\1/') + VERSION=$(echo "$COMMIT_MESSAGE" | sed -E 's/^chore\(eclipse\): release ([0-9]+\.[0-9]+\.[0-9]+).*/\1/') echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Releasing version: $VERSION"📝 Committable suggestion
🧰 Tools
🪛 actionlint (1.7.11)
[error] 30-30: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details
(expression)
🤖 Prompt for AI Agents