Skip to content

Release

Release #43

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
target:
description: 'What to publish'
type: choice
options:
- extension
- mcp-server
- lsp-server
- both
default: extension
bump:
description: 'Version bump type'
type: choice
options:
- patch
- minor
- major
default: patch
dry_run:
description: 'Dry run — build & test only, skip publish/commit/tag'
type: boolean
default: false
permissions:
contents: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
# ── Setup ──────────────────────────────────────────────────
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- run: pnpm install -wD @vscode/vsce ovsx
# ── Compute versions ───────────────────────────────────────
# For each target, we check both package.json AND the registry.
# We bump from whichever is higher, so we never collide with
# an already-published version.
- name: Bump extension version
id: ext_version
if: inputs.target == 'extension' || inputs.target == 'both'
run: |
PKG="packages/extension/package.json"
LOCAL=$(node -p "require('./$PKG').version")
# Query VS Code Marketplace for the latest published version
PUBLISHED=$(npx vsce show Nskha.airtable-formula --json 2>/dev/null \
| node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{console.log(JSON.parse(d).versions[0].version)}
catch{console.log('0.0.0')}
})" || echo "0.0.0")
echo "Local: ${LOCAL}, Marketplace: ${PUBLISHED}"
# Pick the higher version as the base for bumping
BASE=$(node -e "
const a = '${LOCAL}'.split('.').map(Number);
const b = '${PUBLISHED}'.split('.').map(Number);
const cmp = a[0]-b[0] || a[1]-b[1] || a[2]-b[2];
console.log(cmp >= 0 ? '${LOCAL}' : '${PUBLISHED}');
")
NEXT=$(node -e "
const [major, minor, patch] = '${BASE}'.split('.').map(Number);
const bump = '${{ inputs.bump }}';
if (bump === 'major') console.log((major+1)+'.0.0');
else if (bump === 'minor') console.log(major+'.'+(minor+1)+'.0');
else console.log(major+'.'+minor+'.'+(patch+1));
")
echo "current=${BASE}" >> $GITHUB_OUTPUT
echo "next=${NEXT}" >> $GITHUB_OUTPUT
# Write new version to package.json
node -e "
const fs = require('fs');
const p = '$PKG';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.version = '${NEXT}';
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
"
echo "Extension: ${BASE} → ${NEXT}"
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
- name: Bump MCP server version
id: mcp_version
if: inputs.target == 'mcp-server' || inputs.target == 'both'
run: |
PKG="packages/mcp-server/package.json"
LOCAL=$(node -p "require('./$PKG').version")
# Query npm for the latest published version
PUBLISHED=$(npm view airtable-user-mcp version 2>/dev/null || echo "0.0.0")
echo "Local: ${LOCAL}, npm: ${PUBLISHED}"
# Pick the higher version as the base for bumping
BASE=$(node -e "
const a = '${LOCAL}'.split('.').map(Number);
const b = '${PUBLISHED}'.split('.').map(Number);
const cmp = a[0]-b[0] || a[1]-b[1] || a[2]-b[2];
console.log(cmp >= 0 ? '${LOCAL}' : '${PUBLISHED}');
")
NEXT=$(node -e "
const [major, minor, patch] = '${BASE}'.split('.').map(Number);
const bump = '${{ inputs.bump }}';
if (bump === 'major') console.log((major+1)+'.0.0');
else if (bump === 'minor') console.log(major+'.'+(minor+1)+'.0');
else console.log(major+'.'+minor+'.'+(patch+1));
")
echo "current=${BASE}" >> $GITHUB_OUTPUT
echo "next=${NEXT}" >> $GITHUB_OUTPUT
# Write new version to package.json
node -e "
const fs = require('fs');
const p = '$PKG';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.version = '${NEXT}';
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
"
echo "MCP server: ${BASE} → ${NEXT}"
- name: Bump LSP server version
id: lsp_version
if: inputs.target == 'lsp-server' || inputs.target == 'both'
run: |
PKG="packages/lsp-server/package.json"
LOCAL=$(node -p "require('./$PKG').version")
# Query npm for the latest published version
PUBLISHED=$(npm view airtable-user-lsp version 2>/dev/null || echo "0.0.0")
echo "Local: ${LOCAL}, npm: ${PUBLISHED}"
# Pick the higher version as the base for bumping
BASE=$(node -e "
const a = '${LOCAL}'.split('.').map(Number);
const b = '${PUBLISHED}'.split('.').map(Number);
const cmp = a[0]-b[0] || a[1]-b[1] || a[2]-b[2];
console.log(cmp >= 0 ? '${LOCAL}' : '${PUBLISHED}');
")
NEXT=$(node -e "
const [major, minor, patch] = '${BASE}'.split('.').map(Number);
const bump = '${{ inputs.bump }}';
if (bump === 'major') console.log((major+1)+'.0.0');
else if (bump === 'minor') console.log(major+'.'+(minor+1)+'.0');
else console.log(major+'.'+minor+'.'+(patch+1));
")
echo "current=${BASE}" >> $GITHUB_OUTPUT
echo "next=${NEXT}" >> $GITHUB_OUTPUT
# Write new version to package.json
node -e "
const fs = require('fs');
const p = '$PKG';
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.version = '${NEXT}';
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
"
echo "LSP server: ${BASE} → ${NEXT}"
# ── Build & Test ───────────────────────────────────────────
- name: Build all packages
run: pnpm build
- name: Run tests
run: pnpm test
# ── Package extension VSIX ─────────────────────────────────
- name: Package VSIX
id: vsix
if: inputs.target == 'extension' || inputs.target == 'both'
run: |
# Prepare deps for VSIX
node scripts/prepare-package-deps.mjs
# Copy README: strip SVGs + collapse <picture> to single <img> for VS Code Marketplace
node -e "
const fs = require('fs');
let readme = fs.readFileSync('README.md', 'utf8');
// Replace extension icon SVG with PNG
readme = readme.replace(/<img src=\"[^\"]*airtable\\.svg\"[^/]*\\/>/,
'<img src=\"https://raw.githubusercontent.com/Automations-Project/VSCode-Airtable-Formula/main/packages/extension/images/icon.png\" alt=\"Airtable Formula\" width=\"80\" />');
// Replace claude SVG row in table
readme = readme.replace(/\| <img src=\"[^\"]*claude\\.svg\"[^|]*\|[^\n]*\n/,
'| Claude Desktop | Claude Code | Cursor | Windsurf | Cline | Amp |\n');
// Remove mcp SVG icons
readme = readme.replace(/<img src=\"[^\"]*mcp\\.svg\"[^/]*\\/>/g, '');
// Collapse all <picture> blocks to plain <img> — use the light PNG for the marketplace (white bg)
// Strip -light/-dark variant suffix before swapping .svg → .png (only banner.png / architecture.png exist)
readme = readme.replace(/<picture>[\s\S]*?<img src=\"([^\"]+)\"([^/]*)\/>[\s\S]*?<\/picture>/g,
(_, src, attrs) => '<img src=\"' + src.replace(/-(light|dark)\.svg$/, '.png').replace(/\.svg$/, '.png') + '\"' + attrs + '/>');
fs.writeFileSync('packages/extension/README.md', readme);
"
# Package
cd packages/extension
npx vsce package --no-dependencies
VSIX=$(ls *.vsix | head -1)
echo "file=packages/extension/${VSIX}" >> $GITHUB_OUTPUT
echo "Packaged: ${VSIX}"
# ── Publish extension ──────────────────────────────────────
- name: Publish extension to VS Code Marketplace
if: |
!inputs.dry_run &&
(inputs.target == 'extension' || inputs.target == 'both')
run: npx vsce publish --packagePath "${{ steps.vsix.outputs.file }}"
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
- name: Publish extension to Open VSX
if: |
!inputs.dry_run &&
(inputs.target == 'extension' || inputs.target == 'both')
run: |
npx ovsx publish "${{ steps.vsix.outputs.file }}" --pat $OVSX_PAT
# Verify the version actually landed (Open VSX indexes asynchronously)
VERSION="${{ steps.ext_version.outputs.next }}"
echo "Verifying Open VSX indexed v${VERSION}..."
for i in $(seq 1 18); do
FOUND=$(curl -s "https://open-vsx.org/api/Nskha/airtable-formula/${VERSION}" \
| node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
try{const p=JSON.parse(d);console.log(p.version||'not-found')}
catch{console.log('error')}
})")
if [[ "$FOUND" == "$VERSION" ]]; then
echo "✓ Open VSX confirmed: v${VERSION}"
break
fi
echo " not indexed yet (attempt $i/18), waiting 10s..."
sleep 10
done
if [[ "$FOUND" != "$VERSION" ]]; then
echo "::warning::Open VSX did not index v${VERSION} within 3 minutes — indexing may still be in progress (publish succeeded)"
fi
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
# ── Publish MCP server ─────────────────────────────────────
- name: Publish MCP server to npm
if: |
!inputs.dry_run &&
(inputs.target == 'mcp-server' || inputs.target == 'both')
run: |
npm config set //registry.npmjs.org/:_authToken "${NPM_TOKEN}"
npm whoami && echo "npm auth OK" || echo "WARNING: npm whoami failed — verify NPM_TOKEN secret"
cd packages/mcp-server
npm publish --provenance --access public
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# ── Publish LSP server ─────────────────────────────────────
- name: Publish LSP server to npm
if: |
!inputs.dry_run &&
(inputs.target == 'lsp-server' || inputs.target == 'both')
run: |
npm config set //registry.npmjs.org/:_authToken "${NPM_TOKEN}"
cd packages/lsp-server
npm publish --provenance --access public
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# ── Commit, tag, push ──────────────────────────────────────
- name: Commit version bump and create tags
if: "!inputs.dry_run"
run: |
TARGETS=""
TAGS=""
# Stage modified package.json files
if [[ "${{ inputs.target }}" == "extension" || "${{ inputs.target }}" == "both" ]]; then
git add packages/extension/package.json
TARGETS="${TARGETS} extension v${{ steps.ext_version.outputs.next }}"
TAGS="${TAGS} extension/v${{ steps.ext_version.outputs.next }}"
fi
if [[ "${{ inputs.target }}" == "mcp-server" || "${{ inputs.target }}" == "both" ]]; then
git add packages/mcp-server/package.json
TARGETS="${TARGETS} mcp-server v${{ steps.mcp_version.outputs.next }}"
TAGS="${TAGS} mcp-server/v${{ steps.mcp_version.outputs.next }}"
fi
if [[ "${{ inputs.target }}" == "lsp-server" || "${{ inputs.target }}" == "both" ]]; then
git add packages/lsp-server/package.json
TARGETS="${TARGETS} lsp-server v${{ steps.lsp_version.outputs.next }}"
TAGS="${TAGS} lsp-server/v${{ steps.lsp_version.outputs.next }}"
fi
# Commit
git commit -m "release:${TARGETS}"
# Create tags
for TAG in $TAGS; do
git tag "$TAG"
done
# Push commit and tags
git push origin main --tags
# ── GitHub Releases ────────────────────────────────────────
- name: Create extension GitHub Release
if: |
!inputs.dry_run &&
(inputs.target == 'extension' || inputs.target == 'both')
run: |
gh release create "extension/v${{ steps.ext_version.outputs.next }}" \
"${{ steps.vsix.outputs.file }}" \
--title "Extension v${{ steps.ext_version.outputs.next }}" \
--generate-notes \
--target main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create MCP server GitHub Release
if: |
!inputs.dry_run &&
(inputs.target == 'mcp-server' || inputs.target == 'both')
run: |
gh release create "mcp-server/v${{ steps.mcp_version.outputs.next }}" \
--title "MCP Server v${{ steps.mcp_version.outputs.next }}" \
--generate-notes \
--target main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create LSP server GitHub Release
if: |
!inputs.dry_run &&
(inputs.target == 'lsp-server' || inputs.target == 'both')
run: |
gh release create "lsp-server/v${{ steps.lsp_version.outputs.next }}" \
--title "LSP Server v${{ steps.lsp_version.outputs.next }}" \
--generate-notes \
--target main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── Summary ────────────────────────────────────────────────
- name: Summary
if: always()
run: |
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ inputs.dry_run }}" == "true" ]]; then
echo "**DRY RUN — nothing published**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ inputs.target }}" == "extension" || "${{ inputs.target }}" == "both" ]]; then
echo "- Extension: ${{ steps.ext_version.outputs.current }} → **${{ steps.ext_version.outputs.next }}**" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ inputs.target }}" == "mcp-server" || "${{ inputs.target }}" == "both" ]]; then
echo "- MCP Server: ${{ steps.mcp_version.outputs.current }} → **${{ steps.mcp_version.outputs.next }}**" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ inputs.target }}" == "lsp-server" || "${{ inputs.target }}" == "both" ]]; then
echo "- LSP Server: ${{ steps.lsp_version.outputs.current }} → **${{ steps.lsp_version.outputs.next }}**" >> $GITHUB_STEP_SUMMARY
fi