diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 538607b5..77bae315 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: pnpm exec 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 new file mode 100644 index 00000000..bac658fc --- /dev/null +++ b/.github/workflows/prepare-release-lakebase.yml @@ -0,0 +1,105 @@ +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 + + - 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) || 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 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 + 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..bf0a2c89 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,114 @@ +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 + + - 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) || 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: pnpm exec 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 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 + 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..7c2995d0 100644 --- a/.release-it.json +++ b/.release-it.json @@ -1,33 +1,22 @@ { "$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*", + "getLatestTagFromAllRefs": true, + "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 +28,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..eaf50479 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,8 @@ 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 + - finalize-release.ts - Apply release changes (changelog, versions, tags) for secure repo ``` ## Development Commands @@ -142,52 +144,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.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..c5b4e8bd 100644 --- a/packages/lakebase/.release-it.json +++ b/packages/lakebase/.release-it.json @@ -1,39 +1,30 @@ { "$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}", + "getLatestTagFromAllRefs": true, + "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.ts b/tools/check-template-deps.ts new file mode 100644 index 00000000..f3b1f52d --- /dev/null +++ b/tools/check-template-deps.ts @@ -0,0 +1,31 @@ +#!/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. + */ + +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 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( + "Unpinned deps:", + unpinned.map(([k, v]) => `${k}@${v}`).join(", "), + ); + process.exit(1); +} diff --git a/tools/finalize-release.ts b/tools/finalize-release.ts new file mode 100644 index 00000000..d561927b --- /dev/null +++ b/tools/finalize-release.ts @@ -0,0 +1,96 @@ +#!/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 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, STREAM_CHANGELOG[stream] ?? "CHANGELOG.md"); + + if (existsSync(changelogPath)) { + const existing = readFileSync(changelogPath, "utf-8"); + const lines = existing.split("\n"); + // 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); + } + console.log("✓ changelog updated"); +} + +// 2. Bump versions +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(`✓ ${pkg}/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..5286f9df 100644 --- a/tools/publish-template-tag.ts +++ b/tools/publish-template-tag.ts @@ -1,8 +1,10 @@ #!/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"; @@ -41,7 +43,6 @@ if (templateJson.dependencies) { // 2. npm install in template (with retry for registry propagation) const MAX_ATTEMPTS = 3; const templateDir = join(ROOT, "template"); - function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -75,9 +76,12 @@ if (installExit !== 0) { // 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"]], + ["git", ["push", "origin", "HEAD", "--follow-tags"]], ]; for (const [command, args] of commands) { 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";