Release #43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |