From 1be039a910b4aa90e45364c49903568d5f701a86 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 14:32:32 +0200 Subject: [PATCH 01/19] feat: add secure release pipeline (prepare-release workflows) Add two-stage release pipeline where appkit prepares artifacts and a private secure repo handles npm publishing via OIDC Trusted Publishing. Changes: - Update .release-it.json configs to disable git/github/npm operations, used for version calculation and changelog generation only - Add prepare-release.yml workflow (push to main): builds, packs, and uploads artifacts with SHA256 digests for the secure repo to consume - Add prepare-release-lakebase.yml for independent lakebase releases - Add template dependency pinning lint step to CI (tools/check-template-deps.js) - Remove old release.yml and release-lakebase.yml workflows - Remove unused release and release:ci package.json scripts - Update CLAUDE.md releasing section with new architecture Signed-off-by: Pawel Kosiec --- .github/workflows/ci.yml | 2 + .../workflows/prepare-release-lakebase.yml | 100 ++++++++++++ .github/workflows/prepare-release.yml | 109 ++++++++++++++ .github/workflows/release-lakebase.yml | 78 ---------- .github/workflows/release.yml | 142 ------------------ .release-it.json | 33 ++-- CLAUDE.md | 55 +++---- package.json | 2 - packages/lakebase/.release-it.json | 34 ++--- packages/lakebase/package.json | 2 - tools/check-template-deps.js | 18 +++ 11 files changed, 270 insertions(+), 305 deletions(-) create mode 100644 .github/workflows/prepare-release-lakebase.yml create mode 100644 .github/workflows/prepare-release.yml delete mode 100644 .github/workflows/release-lakebase.yml delete mode 100644 .github/workflows/release.yml create mode 100644 tools/check-template-deps.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 538607b5..299ace32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,8 @@ jobs: run: pnpm run knip - name: Run License Check run: pnpm run check:licenses + - name: Check template deps are pinned + run: node tools/check-template-deps.js test: name: Unit Tests diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml new file mode 100644 index 00000000..ebc6c331 --- /dev/null +++ b/.github/workflows/prepare-release-lakebase.yml @@ -0,0 +1,100 @@ +name: Prepare Release Lakebase + +on: + push: + branches: + - main + paths: + - 'packages/lakebase/**' + +concurrency: + group: prepare-release-lakebase + cancel-in-progress: true + +permissions: + contents: read + id-token: write + +jobs: + prepare: + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Setup JFrog npm + uses: ./.github/actions/setup-jfrog-npm + with: + npmrc-path: .npmrc + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check for releasable commits + id: version + working-directory: packages/lakebase + run: | + VERSION=$(pnpm exec release-it --release-version --ci \ + -c .release-it.prepare.json 2>/dev/null) || true + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Next version: $VERSION" + else + echo "No releasable commits — skipping release preparation" + echo "version=" >> "$GITHUB_OUTPUT" + fi + + - name: Generate changelog + if: steps.version.outputs.version != '' + working-directory: packages/lakebase + run: | + pnpm exec release-it ${{ steps.version.outputs.version }} --ci + - name: Build + if: steps.version.outputs.version != '' + run: pnpm --filter=@databricks/lakebase build:package + + - name: Dist + if: steps.version.outputs.version != '' + run: pnpm --filter=@databricks/lakebase dist + + - name: SBOM + if: steps.version.outputs.version != '' + run: pnpm --filter=@databricks/lakebase release:sbom + + - name: Pack + if: steps.version.outputs.version != '' + run: npm pack packages/lakebase/tmp + + - name: Generate SHA256 + if: steps.version.outputs.version != '' + run: sha256sum *.tgz > SHA256SUMS + + - name: Write version file + if: steps.version.outputs.version != '' + run: echo "${{ steps.version.outputs.version }}" > VERSION + + - name: Upload release artifacts + if: steps.version.outputs.version != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: lakebase-release-${{ github.run_number }} + retention-days: 7 + path: | + *.tgz + packages/lakebase/changelog-diff.md + VERSION + SHA256SUMS diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..7c2edadb --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,109 @@ +name: Prepare Release + +on: + push: + branches: + - main + +concurrency: + group: prepare-release + cancel-in-progress: true + +permissions: + contents: read + id-token: write + +jobs: + prepare: + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Setup JFrog npm + uses: ./.github/actions/setup-jfrog-npm + with: + npmrc-path: .npmrc + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check for releasable commits + id: version + run: | + VERSION=$(pnpm exec release-it --release-version --ci \ + -c .release-it.prepare.json 2>/dev/null) || true + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Next version: $VERSION" + else + echo "No releasable commits — skipping release preparation" + echo "version=" >> "$GITHUB_OUTPUT" + fi + + - name: Generate changelog + if: steps.version.outputs.version != '' + run: | + pnpm exec release-it ${{ steps.version.outputs.version }} --ci + - name: Sync versions + if: steps.version.outputs.version != '' + run: tsx tools/sync-versions.ts "${{ steps.version.outputs.version }}" + + - name: Build + if: steps.version.outputs.version != '' + run: pnpm build && pnpm --filter=docs build + + - name: Dist + if: steps.version.outputs.version != '' + run: | + pnpm --filter=@databricks/appkit dist + pnpm --filter=@databricks/appkit-ui dist + + - name: SBOM + if: steps.version.outputs.version != '' + run: pnpm release:sbom + + - name: Build NOTICE + if: steps.version.outputs.version != '' + run: pnpm build:notice + + - name: Pack + if: steps.version.outputs.version != '' + run: | + npm pack packages/appkit/tmp + npm pack packages/appkit-ui/tmp + + - name: Generate SHA256 + if: steps.version.outputs.version != '' + run: sha256sum *.tgz > SHA256SUMS + + - name: Write version file + if: steps.version.outputs.version != '' + run: echo "${{ steps.version.outputs.version }}" > VERSION + + - name: Upload release artifacts + if: steps.version.outputs.version != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: appkit-release-${{ github.run_number }} + retention-days: 7 + path: | + *.tgz + changelog-diff.md + VERSION + SHA256SUMS + NOTICE.md diff --git a/.github/workflows/release-lakebase.yml b/.github/workflows/release-lakebase.yml deleted file mode 100644 index 694d9e42..00000000 --- a/.github/workflows/release-lakebase.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Release @databricks/lakebase - -on: - # push: - # branches: - # - main - # paths: - # - 'packages/lakebase/**' - workflow_dispatch: - inputs: - dry-run: - description: "Dry run (no actual release)" - required: false - type: boolean - default: false - -concurrency: - group: release - cancel-in-progress: false - -jobs: - release: - runs-on: - group: databricks-protected-runner-group - labels: linux-ubuntu-latest - - environment: release - - env: - DRY_RUN: ${{ inputs.dry-run == true }} - - permissions: - contents: write - id-token: write - - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Setup JFrog npm - uses: ./.github/actions/setup-jfrog-npm - with: - npmrc-path: .npmrc - - - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - registry-url: "https://registry.npmjs.org" - cache: "pnpm" - - - name: Update npm - run: npm install -g npm@11.12.0 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Release - working-directory: packages/lakebase - run: | - if [ "$DRY_RUN" == "true" ]; then - pnpm release:dry - else - pnpm release:ci - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index cc4db6a1..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: Release - -on: - # push: - # branches: - # - main - # paths-ignore: - # - 'packages/lakebase/**' - workflow_dispatch: - inputs: - dry-run: - description: "Dry run (no actual release)" - required: false - type: boolean - default: false - -concurrency: - group: release - cancel-in-progress: false - -jobs: - release: - runs-on: - group: databricks-protected-runner-group - labels: linux-ubuntu-latest - - environment: release - - outputs: - version: ${{ steps.version.outputs.version }} - - permissions: - contents: write - id-token: write - - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Setup JFrog npm - uses: ./.github/actions/setup-jfrog-npm - with: - npmrc-path: .npmrc - - - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - registry-url: "https://registry.npmjs.org" - cache: "pnpm" - - - name: Update npm - run: npm install -g npm@11.12.0 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Determine release mode - id: mode - run: | - if [ "${{ github.event_name }}" == "push" ]; then - echo "dry_run=false" >> $GITHUB_OUTPUT - else - echo "dry_run=${{ inputs.dry-run }}" >> $GITHUB_OUTPUT - fi - - - name: Determine version - id: version - if: steps.mode.outputs.dry_run != 'true' - run: | - VERSION=$(pnpm exec release-it --release-version --ci 2>/dev/null) || true - if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Next version: $VERSION" - else - echo "No releasable version detected" - fi - - - name: Release - run: | - if [ "${{ steps.mode.outputs.dry_run }}" == "true" ]; then - pnpm release:dry - else - pnpm release:ci - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - sync-template: - runs-on: - group: databricks-protected-runner-group - labels: linux-ubuntu-latest - - needs: release - # in case a dry run is performed, the version is not set so we need to check for it. - if: needs.release.outputs.version != '' - - permissions: - contents: write - id-token: write - - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - ref: main - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Setup JFrog npm - uses: ./.github/actions/setup-jfrog-npm - - - name: Setup pnpm - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 - - - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: 24 - cache: "pnpm" - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Sync template and push tag - run: pnpm exec tsx tools/publish-template-tag.ts ${{ needs.release.outputs.version }} diff --git a/.release-it.json b/.release-it.json index 3fb8718f..44c329ad 100644 --- a/.release-it.json +++ b/.release-it.json @@ -1,33 +1,21 @@ { "$schema": "https://unpkg.com/release-it@19/schema/release-it.json", "git": { - "commitMessage": "chore: release v${version} [skip ci]", - "tagName": "v${version}", - "tagMatch": "v*", - "tagAnnotation": "Release v${version}", - "requireBranch": "main", - "requireCleanWorkingDir": true, + "commit": false, + "tag": false, + "push": false, + "requireBranch": false, + "requireCleanWorkingDir": false, "requireCommits": true, "requireCommitsFail": false, - "commitsPath": "packages/appkit packages/appkit-ui packages/shared", - "push": true, - "pushArgs": ["--follow-tags"] + "tagMatch": "v*", + "commitsPath": "packages/appkit packages/appkit-ui packages/shared" }, "github": { - "release": true, - "releaseName": "AppKit v${version}", - "autoGenerate": false, - "draft": false, - "preRelease": false, - "tokenRef": "GITHUB_TOKEN" + "release": false }, "npm": false, - "hooks": { - "before:init": "pnpm audit --audit-level=high --prod", - "after:bump": "tsx tools/sync-versions.ts ${version} && pnpm build:notice && git add NOTICE.md", - "before:release": "pnpm build && pnpm --filter=docs build && pnpm --filter=@databricks/appkit dist && pnpm --filter=@databricks/appkit-ui dist && pnpm release:sbom", - "after:release": "npm publish packages/appkit/tmp --access public --provenance && npm publish packages/appkit-ui/tmp --access public --provenance" - }, + "hooks": {}, "plugins": { "@release-it/conventional-changelog": { "preset": { @@ -39,8 +27,7 @@ "commitGroupsSort": ["appkit", "appkit-ui"], "commitsSort": ["type", "subject"] }, - "infile": "CHANGELOG.md", - "header": "# Changelog\n\nAll notable changes to this project will be documented in this file.", + "infile": "changelog-diff.md", "gitRawCommitsOpts": { "path": ["packages/appkit", "packages/appkit-ui", "packages/shared"] }, diff --git a/CLAUDE.md b/CLAUDE.md index d2092ceb..d8831bb6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,52 +142,35 @@ pnpm clean:full # Remove build artifacts + node_modules ### Releasing -This project uses [release-it](https://github.com/release-it/release-it) with [conventional-changelog](https://www.conventionalcommits.org/) for automated releases. Both packages (`appkit` and `appkit-ui`) are always released together with the same version. +This project uses a two-stage release pipeline. Both packages (`appkit` and `appkit-ui`) are always released together with the same version. `@databricks/lakebase` is released independently. -#### GitHub Actions (Recommended) +#### Stage 1: Prepare (this repo) -Releases are automated via GitHub Actions and trigger in two ways: +The `prepare-release` workflow runs automatically on push to `main`: +1. Determines version from conventional commits using [release-it](https://github.com/release-it/release-it) with `.release-it.prepare.json` +2. Generates changelog diff +3. Builds, packs, and uploads artifacts (`.tgz`, changelog, SHA256 digests) +4. **Does NOT** commit, tag, push, or publish — only uploads artifacts -**Automatic (on merge to main):** -- When PRs are merged to `main`, the workflow automatically runs -- Analyzes commits since last release using conventional commits -- If there are `feat:` or `fix:` commits, both packages are released together -- If no releasable commits, the release is skipped +Lakebase has a separate `prepare-release-lakebase` workflow triggered by changes to `packages/lakebase/**`. -**Manual (workflow_dispatch):** -1. Go to **Actions → Release → Run workflow** -2. Optionally enable "Dry run" to preview without publishing -3. Click "Run workflow" +#### Stage 2: Publish (secure repo) -**Permissions (already configured, no secrets needed):** -- `contents: write` - to push commits and tags -- `id-token: write` - for npm OIDC/provenance publishing +A private secure release repo polls for new artifacts every 15 minutes: +1. Downloads and verifies SHA256 digests (fail-closed) +2. Runs security scan +3. Publishes to npm via OIDC Trusted Publishing (no stored tokens) +4. Applies changelog, bumps versions, commits, tags, and pushes back to this repo via GitHub App +5. Creates GitHub Release +6. Runs template sync -Both `GITHUB_TOKEN` and npm OIDC are provided automatically by GitHub Actions. +Manual fallback: `workflow_dispatch` with a specific run ID on the secure repo. -The workflow automatically: -- Builds all packages -- Bumps version based on conventional commits -- Updates `CHANGELOG.md` -- Creates git tag and GitHub release -- Publishes to npm - -#### Local Release (Alternative) - -**Prerequisites:** -- Be on `main` branch with a clean working directory -- Set `GITHUB_TOKEN` environment variable -- Be logged in to npm (`npm login`) +#### Local Preview ```bash -# Dry run (preview what will happen without making changes) +# Preview next version and changelog (no side effects) pnpm release:dry - -# Interactive release (prompts for version bump) -pnpm release - -# CI release (non-interactive, for automation) -pnpm release:ci ``` #### Version Bumps (Conventional Commits) diff --git a/package.json b/package.json index 8d84f069..0d3af43f 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,8 @@ "lint": "biome lint .", "pack:sdk": "pnpm build && pnpm --filter=docs build && pnpm -r tarball", "prepare": "husky", - "release": "release-it", "release:sbom": "pnpm exec cdxgen -t js --no-recurse --required-only -o packages/appkit/tmp/sbom.cdx.json packages/appkit && pnpm exec cdxgen -t js --no-recurse --required-only -o packages/appkit-ui/tmp/sbom.cdx.json packages/appkit-ui", "release:dry": "release-it --dry-run", - "release:ci": "release-it --ci", "setup:repo": "./tools/setup.sh", "start": "NODE_ENV=production pnpm build && pnpm --filter=dev-playground build:app && pnpm --filter=dev-playground start:local", "test:watch": "vitest", diff --git a/packages/lakebase/.release-it.json b/packages/lakebase/.release-it.json index 65c6526d..8f9061e3 100644 --- a/packages/lakebase/.release-it.json +++ b/packages/lakebase/.release-it.json @@ -1,39 +1,29 @@ { "$schema": "https://unpkg.com/release-it@19/schema/release-it.json", "git": { - "commitMessage": "chore(lakebase): release v${version} [skip ci]", - "tagName": "lakebase-v${version}", + "commit": false, + "tag": false, + "push": false, + "requireBranch": false, + "requireCleanWorkingDir": false, + "requireCommits": true, + "requireCommitsFail": false, "tagMatch": "lakebase-v*", - "tagAnnotation": "Release @databricks/lakebase v${version}", - "commitsPath": ".", - "requireBranch": "main", - "requireCleanWorkingDir": true, - "push": true, - "pushArgs": ["--follow-tags"] + "tagName": "lakebase-v${version}", + "commitsPath": "." }, "github": { - "release": true, - "releaseName": "@databricks/lakebase v${version}", - "autoGenerate": false, - "draft": false, - "preRelease": false, - "tokenRef": "GITHUB_TOKEN" + "release": false }, "npm": false, - "hooks": { - "before:init": "pnpm audit --audit-level=high --prod", - "after:bump": "npm version ${version} --no-git-tag-version --allow-same-version", - "before:release": "pnpm build:package && pnpm dist && pnpm release:sbom", - "after:release": "npm publish ./tmp --access public --provenance" - }, + "hooks": {}, "plugins": { "@release-it/conventional-changelog": { "preset": { "name": "conventionalcommits", "bumpStrict": true }, - "infile": "CHANGELOG.md", - "header": "# Changelog\n\nAll notable changes to @databricks/lakebase will be documented in this file.", + "infile": "changelog-diff.md", "gitRawCommitsOpts": { "path": "." }, "commitsOpts": { "path": "." } } diff --git a/packages/lakebase/package.json b/packages/lakebase/package.json index d2adbb9b..5f8c7321 100644 --- a/packages/lakebase/package.json +++ b/packages/lakebase/package.json @@ -44,9 +44,7 @@ "dist": "tsx ../../tools/dist-lakebase.ts", "tarball": "rm -rf tmp && pnpm dist && npm pack ./tmp --pack-destination ./tmp", "typecheck": "tsc --noEmit", - "release": "release-it", "release:dry": "release-it --dry-run", - "release:ci": "release-it --ci", "release:sbom": "pnpm exec cdxgen -t js --no-recurse --required-only -o tmp/sbom.cdx.json ." }, "dependencies": { diff --git a/tools/check-template-deps.js b/tools/check-template-deps.js new file mode 100644 index 00000000..f542250e --- /dev/null +++ b/tools/check-template-deps.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node +/** + * Validates that all dependencies in template/package.json use exact versions + * (no ^, ~, >=, * prefixes). This prevents supply chain attacks during + * template sync where npm install could pull unexpected transitive deps. + */ + +const pkg = require("../template/package.json"); +const deps = { ...pkg.dependencies, ...pkg.devDependencies }; +const unpinned = Object.entries(deps).filter(([, v]) => /^[~^>=*]/.test(v)); + +if (unpinned.length) { + console.error( + "Unpinned deps:", + unpinned.map(([k, v]) => `${k}@${v}`).join(", "), + ); + process.exit(1); +} From d24a211ce77b5693d3c03a04447b199a36afff32 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 14:55:45 +0200 Subject: [PATCH 02/19] fix: remove stale config refs, split meta artifact, TS refactor - Remove stale -c .release-it.prepare.json from workflow version checks - Add pull_request trigger to prepare-release for PR testing - Split VERSION into separate meta artifact for faster poll reads - Refactor check-template-deps.js to TypeScript - Add secure repo usage comment to sync-versions.ts Signed-off-by: Pawel Kosiec --- .github/workflows/ci.yml | 2 +- .github/workflows/prepare-release-lakebase.yml | 11 +++++++++-- .github/workflows/prepare-release.yml | 14 ++++++++++++-- ...k-template-deps.js => check-template-deps.ts} | 16 +++++++++++++--- tools/sync-versions.ts | 5 +++++ 5 files changed, 40 insertions(+), 8 deletions(-) rename tools/{check-template-deps.js => check-template-deps.ts} (60%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 299ace32..548d095f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Run License Check run: pnpm run check:licenses - name: Check template deps are pinned - run: node tools/check-template-deps.js + run: tsx tools/check-template-deps.ts test: name: Unit Tests diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index ebc6c331..671df545 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -48,8 +48,7 @@ jobs: id: version working-directory: packages/lakebase run: | - VERSION=$(pnpm exec release-it --release-version --ci \ - -c .release-it.prepare.json 2>/dev/null) || true + VERSION=$(pnpm exec release-it --release-version --ci 2>/dev/null) || true if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Next version: $VERSION" @@ -87,6 +86,14 @@ jobs: if: steps.version.outputs.version != '' run: echo "${{ steps.version.outputs.version }}" > VERSION + - name: Upload release metadata + if: steps.version.outputs.version != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: lakebase-release-meta-${{ github.run_number }} + retention-days: 7 + path: VERSION + - name: Upload release artifacts if: steps.version.outputs.version != '' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 7c2edadb..f5fec3ff 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main concurrency: group: prepare-release @@ -45,8 +48,7 @@ jobs: - name: Check for releasable commits id: version run: | - VERSION=$(pnpm exec release-it --release-version --ci \ - -c .release-it.prepare.json 2>/dev/null) || true + VERSION=$(pnpm exec release-it --release-version --ci 2>/dev/null) || true if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Next version: $VERSION" @@ -95,6 +97,14 @@ jobs: if: steps.version.outputs.version != '' run: echo "${{ steps.version.outputs.version }}" > VERSION + - name: Upload release metadata + if: steps.version.outputs.version != '' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: appkit-release-meta-${{ github.run_number }} + retention-days: 7 + path: VERSION + - name: Upload release artifacts if: steps.version.outputs.version != '' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/tools/check-template-deps.js b/tools/check-template-deps.ts similarity index 60% rename from tools/check-template-deps.js rename to tools/check-template-deps.ts index f542250e..8174a97b 100644 --- a/tools/check-template-deps.js +++ b/tools/check-template-deps.ts @@ -1,12 +1,22 @@ -#!/usr/bin/env node +#!/usr/bin/env tsx /** * Validates that all dependencies in template/package.json use exact versions * (no ^, ~, >=, * prefixes). This prevents supply chain attacks during * template sync where npm install could pull unexpected transitive deps. */ -const pkg = require("../template/package.json"); -const deps = { ...pkg.dependencies, ...pkg.devDependencies }; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const pkg = JSON.parse( + readFileSync(join(import.meta.dirname, "../template/package.json"), "utf-8"), +); + +const deps: Record = { + ...pkg.dependencies, + ...pkg.devDependencies, +}; + const unpinned = Object.entries(deps).filter(([, v]) => /^[~^>=*]/.test(v)); if (unpinned.length) { diff --git a/tools/sync-versions.ts b/tools/sync-versions.ts index ead8aa26..54faf813 100644 --- a/tools/sync-versions.ts +++ b/tools/sync-versions.ts @@ -4,6 +4,11 @@ * Used by release-it after version bump. */ +/** + * NOTE: This script is also used by the private secure release repo + * during the finalize step. Changes here affect the release pipeline. + */ + import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; From 664d56bdf5904547cf6e8cb257b04f4503550fe5 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 15:00:16 +0200 Subject: [PATCH 03/19] fix: use default ~/.npmrc path for JFrog setup in prepare-release Project-level .npmrc caused E401 in nested npm install (dev-playground client postinstall). Match CI workflow pattern: use default ~/.npmrc. Signed-off-by: Pawel Kosiec --- .github/workflows/prepare-release-lakebase.yml | 2 -- .github/workflows/prepare-release.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index 671df545..66708d62 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -29,8 +29,6 @@ jobs: - name: Setup JFrog npm uses: ./.github/actions/setup-jfrog-npm - with: - npmrc-path: .npmrc - name: Setup pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index f5fec3ff..980766c2 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -30,8 +30,6 @@ jobs: - name: Setup JFrog npm uses: ./.github/actions/setup-jfrog-npm - with: - npmrc-path: .npmrc - name: Setup pnpm uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 From a62feaa768ae05164eb515acf9258c5e46a775a4 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 15:06:31 +0200 Subject: [PATCH 04/19] fix: use pnpm exec tsx to match CI conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare tsx isn't on PATH in CI runners — use pnpm exec tsx like all other workflow steps. Signed-off-by: Pawel Kosiec --- .github/workflows/ci.yml | 2 +- .github/workflows/prepare-release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 548d095f..77bae315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Run License Check run: pnpm run check:licenses - name: Check template deps are pinned - run: tsx tools/check-template-deps.ts + run: pnpm exec tsx tools/check-template-deps.ts test: name: Unit Tests diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 980766c2..f5b53434 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -61,7 +61,7 @@ jobs: pnpm exec release-it ${{ steps.version.outputs.version }} --ci - name: Sync versions if: steps.version.outputs.version != '' - run: tsx tools/sync-versions.ts "${{ steps.version.outputs.version }}" + run: pnpm exec tsx tools/sync-versions.ts "${{ steps.version.outputs.version }}" - name: Build if: steps.version.outputs.version != '' From 36fe1c6e5dfc45ac298eb6ad0de5456226fffb81 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 15:09:36 +0200 Subject: [PATCH 05/19] fix(appkit): trigger prepare-release pipeline test Add PR trigger to prepare-release-lakebase workflow and trivial changes to packages/appkit and packages/lakebase to produce releasable commits for version detection. TODO: revert before merging Signed-off-by: Pawel Kosiec --- .github/workflows/prepare-release-lakebase.yml | 3 +++ packages/appkit/src/index.ts | 1 + packages/lakebase/src/index.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index 66708d62..97586010 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -6,6 +6,9 @@ on: - main paths: - 'packages/lakebase/**' + pull_request: + branches: + - main concurrency: group: prepare-release-lakebase diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index 8db7f1d7..7b2576fd 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -1,3 +1,4 @@ +// TODO: remove this comment — added to trigger prepare-release test /** * @packageDocumentation * diff --git a/packages/lakebase/src/index.ts b/packages/lakebase/src/index.ts index 45217317..02dba488 100644 --- a/packages/lakebase/src/index.ts +++ b/packages/lakebase/src/index.ts @@ -1,3 +1,4 @@ +// TODO: remove this comment — added to trigger prepare-release test export { getUsernameWithApiLookup, getWorkspaceClient } from "./config"; export { generateDatabaseCredential } from "./credentials"; export { createLakebasePool } from "./pool"; From 8c3111e6a20e9074d76c629b89e7a77d8a9ca069 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 15:19:46 +0200 Subject: [PATCH 06/19] fix: enable getLatestTagFromAllRefs for PR branch compatibility release-it uses git describe by default, which only finds tags reachable from the current commit. On PR checkouts (merge commit), tags may not be reachable. Setting getLatestTagFromAllRefs: true considers all tags regardless of reachability. Also remove 2>/dev/null from version check to show release-it errors. Signed-off-by: Pawel Kosiec --- .github/workflows/prepare-release-lakebase.yml | 2 +- .github/workflows/prepare-release.yml | 2 +- .release-it.json | 1 + packages/lakebase/.release-it.json | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index 97586010..05956524 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -49,7 +49,7 @@ jobs: id: version working-directory: packages/lakebase run: | - VERSION=$(pnpm exec release-it --release-version --ci 2>/dev/null) || true + VERSION=$(pnpm exec release-it --release-version --ci) || true if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Next version: $VERSION" diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index f5b53434..a233bfbc 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -46,7 +46,7 @@ jobs: - name: Check for releasable commits id: version run: | - VERSION=$(pnpm exec release-it --release-version --ci 2>/dev/null) || true + VERSION=$(pnpm exec release-it --release-version --ci) || true if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Next version: $VERSION" diff --git a/.release-it.json b/.release-it.json index 44c329ad..7c2995d0 100644 --- a/.release-it.json +++ b/.release-it.json @@ -9,6 +9,7 @@ "requireCommits": true, "requireCommitsFail": false, "tagMatch": "v*", + "getLatestTagFromAllRefs": true, "commitsPath": "packages/appkit packages/appkit-ui packages/shared" }, "github": { diff --git a/packages/lakebase/.release-it.json b/packages/lakebase/.release-it.json index 8f9061e3..c5b4e8bd 100644 --- a/packages/lakebase/.release-it.json +++ b/packages/lakebase/.release-it.json @@ -10,6 +10,7 @@ "requireCommitsFail": false, "tagMatch": "lakebase-v*", "tagName": "lakebase-v${version}", + "getLatestTagFromAllRefs": true, "commitsPath": "." }, "github": { From 290d9cee4be470ca12795ac980519c1ad9c7b363 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 15:30:11 +0200 Subject: [PATCH 07/19] fix: checkout PR head ref to fix detached HEAD for release-it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release-it fails with "ref HEAD is not a symbolic ref" on PR merge commit checkouts. Use github.head_ref to checkout the actual branch. Also revert trivial test changes in package source files — existing conventional commits from main are sufficient for version detection. Signed-off-by: Pawel Kosiec --- .github/workflows/prepare-release-lakebase.yml | 1 + .github/workflows/prepare-release.yml | 1 + packages/appkit/src/index.ts | 1 - packages/lakebase/src/index.ts | 1 - 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index 05956524..3aa0e0d3 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -29,6 +29,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 + ref: ${{ github.head_ref || '' }} # TODO: remove before merging — needed for PR testing only - name: Setup JFrog npm uses: ./.github/actions/setup-jfrog-npm diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index a233bfbc..df76eed5 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -27,6 +27,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 + ref: ${{ github.head_ref || '' }} # TODO: remove before merging — needed for PR testing only - name: Setup JFrog npm uses: ./.github/actions/setup-jfrog-npm diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index 7b2576fd..8db7f1d7 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -1,4 +1,3 @@ -// TODO: remove this comment — added to trigger prepare-release test /** * @packageDocumentation * diff --git a/packages/lakebase/src/index.ts b/packages/lakebase/src/index.ts index 02dba488..45217317 100644 --- a/packages/lakebase/src/index.ts +++ b/packages/lakebase/src/index.ts @@ -1,4 +1,3 @@ -// TODO: remove this comment — added to trigger prepare-release test export { getUsernameWithApiLookup, getWorkspaceClient } from "./config"; export { generateDatabaseCredential } from "./credentials"; export { createLakebasePool } from "./pool"; From 67fd3bf4b7fa123029e87ef7c4f83f1f6762500c Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 16:24:13 +0200 Subject: [PATCH 08/19] fix: correct docs and add missing paths filter for lakebase PR trigger - Fix .release-it.prepare.json reference to .release-it.json in CLAUDE.md - Add check-template-deps.ts to tools list in CLAUDE.md - Add missing paths filter on pull_request trigger in prepare-release-lakebase so it only runs when lakebase files change (matching the push trigger) Signed-off-by: Pawel Kosiec --- .github/workflows/prepare-release-lakebase.yml | 2 ++ CLAUDE.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index 3aa0e0d3..a72c00a6 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -9,6 +9,8 @@ on: pull_request: branches: - main + paths: + - 'packages/lakebase/**' concurrency: group: prepare-release-lakebase diff --git a/CLAUDE.md b/CLAUDE.md index d8831bb6..3a6a418f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,7 @@ Examples: - generate-app-templates.ts - Generate app templates - check-licenses.ts - License compliance checks - build-notice.ts - Build NOTICE.md from dependencies + - check-template-deps.ts - Validate template package.json dependencies are pinned ``` ## Development Commands @@ -147,7 +148,7 @@ This project uses a two-stage release pipeline. Both packages (`appkit` and `app #### Stage 1: Prepare (this repo) The `prepare-release` workflow runs automatically on push to `main`: -1. Determines version from conventional commits using [release-it](https://github.com/release-it/release-it) with `.release-it.prepare.json` +1. Determines version from conventional commits using [release-it](https://github.com/release-it/release-it) with `.release-it.json` 2. Generates changelog diff 3. Builds, packs, and uploads artifacts (`.tgz`, changelog, SHA256 digests) 4. **Does NOT** commit, tag, push, or publish — only uploads artifacts From 7cf8e92f9aad1008eef64f93528c2ab8371e0894 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 18:10:14 +0200 Subject: [PATCH 09/19] refactor: add finalize-release.ts and enhance publish-template-tag.ts - New tools/finalize-release.ts: handles changelog splicing, version bumps, NOTICE copy, and git commit/tag for the secure release pipeline - Updated tools/publish-template-tag.ts: added lockfile diff check to verify only @databricks/appkit* packages change during template sync Signed-off-by: Pawel Kosiec --- tools/finalize-release.ts | 89 +++++++++++++++++++++++++++++++++++ tools/publish-template-tag.ts | 49 +++++++++++++++++-- 2 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 tools/finalize-release.ts diff --git a/tools/finalize-release.ts b/tools/finalize-release.ts new file mode 100644 index 00000000..41d1d7f7 --- /dev/null +++ b/tools/finalize-release.ts @@ -0,0 +1,89 @@ +#!/usr/bin/env tsx +/** + * Applies release changes to the appkit repo: changelog, version bumps, + * NOTICE copy, then commits and tags. Does NOT push — the caller handles that. + * + * Used by the private secure release repo during the finalize step. + * Changes here affect the release pipeline. + * + * Usage: tsx tools/finalize-release.ts + * version — semver string, e.g. "0.22.0" + * tag — git tag, e.g. "v0.22.0" or "lakebase-v0.3.0" + * stream — "appkit" or "lakebase" + * artifacts-dir — path to the downloaded release artifacts + */ + +import { spawnSync } from "node:child_process"; +import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = process.cwd(); + +const [version, tag, stream, artifactsDir] = process.argv.slice(2); +if (!version || !tag || !stream || !artifactsDir) { + console.error( + "Usage: tsx tools/finalize-release.ts ", + ); + process.exit(1); +} + +function run(cmd: string, args: string[]): void { + const result = spawnSync(cmd, args, { cwd: ROOT, stdio: "inherit" }); + if (result.status !== 0) { + console.error(`Command failed: ${cmd} ${args.join(" ")}`); + process.exit(result.status ?? 1); + } +} + +// 1. Apply changelog diff +const changelogDiff = join(artifactsDir, "changelog-diff.md"); +if (existsSync(changelogDiff)) { + const diff = readFileSync(changelogDiff, "utf-8"); + const changelogPath = join(ROOT, "CHANGELOG.md"); + + if (existsSync(changelogPath)) { + const existing = readFileSync(changelogPath, "utf-8"); + const lines = existing.split("\n"); + // Insert after the header (first 3 lines: title, blank, description) + const header = lines.slice(0, 3).join("\n"); + const rest = lines.slice(3).join("\n"); + writeFileSync(changelogPath, `${header}\n\n${diff}\n${rest}`); + } else { + copyFileSync(changelogDiff, changelogPath); + } + console.log("✓ changelog updated"); +} + +// 2. Bump versions +if (stream === "appkit") { + // Reuse sync-versions.ts logic for appkit + appkit-ui + const PACKAGES = ["packages/appkit", "packages/appkit-ui"]; + for (const pkg of PACKAGES) { + const pkgJsonPath = join(ROOT, pkg, "package.json"); + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); + pkgJson.version = version; + writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); + console.log(`✓ ${pkg}/package.json → ${version}`); + } +} else { + // Lakebase is a single package + const pkgJsonPath = join(ROOT, "packages/lakebase/package.json"); + const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); + pkgJson.version = version; + writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); + console.log(`✓ packages/lakebase/package.json → ${version}`); +} + +// 3. Copy NOTICE.md if present +const noticeSrc = join(artifactsDir, "NOTICE.md"); +if (existsSync(noticeSrc)) { + copyFileSync(noticeSrc, join(ROOT, "NOTICE.md")); + console.log("✓ NOTICE.md copied"); +} + +// 4. Commit and tag (do NOT push) +run("git", ["add", "-A"]); +run("git", ["commit", "-s", "-m", `chore: release ${tag} [skip ci]`]); +run("git", ["tag", "-a", tag, "-m", `Release ${tag}`]); + +console.log(`✓ committed and tagged ${tag}`); diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index da5114a2..2db1b608 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -1,12 +1,20 @@ #!/usr/bin/env tsx /** * Syncs the template to the given version (with retry), then commits, tags - * template-vX.X.X, and pushes. Used by the Release workflow (sync-template job - * in .github/workflows/release.yml) and for manual runs. + * template-vX.X.X, and pushes. + * + * Used by the private secure release repo during the template-sync step. + * Changes here affect the release pipeline. */ import { spawnSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; +import { + copyFileSync, + existsSync, + readFileSync, + unlinkSync, + writeFileSync, +} from "node:fs"; import { join } from "node:path"; const ROOT = process.cwd(); @@ -41,6 +49,8 @@ if (templateJson.dependencies) { // 2. npm install in template (with retry for registry propagation) const MAX_ATTEMPTS = 3; const templateDir = join(ROOT, "template"); +const lockfilePath = join(templateDir, "package-lock.json"); +const lockfileBackup = join(templateDir, "package-lock.json.before"); function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -66,12 +76,45 @@ async function runNpmInstallWithRetry(): Promise { return lastStatus; } +// Save lockfile before install for diff check +if (existsSync(lockfilePath)) { + copyFileSync(lockfilePath, lockfileBackup); +} + const installExit = await runNpmInstallWithRetry(); if (installExit !== 0) { console.error(`npm install failed after ${MAX_ATTEMPTS} attempts`); process.exit(installExit); } +// Lockfile diff check: verify only @databricks/appkit* packages changed +if (existsSync(lockfileBackup)) { + const parsePkgs = (path: string): string[] => { + const lock = JSON.parse(readFileSync(path, "utf-8")); + return Object.keys(lock.packages ?? {}).sort(); + }; + + const before = new Set(parsePkgs(lockfileBackup)); + const after = new Set(parsePkgs(lockfilePath)); + + const added = [...after].filter((p) => !before.has(p)); + const removed = [...before].filter((p) => !after.has(p)); + const unexpected = [...added, ...removed].filter( + (p) => !p.includes("@databricks/appkit"), + ); + + if (unexpected.length > 0) { + console.error("Unexpected packages changed in lockfile:"); + for (const pkg of unexpected) { + console.error(` ${pkg}`); + } + unlinkSync(lockfileBackup); + process.exit(1); + } + + unlinkSync(lockfileBackup); +} + // 3. Git add, commit, tag, push const commands: [string, string[]][] = [ ["git", ["add", "template/package.json", "template/package-lock.json"]], From af9527dca718287d2a9031e804affbab72761125 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 18:18:18 +0200 Subject: [PATCH 10/19] refactor: use stream-to-packages map in finalize-release.ts Signed-off-by: Pawel Kosiec --- tools/finalize-release.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tools/finalize-release.ts b/tools/finalize-release.ts index 41d1d7f7..769bf6f3 100644 --- a/tools/finalize-release.ts +++ b/tools/finalize-release.ts @@ -55,23 +55,23 @@ if (existsSync(changelogDiff)) { } // 2. Bump versions -if (stream === "appkit") { - // Reuse sync-versions.ts logic for appkit + appkit-ui - const PACKAGES = ["packages/appkit", "packages/appkit-ui"]; - for (const pkg of PACKAGES) { - const pkgJsonPath = join(ROOT, pkg, "package.json"); - const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); - pkgJson.version = version; - writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); - console.log(`✓ ${pkg}/package.json → ${version}`); - } -} else { - // Lakebase is a single package - const pkgJsonPath = join(ROOT, "packages/lakebase/package.json"); +const STREAM_PACKAGES: Record = { + appkit: ["packages/appkit", "packages/appkit-ui"], + lakebase: ["packages/lakebase"], +}; + +const packages = STREAM_PACKAGES[stream]; +if (!packages) { + console.error(`Unknown stream: ${stream}`); + process.exit(1); +} + +for (const pkg of packages) { + const pkgJsonPath = join(ROOT, pkg, "package.json"); const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); pkgJson.version = version; writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); - console.log(`✓ packages/lakebase/package.json → ${version}`); + console.log(`✓ ${pkg}/package.json → ${version}`); } // 3. Copy NOTICE.md if present From 82a5824e0c51e41af1d9bb24350778a7a5d4a53d Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 18:58:49 +0200 Subject: [PATCH 11/19] fix: use public npm registry in publish-template-tag.ts Signed-off-by: Pawel Kosiec --- tools/publish-template-tag.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index 2db1b608..f01ce550 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -59,7 +59,13 @@ function sleep(ms: number): Promise { async function runNpmInstallWithRetry(): Promise { let lastStatus = 0; for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - const status = run("npm", ["install"], { cwd: templateDir }); + // Use public npm registry — JFrog proxy has up to 7-day propagation delay + // for newly published packages + const status = run( + "npm", + ["install", "--registry", "https://registry.npmjs.org"], + { cwd: templateDir }, + ); lastStatus = status; if (status === 0) { console.log("✓ template/package-lock.json updated (npm install)"); From b68dd051605e62333a834db7162dae17a45a7b11 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 19:00:57 +0200 Subject: [PATCH 12/19] revert: remove inline registry flag from publish-template-tag.ts Signed-off-by: Pawel Kosiec --- tools/publish-template-tag.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index f01ce550..2db1b608 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -59,13 +59,7 @@ function sleep(ms: number): Promise { async function runNpmInstallWithRetry(): Promise { let lastStatus = 0; for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - // Use public npm registry — JFrog proxy has up to 7-day propagation delay - // for newly published packages - const status = run( - "npm", - ["install", "--registry", "https://registry.npmjs.org"], - { cwd: templateDir }, - ); + const status = run("npm", ["install"], { cwd: templateDir }); lastStatus = status; if (status === 0) { console.log("✓ template/package-lock.json updated (npm install)"); From f1041fbb06addc447f9de0a183aa5b2d7ee294b7 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 19:10:56 +0200 Subject: [PATCH 13/19] fix: allow @databricks/lakebase changes in template lockfile diff check Signed-off-by: Pawel Kosiec --- tools/publish-template-tag.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index 2db1b608..d63f80c3 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -87,7 +87,8 @@ if (installExit !== 0) { process.exit(installExit); } -// Lockfile diff check: verify only @databricks/appkit* packages changed +// Lockfile diff check: verify only @databricks packages changed +const ALLOWED_PACKAGES = ["@databricks/appkit", "@databricks/lakebase"]; if (existsSync(lockfileBackup)) { const parsePkgs = (path: string): string[] => { const lock = JSON.parse(readFileSync(path, "utf-8")); @@ -100,7 +101,7 @@ if (existsSync(lockfileBackup)) { const added = [...after].filter((p) => !before.has(p)); const removed = [...before].filter((p) => !after.has(p)); const unexpected = [...added, ...removed].filter( - (p) => !p.includes("@databricks/appkit"), + (p) => !ALLOWED_PACKAGES.some((allowed) => p.includes(allowed)), ); if (unexpected.length > 0) { From 38dccd50a58e662f7a1d90c7b3606b92962c0e9c Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 19:23:05 +0200 Subject: [PATCH 14/19] fix: add appkit-ui to lockfile allowlist and DCO sign-off to template sync - Add @databricks/appkit-ui to ALLOWED_PACKAGES so the lockfile diff check explicitly permits it (previously passed accidentally via substring match on @databricks/appkit) - Add -s flag to template sync git commit for DCO compliance Signed-off-by: Pawel Kosiec --- tools/publish-template-tag.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index d63f80c3..37b8d19f 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -88,7 +88,11 @@ if (installExit !== 0) { } // Lockfile diff check: verify only @databricks packages changed -const ALLOWED_PACKAGES = ["@databricks/appkit", "@databricks/lakebase"]; +const ALLOWED_PACKAGES = [ + "@databricks/appkit", + "@databricks/appkit-ui", + "@databricks/lakebase", +]; if (existsSync(lockfileBackup)) { const parsePkgs = (path: string): string[] => { const lock = JSON.parse(readFileSync(path, "utf-8")); @@ -119,7 +123,10 @@ if (existsSync(lockfileBackup)) { // 3. Git add, commit, tag, push const commands: [string, string[]][] = [ ["git", ["add", "template/package.json", "template/package-lock.json"]], - ["git", ["commit", "-m", `chore: sync template to v${version} [skip ci]`]], + [ + "git", + ["commit", "-s", "-m", `chore: sync template to v${version} [skip ci]`], + ], ["git", ["tag", "-a", `template-v${version}`, "-m", `Template v${version}`]], ["git", ["push", "origin", "main", "--follow-tags"]], ]; From fb2fc3a0c9fc74a0338c02e20794dfcd7f0bc75e Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 19:43:08 +0200 Subject: [PATCH 15/19] test: push to HEAD instead of hardcoded main (revert or keep after testing) Signed-off-by: Pawel Kosiec --- tools/publish-template-tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index 37b8d19f..e580f7d3 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -128,7 +128,7 @@ const commands: [string, string[]][] = [ ["commit", "-s", "-m", `chore: sync template to v${version} [skip ci]`], ], ["git", ["tag", "-a", `template-v${version}`, "-m", `Template v${version}`]], - ["git", ["push", "origin", "main", "--follow-tags"]], + ["git", ["push", "origin", "HEAD", "--follow-tags"]], // TODO: revert to "main" after testing (or keep — HEAD == main in production) ]; for (const [command, args] of commands) { From 3644af418f1dc1dae65636320ce5f9650824085e Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 8 Apr 2026 21:26:24 +0200 Subject: [PATCH 16/19] refactor: push to HEAD in template sync, remove PR triggers and temp refs from prepare-release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - publish-template-tag.ts: push to HEAD instead of hardcoded main (HEAD == main in production, more flexible) - Remove pull_request triggers from prepare-release workflows — these should only run on push to main - Remove temporary ref override used for PR testing Signed-off-by: Pawel Kosiec --- .github/workflows/prepare-release-lakebase.yml | 6 ------ .github/workflows/prepare-release.yml | 4 ---- tools/publish-template-tag.ts | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/prepare-release-lakebase.yml b/.github/workflows/prepare-release-lakebase.yml index a72c00a6..bac658fc 100644 --- a/.github/workflows/prepare-release-lakebase.yml +++ b/.github/workflows/prepare-release-lakebase.yml @@ -6,11 +6,6 @@ on: - main paths: - 'packages/lakebase/**' - pull_request: - branches: - - main - paths: - - 'packages/lakebase/**' concurrency: group: prepare-release-lakebase @@ -31,7 +26,6 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - ref: ${{ github.head_ref || '' }} # TODO: remove before merging — needed for PR testing only - name: Setup JFrog npm uses: ./.github/actions/setup-jfrog-npm diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index df76eed5..bf0a2c89 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -4,9 +4,6 @@ on: push: branches: - main - pull_request: - branches: - - main concurrency: group: prepare-release @@ -27,7 +24,6 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - ref: ${{ github.head_ref || '' }} # TODO: remove before merging — needed for PR testing only - name: Setup JFrog npm uses: ./.github/actions/setup-jfrog-npm diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index e580f7d3..39c99ffd 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -128,7 +128,7 @@ const commands: [string, string[]][] = [ ["commit", "-s", "-m", `chore: sync template to v${version} [skip ci]`], ], ["git", ["tag", "-a", `template-v${version}`, "-m", `Template v${version}`]], - ["git", ["push", "origin", "HEAD", "--follow-tags"]], // TODO: revert to "main" after testing (or keep — HEAD == main in production) + ["git", ["push", "origin", "HEAD", "--follow-tags"]], ]; for (const [command, args] of commands) { From 6770c5930a908fe1c888990a3b7b6ca2821076cf Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Thu, 9 Apr 2026 10:26:18 +0200 Subject: [PATCH 17/19] fix: remove lockfile diff check from template sync The check verified that only @databricks/* packages changed in the lockfile after npm install. In practice, bumping appkit legitimately changes transitive dependencies (tanstack, hookform, etc.), causing the check to block valid releases. Security is already covered by the scan step on the secure release repo. Signed-off-by: Pawel Kosiec --- tools/publish-template-tag.ts | 49 +---------------------------------- 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/tools/publish-template-tag.ts b/tools/publish-template-tag.ts index 39c99ffd..5286f9df 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -8,13 +8,7 @@ */ import { spawnSync } from "node:child_process"; -import { - copyFileSync, - existsSync, - readFileSync, - unlinkSync, - writeFileSync, -} from "node:fs"; +import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const ROOT = process.cwd(); @@ -49,9 +43,6 @@ if (templateJson.dependencies) { // 2. npm install in template (with retry for registry propagation) const MAX_ATTEMPTS = 3; const templateDir = join(ROOT, "template"); -const lockfilePath = join(templateDir, "package-lock.json"); -const lockfileBackup = join(templateDir, "package-lock.json.before"); - function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -76,50 +67,12 @@ async function runNpmInstallWithRetry(): Promise { return lastStatus; } -// Save lockfile before install for diff check -if (existsSync(lockfilePath)) { - copyFileSync(lockfilePath, lockfileBackup); -} - const installExit = await runNpmInstallWithRetry(); if (installExit !== 0) { console.error(`npm install failed after ${MAX_ATTEMPTS} attempts`); process.exit(installExit); } -// Lockfile diff check: verify only @databricks packages changed -const ALLOWED_PACKAGES = [ - "@databricks/appkit", - "@databricks/appkit-ui", - "@databricks/lakebase", -]; -if (existsSync(lockfileBackup)) { - const parsePkgs = (path: string): string[] => { - const lock = JSON.parse(readFileSync(path, "utf-8")); - return Object.keys(lock.packages ?? {}).sort(); - }; - - const before = new Set(parsePkgs(lockfileBackup)); - const after = new Set(parsePkgs(lockfilePath)); - - const added = [...after].filter((p) => !before.has(p)); - const removed = [...before].filter((p) => !after.has(p)); - const unexpected = [...added, ...removed].filter( - (p) => !ALLOWED_PACKAGES.some((allowed) => p.includes(allowed)), - ); - - if (unexpected.length > 0) { - console.error("Unexpected packages changed in lockfile:"); - for (const pkg of unexpected) { - console.error(` ${pkg}`); - } - unlinkSync(lockfileBackup); - process.exit(1); - } - - unlinkSync(lockfileBackup); -} - // 3. Git add, commit, tag, push const commands: [string, string[]][] = [ ["git", ["add", "template/package.json", "template/package-lock.json"]], From cceb3dc93a33c14e3ce45376ba04da43994252ae Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Thu, 9 Apr 2026 10:40:29 +0200 Subject: [PATCH 18/19] fix: harden template dep pinning and changelog insertion - Replace blocklist regex with allowlist in check-template-deps.ts to reject file:, git:, latest, and other non-pinned specifiers while allowing exact semver, pre-release tags, and npm aliases - Find first version heading (## [) instead of assuming 3-line header in finalize-release.ts changelog insertion - Document finalize-release.ts in CLAUDE.md tools list Signed-off-by: Pawel Kosiec --- CLAUDE.md | 1 + tools/check-template-deps.ts | 5 ++++- tools/finalize-release.ts | 10 ++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3a6a418f..eaf50479 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,7 @@ Examples: - check-licenses.ts - License compliance checks - build-notice.ts - Build NOTICE.md from dependencies - check-template-deps.ts - Validate template package.json dependencies are pinned + - finalize-release.ts - Apply release changes (changelog, versions, tags) for secure repo ``` ## Development Commands diff --git a/tools/check-template-deps.ts b/tools/check-template-deps.ts index 8174a97b..f3b1f52d 100644 --- a/tools/check-template-deps.ts +++ b/tools/check-template-deps.ts @@ -17,7 +17,10 @@ const deps: Record = { ...pkg.devDependencies, }; -const unpinned = Object.entries(deps).filter(([, v]) => /^[~^>=*]/.test(v)); +const PINNED_VERSION = /^(npm:(@[\w-]+\/)?[\w.-]+@)?\d+\.\d+\.\d+(-[\w.]+)?$/; +const unpinned = Object.entries(deps).filter( + ([, v]) => !PINNED_VERSION.test(v), +); if (unpinned.length) { console.error( diff --git a/tools/finalize-release.ts b/tools/finalize-release.ts index 769bf6f3..d9b245a9 100644 --- a/tools/finalize-release.ts +++ b/tools/finalize-release.ts @@ -44,10 +44,12 @@ if (existsSync(changelogDiff)) { if (existsSync(changelogPath)) { const existing = readFileSync(changelogPath, "utf-8"); const lines = existing.split("\n"); - // Insert after the header (first 3 lines: title, blank, description) - const header = lines.slice(0, 3).join("\n"); - const rest = lines.slice(3).join("\n"); - writeFileSync(changelogPath, `${header}\n\n${diff}\n${rest}`); + // Insert before the first version section (## [...]) + const firstSection = lines.findIndex((l) => /^## \[/.test(l)); + const insertAt = firstSection > 0 ? firstSection : lines.length; + const header = lines.slice(0, insertAt).join("\n"); + const rest = lines.slice(insertAt).join("\n"); + writeFileSync(changelogPath, `${header}\n${diff}\n\n${rest}`); } else { copyFileSync(changelogDiff, changelogPath); } From 4b20a2e3d96f6527ae25ac5642f74932586dae20 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Thu, 9 Apr 2026 10:41:57 +0200 Subject: [PATCH 19/19] fix: use stream-aware changelog path in finalize-release Route changelog diff to packages/lakebase/CHANGELOG.md when stream=lakebase instead of always writing to root CHANGELOG.md. Signed-off-by: Pawel Kosiec --- tools/finalize-release.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/finalize-release.ts b/tools/finalize-release.ts index d9b245a9..d561927b 100644 --- a/tools/finalize-release.ts +++ b/tools/finalize-release.ts @@ -36,10 +36,15 @@ function run(cmd: string, args: string[]): void { } // 1. Apply changelog diff +const STREAM_CHANGELOG: Record = { + appkit: "CHANGELOG.md", + lakebase: "packages/lakebase/CHANGELOG.md", +}; + const changelogDiff = join(artifactsDir, "changelog-diff.md"); if (existsSync(changelogDiff)) { const diff = readFileSync(changelogDiff, "utf-8"); - const changelogPath = join(ROOT, "CHANGELOG.md"); + const changelogPath = join(ROOT, STREAM_CHANGELOG[stream] ?? "CHANGELOG.md"); if (existsSync(changelogPath)) { const existing = readFileSync(changelogPath, "utf-8");