Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/check_wix_proxy_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# not a formality — do not do it lightly.
PUBLISH_WORKFLOWS = frozenset(
{
".github/workflows/functions-compiler-publish.yml",
".github/workflows/manual-publish.yml",
".github/workflows/preview-publish.yml",
}
Expand Down
143 changes: 143 additions & 0 deletions .github/workflows/functions-compiler-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Manual Functions Compiler Publish

on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g., 1.0.0, patch, minor, major)"
required: true
default: "patch"
type: string
npm_tag:
description: "NPM tag to publish with (e.g., latest, beta, alpha)"
required: true
default: "latest"
type: string
dry_run:
description: "Run in dry-run mode (no actual publish)"
required: false
default: false
type: boolean

env:
COMPILER_PACKAGE_DIR: packages/functions-compiler

# This workflow deliberately does NOT run the Wix gateway proxy: the gateway
# cannot carry `npm publish` (it rejects `PUT /<package>`), so per secplatform's
# interim policy for OSS repos this job relies on `--frozen-lockfile` plus
# bunfig.toml's minimumReleaseAge instead. It resolves nothing — the build is
# local (tsc plus two asset copies) and the packaging proof that does hit the
# registry (`scripts/verify-package.ts`) stays in functions-compiler.yml, behind
# the gateway, where it runs on every push to main. Exemption lives in
# .github/scripts/check_wix_proxy_steps.py.
jobs:
publish:
runs-on: ubuntu-latest
permissions:
# contents: write for the release commit and tag.
# id-token: write for npm trusted publishing (OIDC).
contents: write
id-token: write

steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2
with:
app-id: ${{ vars.BASE44_GITHUB_ACTIONS_APP_ID }}
private-key: ${{ secrets.BASE44_GITHUB_ACTIONS_APP_PRIVATE_KEY }}
owner: base44

- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version-file: ".node-version"
registry-url: "https://registry.npmjs.org"

# No `npm install -g npm@latest`: trusted publishing needs npm >= 11.5.1, and
# the npm bundled with .node-version's Node 24 is already newer.

- name: Setup Bun
id: setup-bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
# Unpinned, matching functions-compiler.yml. The pin in manual-publish.yml
# exists for the CLI's `build:binaries` cross-compile; this package builds
# with tsc and compiles nothing.
bun-version: latest

- name: Cache Bun dependencies
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Set version
working-directory: ${{ env.COMPILER_PACKAGE_DIR }}
# `--no-workspaces` keeps npm from bumping the CLI alongside this package.
# bun.lock records workspace versions but `--frozen-lockfile` tolerates the
# drift, so the bump needs no lockfile update.
run: |
npm version "${{ github.event.inputs.version }}" --no-git-tag-version --no-workspaces
echo "NEW_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV

# The version is also a literal in src/version.ts, because it goes into every
# compiled shard's banner and a host that bundles this module ships no
# package.json to read it from. `npm version` does not know about that file;
# without this step the release would publish a banner claiming the previous
# version, and leave main red on version.test.ts.
- name: Sync COMPILER_VERSION
run: bun run scripts/sync-version.ts
working-directory: ${{ env.COMPILER_PACKAGE_DIR }}

- name: Build package
run: bun run build
working-directory: ${{ env.COMPILER_PACKAGE_DIR }}

- name: Show package info
working-directory: ${{ env.COMPILER_PACKAGE_DIR }}
run: |
echo "Package name: $(node -p "require('./package.json').name")"
echo "Version: ${{ env.NEW_VERSION }}"
echo "NPM tag: ${{ github.event.inputs.npm_tag }}"
echo "Dry run: ${{ github.event.inputs.dry_run }}"

- name: Publish to NPM
# Authenticates via npm trusted publishing (OIDC), so no NPM_TOKEN reaches
# the build. Needs a trusted publisher for `@base44/functions-compiler` on
# npmjs.com registered against this repo and THIS workflow filename — the
# registry keys on the filename, so this entry is separate from the ones
# for manual-publish.yml and preview-publish.yml.
#
# Unlike the CLI, this package is not bundled: esbuild, @deno/loader and zod
# are real runtime dependencies that consumers install. Nothing is stripped
# from package.json before publish.
working-directory: ${{ env.COMPILER_PACKAGE_DIR }}
run: npm publish --tag ${{ github.event.inputs.npm_tag }} ${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }}

