diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index 597ad4d3a7..9ec6ede1a6 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -26,8 +26,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml
index 93e20e7861..4f9d810bf3 100644
--- a/.github/workflows/chromatic.yml
+++ b/.github/workflows/chromatic.yml
@@ -32,8 +32,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 98aa0b9c04..aabce74779 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
+ - release
push:
branches:
- main
@@ -33,8 +34,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies (root only, no scripts)
run: pnpm install --filter . --ignore-scripts
@@ -55,8 +54,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
@@ -77,8 +74,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
@@ -105,8 +100,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
@@ -144,8 +137,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
@@ -174,8 +165,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
@@ -202,8 +191,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
@@ -227,8 +214,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies (root only, no scripts)
run: pnpm install --filter . --ignore-scripts
diff --git a/.github/workflows/lunaria.yml b/.github/workflows/lunaria.yml
index 0ecb5af4f7..1c4238b221 100644
--- a/.github/workflows/lunaria.yml
+++ b/.github/workflows/lunaria.yml
@@ -34,8 +34,6 @@ jobs:
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
name: 🟧 Install pnpm
- with:
- cache: true
- name: 📦 Install dependencies
run: pnpm install
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
new file mode 100644
index 0000000000..09b9ec45ed
--- /dev/null
+++ b/.github/workflows/release-pr.yml
@@ -0,0 +1,152 @@
+name: release-pr
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ release-pr:
+ name: 🚀 Create or update release PR
+ runs-on: ubuntu-slim
+ if: github.repository == 'npmx-dev/npmx.dev'
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: 🔍 Check for unreleased commits
+ id: check
+ run: |
+ git fetch origin release
+ COMMITS=$(git log origin/release..origin/main --oneline)
+ if [ -z "$COMMITS" ]; then
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ echo "No new commits to release"
+ else
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "Found unreleased commits:"
+ echo "$COMMITS"
+ fi
+
+ - name: 📝 Generate changelog body
+ if: steps.check.outputs.skip == 'false'
+ id: changelog
+ run: |
+ # Get the latest tag, or use initial commit if no tags exist
+ LATEST_TAG=$(git describe --tags --abbrev=0 origin/release 2>/dev/null || echo "")
+
+ if [ -z "$LATEST_TAG" ]; then
+ FROM_REF=$(git rev-list --max-parents=0 HEAD)
+ CURRENT_VERSION="0.0.0"
+ else
+ FROM_REF="$LATEST_TAG"
+ CURRENT_VERSION="${LATEST_TAG#v}"
+ fi
+
+ # Categorize commits
+ FEATURES=""
+ FIXES=""
+ CHORES=""
+ OTHER=""
+
+ while IFS= read -r line; do
+ [ -z "$line" ] && continue
+ SHA=$(echo "$line" | cut -d' ' -f1)
+ MSG=$(echo "$line" | cut -d' ' -f2-)
+ ENTRY="- $MSG (\`$SHA\`)"
+
+ if echo "$MSG" | grep -qE '^feat(\(|:)'; then
+ FEATURES="${FEATURES}${ENTRY}\n"
+ elif echo "$MSG" | grep -qE '^fix(\(|:)'; then
+ FIXES="${FIXES}${ENTRY}\n"
+ elif echo "$MSG" | grep -qE '^(chore|ci|build|perf|refactor|style|test|docs)(\(|:)'; then
+ CHORES="${CHORES}${ENTRY}\n"
+ else
+ OTHER="${OTHER}${ENTRY}\n"
+ fi
+ done <<< "$(git log "$FROM_REF"..origin/main --oneline --no-merges)"
+
+ # Determine next version
+ HAS_BREAKING=$(git log "$FROM_REF"..origin/main --format='%B' | grep -c 'BREAKING CHANGE\|!:' || true)
+ HAS_FEAT=$(git log "$FROM_REF"..origin/main --oneline --no-merges | grep -cE '^[a-f0-9]+ feat(\(|:)' || true)
+
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
+
+ if [ "$HAS_BREAKING" -gt 0 ] && [ "$MAJOR" -gt 0 ]; then
+ NEXT_VERSION="$((MAJOR + 1)).0.0"
+ elif [ "$HAS_FEAT" -gt 0 ]; then
+ NEXT_VERSION="${MAJOR}.$((MINOR + 1)).0"
+ else
+ NEXT_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ fi
+
+ echo "next_version=v${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
+
+ # Build the PR body
+ BODY="This PR will deploy the following changes to production (\`npmx.dev\`).\n\n"
+ BODY="${BODY}**Next version: \`v${NEXT_VERSION}\`** (current: \`v${CURRENT_VERSION}\`)\n\n"
+
+ if [ -n "$FEATURES" ]; then
+ BODY="${BODY}### Features\n\n${FEATURES}\n"
+ fi
+ if [ -n "$FIXES" ]; then
+ BODY="${BODY}### Fixes\n\n${FIXES}\n"
+ fi
+ if [ -n "$CHORES" ]; then
+ BODY="${BODY}### Other Changes\n\n${CHORES}\n"
+ fi
+ if [ -n "$OTHER" ]; then
+ BODY="${BODY}### Uncategorized\n\n${OTHER}\n"
+ fi
+
+ BODY="${BODY}---\n\n"
+ BODY="${BODY}> Merging this PR will:\n"
+ BODY="${BODY}> - Deploy to \`npmx.dev\` via Vercel\n"
+ BODY="${BODY}> - Create a \`v${NEXT_VERSION}\` tag and GitHub Release\n"
+ BODY="${BODY}> - Publish \`npmx-connector@${NEXT_VERSION}\` to npm"
+
+ # Write body to file, truncating if needed (GitHub limits PR body to 65536 chars)
+ echo -e "$BODY" > /tmp/pr-body.md
+ if [ "$(wc -c < /tmp/pr-body.md)" -gt 60000 ]; then
+ COMMIT_COUNT=$(git log "$FROM_REF"..origin/main --oneline --no-merges | wc -l)
+ COMPARE_URL="https://github.com/npmx-dev/npmx.dev/compare/${FROM_REF}...main"
+ TRUNCATED="This PR will deploy the following changes to production (\`npmx.dev\`).\n\n"
+ TRUNCATED="${TRUNCATED}**Next version: \`v${NEXT_VERSION}\`** (current: \`v${CURRENT_VERSION}\`)\n\n"
+ TRUNCATED="${TRUNCATED}> **${COMMIT_COUNT} commits** are included in this release. The full changelog is too large to display here.\n>\n"
+ TRUNCATED="${TRUNCATED}> [View full diff on GitHub](${COMPARE_URL})\n\n"
+ TRUNCATED="${TRUNCATED}---\n\n"
+ TRUNCATED="${TRUNCATED}> Merging this PR will:\n"
+ TRUNCATED="${TRUNCATED}> - Deploy to \`npmx.dev\` via Vercel\n"
+ TRUNCATED="${TRUNCATED}> - Create a \`v${NEXT_VERSION}\` tag and GitHub Release\n"
+ TRUNCATED="${TRUNCATED}> - Publish \`npmx-connector@${NEXT_VERSION}\` to npm"
+ echo -e "$TRUNCATED" > /tmp/pr-body.md
+ fi
+
+ - name: 🚀 Create or update release PR
+ if: steps.check.outputs.skip == 'false'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NEXT_VERSION: ${{ steps.changelog.outputs.next_version }}
+ run: |
+ EXISTING_PR=$(gh pr list --base release --head main --state open --json number --jq '.[0].number')
+
+ if [ -n "$EXISTING_PR" ]; then
+ gh pr edit "$EXISTING_PR" \
+ --title "chore: release ${NEXT_VERSION}" \
+ --body-file /tmp/pr-body.md
+ echo "Updated existing PR #${EXISTING_PR}"
+ else
+ gh pr create \
+ --base release \
+ --head main \
+ --title "chore: release ${NEXT_VERSION}" \
+ --body-file /tmp/pr-body.md \
+ --label "release"
+ echo "Created new release PR"
+ fi
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
new file mode 100644
index 0000000000..af7ea9ef3e
--- /dev/null
+++ b/.github/workflows/release-tag.yml
@@ -0,0 +1,158 @@
+name: release-tag
+
+on:
+ push:
+ branches:
+ - release
+
+permissions: {}
+
+jobs:
+ tag:
+ name: 🏷️ Tag release and create GitHub Release
+ runs-on: ubuntu-slim
+ if: github.repository == 'npmx-dev/npmx.dev'
+ permissions:
+ contents: write
+ outputs:
+ version: ${{ steps.version.outputs.next }}
+ skipped: ${{ steps.check.outputs.skip }}
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: 🔢 Determine next version
+ id: version
+ run: |
+ # Get the latest tag on this branch
+ LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
+
+ if [ -z "$LATEST_TAG" ]; then
+ CURRENT_VERSION="0.0.0"
+ FROM_REF=$(git rev-list --max-parents=0 HEAD)
+ else
+ CURRENT_VERSION="${LATEST_TAG#v}"
+ FROM_REF="$LATEST_TAG"
+ fi
+
+ # Analyze conventional commits since last tag
+ HAS_BREAKING=$(git log "${FROM_REF}..HEAD" --format='%B' | grep -c 'BREAKING CHANGE\|!:' || true)
+ HAS_FEAT=$(git log "${FROM_REF}..HEAD" --oneline --no-merges | grep -cE '^[a-f0-9]+ feat(\(|:)' || true)
+ HAS_FIX=$(git log "${FROM_REF}..HEAD" --oneline --no-merges | grep -cE '^[a-f0-9]+ fix(\(|:)' || true)
+
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
+
+ if [ "$HAS_BREAKING" -gt 0 ] && [ "$MAJOR" -gt 0 ]; then
+ NEXT_VERSION="$((MAJOR + 1)).0.0"
+ elif [ "$HAS_FEAT" -gt 0 ]; then
+ NEXT_VERSION="${MAJOR}.$((MINOR + 1)).0"
+ elif [ "$HAS_FIX" -gt 0 ]; then
+ NEXT_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ else
+ # Only chore/docs/ci commits — still bump patch
+ NEXT_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
+ fi
+
+ echo "current=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
+ echo "next=v${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
+ echo "from=$FROM_REF" >> "$GITHUB_OUTPUT"
+ echo "Bumping from v${CURRENT_VERSION} to v${NEXT_VERSION}"
+
+ - name: 🔍 Check if tag already exists
+ id: check
+ env:
+ VERSION: ${{ steps.version.outputs.next }}
+ run: |
+ if git rev-parse "$VERSION" >/dev/null 2>&1; then
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ echo "Tag $VERSION already exists, skipping"
+ else
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: 🏷️ Create and push tag
+ if: steps.check.outputs.skip == 'false'
+ env:
+ VERSION: ${{ steps.version.outputs.next }}
+ run: |
+ git tag -a "$VERSION" -m "Release $VERSION"
+ git push origin "$VERSION"
+
+ - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ if: steps.check.outputs.skip == 'false'
+ with:
+ node-version: lts/*
+
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
+ if: steps.check.outputs.skip == 'false'
+ name: 🟧 Install pnpm
+
+ - name: 📦 Install dependencies
+ if: steps.check.outputs.skip == 'false'
+ run: pnpm install --filter . --ignore-scripts
+
+ - name: 📝 Generate release notes
+ if: steps.check.outputs.skip == 'false'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ FROM_REF: ${{ steps.version.outputs.from }}
+ run: node scripts/release-notes.ts "$FROM_REF" > /tmp/release-notes.md
+
+ - name: 🚀 Create GitHub Release
+ if: steps.check.outputs.skip == 'false'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ steps.version.outputs.next }}
+ run: |
+ gh release create "$VERSION" \
+ --notes-file /tmp/release-notes.md \
+ --title "$VERSION"
+
+ publish-connector:
+ name: 📦 Publish npmx-connector to npm
+ runs-on: ubuntu-slim
+ needs: tag
+ if: needs.tag.outputs.skipped == 'false'
+ permissions:
+ contents: read
+ id-token: write
+ environment: npm-publish
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: release
+
+ - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ with:
+ node-version: lts/*
+ registry-url: https://registry.npmjs.org
+
+ - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # 4e1c8eafbd745f64b1ef30a7d7ed7965034c486c
+ name: 🟧 Install pnpm
+ with:
+ cache: false
+
+ - name: 📦 Install dependencies
+ run: pnpm install --filter npmx-connector...
+
+ - name: 🔢 Set connector version
+ env:
+ VERSION: ${{ needs.tag.outputs.version }}
+ run: |
+ # Strip the 'v' prefix for package.json
+ PKG_VERSION="${VERSION#v}"
+ cd cli
+ npm version "$PKG_VERSION" --no-git-tag-version
+ echo "Publishing npmx-connector@${PKG_VERSION}"
+
+ - name: 🏗️ Build connector
+ run: pnpm --filter npmx-connector build
+
+ - name: 📤 Publish to npm with provenance
+ # Uses OIDC trusted publishing — no NPM_TOKEN needed.
+ # Configure on npmjs.com: repo npmx-dev/npmx.dev, workflow release-tag.yml, environment npm-publish
+ run: npm publish --provenance --access public
+ working-directory: cli
diff --git a/README.md b/README.md
index 1717303dd2..ca49a74f76 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ What npmx offers:
## Shortcuts
- [chat.npmx.dev](https://chat.npmx.dev) - Discord Server
+- [build.npmx.dev](https://build.npmx.dev) - Builders Discord Server
- [social.npmx.dev](https://social.npmx.dev) - Bluesky Profile
- [repo.npmx.dev](https://repo.npmx.dev) - GitHub Repository
- [issues.npmx.dev](https://issues.npmx.dev) - GitHub Issues
diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue
index 49fcdf3a78..d89d3cf3d3 100644
--- a/app/components/AppFooter.vue
+++ b/app/components/AppFooter.vue
@@ -4,6 +4,7 @@ import { NPMX_DOCS_SITE } from '#shared/utils/constants'
const route = useRoute()
const isHome = computed(() => route.name === 'index')
+const discord = useDiscordLink()
const modalRef = useTemplateRef('modalRef')
const showModal = () => modalRef.value?.showModal?.()
const closeModal = () => modalRef.value?.close?.()
@@ -124,8 +125,8 @@ const closeModal = () => modalRef.value?.close?.()
{{ $t('footer.social') }}
-
- {{ $t('footer.chat') }}
+
+ {{ discord.label }}
diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue
index 18801fca35..5febbd6fa7 100644
--- a/app/components/AppHeader.vue
+++ b/app/components/AppHeader.vue
@@ -5,6 +5,7 @@ import { isEditableElement } from '~/utils/input'
import { NPMX_DOCS_SITE } from '#shared/utils/constants'
const keyboardShortcuts = useKeyboardShortcuts()
+const discord = useDiscordLink()
withDefaults(
defineProps<{
@@ -122,8 +123,8 @@ const mobileLinks = computed(() => [
},
{
name: 'Chat',
- label: $t('footer.chat'),
- href: 'https://chat.npmx.dev',
+ label: discord.value.label,
+ href: discord.value.url,
target: '_blank',
type: 'link',
external: true,
diff --git a/app/components/CallToAction.vue b/app/components/CallToAction.vue
index 8ed0f0c158..ba79780c9e 100644
--- a/app/components/CallToAction.vue
+++ b/app/components/CallToAction.vue
@@ -1,4 +1,5 @@