- name: Create Git tag
if: github.event.inputs.dry_run == 'false'
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
# Tagged `functions-compiler-v*`, not `v*`: the bare `v*` series belongs to
# the CLI and the two release trains move independently.
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git add ${{ env.COMPILER_PACKAGE_DIR }}/package.json ${{ env.COMPILER_PACKAGE_DIR }}/src/version.ts
git commit -m "chore(functions-compiler): release v${{ env.NEW_VERSION }}"
git tag functions-compiler-v${{ env.NEW_VERSION }}
git push origin HEAD:${{ github.ref }}
git push origin functions-compiler-v${{ env.NEW_VERSION }}
27 changes: 27 additions & 0 deletions packages/functions-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,30 @@ bun run build # shims + tsc -> lib/ + assets; what gets published
a sibling workspace — needs `bun run build` here first. There is deliberately
no source-resolving export condition: the tarball ships `lib/` alone, and a
second resolution path would mean two answers to "which code ran".

## Releasing

Run the **Manual Functions Compiler Publish** workflow
(`.github/workflows/functions-compiler-publish.yml`) from the Actions tab. It
bumps the version, syncs the `COMPILER_VERSION` literal, builds `lib/`,
publishes to npm, and pushes a `functions-compiler-v<version>` tag plus the
release commit.

A version lives in two files — `package.json` and the literal in
`src/version.ts` that goes into every compiled shard's banner. Bumping by hand
means running `bun run scripts/sync-version.ts` after editing `package.json`;
`version.test.ts` fails the build if the two drift apart. The CLI's own
release train is a separate workflow with its own `v<version>` tags; the two
never move together.

Authentication is npm **trusted publishing** (OIDC) — no token in the repo. The
registry keys a trusted publisher on the repo *and the workflow filename*, so
`@base44/functions-compiler` needs its own entry on npmjs.com pointing at
`functions-compiler-publish.yml`; the entries for `manual-publish.yml` do not
cover it.

The workflow only builds and publishes. What proves the tarball actually works —
`scripts/verify-package.ts`, which packs, installs the tarball into a throwaway
directory and compiles a real function there — runs in `functions-compiler.yml`
on every push to `main`, behind the Wix embargo gateway. Publish from a commit
that went green there.
5 changes: 5 additions & 0 deletions packages/functions-compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"version": "0.1.1",
"description": "Production compiler for Base44 backend functions — turns function sources into a single Cloudflare Workers module.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/base44/cli",
"directory": "packages/functions-compiler"
},
"publishConfig": {
"access": "public"
},
Expand Down
34 changes: 34 additions & 0 deletions packages/functions-compiler/scripts/sync-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Rewrites the COMPILER_VERSION literal in src/version.ts to match package.json.
//
// The version has to be a literal (see src/version.ts for why), so a release
// edits two files. `npm version` only knows about one of them, and a stale
// literal is invisible: the build succeeds, the publish succeeds, and every
// compiled shard's banner then claims a version that was never published.
// version.test.ts catches the drift, but only after the fact — this closes it.
//
// bun run scripts/sync-version.ts

import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const packageJsonPath = fileURLToPath(new URL("../package.json", import.meta.url));
const versionFilePath = fileURLToPath(new URL("../src/version.ts", import.meta.url));

const { version } = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version: string };
if (!version) throw new Error("package.json has no version");

const source = readFileSync(versionFilePath, "utf8");
const literal = /^export const COMPILER_VERSION = "(.*)";$/m;

const match = source.match(literal);
if (!match) {
// A silent no-op here would ship the exact drift this script exists to stop.
throw new Error(`no COMPILER_VERSION literal found in ${versionFilePath}`);
}

if (match[1] === version) {
console.log(`COMPILER_VERSION already ${version}`);
} else {
writeFileSync(versionFilePath, source.replace(literal, `export const COMPILER_VERSION = "${version}";`));
console.log(`COMPILER_VERSION ${match[1]} -> ${version}`);
}
2 changes: 1 addition & 1 deletion packages/functions-compiler/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
* drifts from `package.json`, so bumping the package still means editing two
* files but cannot mean forgetting one.
*/
export const COMPILER_VERSION = "0.1.0";
export const COMPILER_VERSION = "0.1.1";
Loading