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
30 changes: 25 additions & 5 deletions .github/scripts/check_wix_proxy_steps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""Fail if any GitHub Actions job skips the mandatory Wix gateway proxy action.

There is no opt-out marker by design. A job that genuinely cannot run the proxy
(a non-ubuntu runner, say) changes this script in the same PR, so the exception
gets reviewed in the open.
There is no per-job opt-out marker by design. A job that genuinely cannot run the
proxy changes this script in the same PR, so the exception gets reviewed in the
open — which is exactly how PUBLISH_WORKFLOWS below came to exist.
"""

from __future__ import annotations
Expand All @@ -19,6 +19,19 @@
# so a sparse checkout has to materialize both directories.
REQUIRED_PATHS = (".github/actions/wix-gateway-proxy", ".github/certs")

# Publish workflows, exempt from the gateway by secplatform's interim policy for
# OSS repos: the gateway cannot carry `npm publish`, so these rely on the
# committed lockfile plus bunfig.toml's minimumReleaseAge instead.
#
# Adding a file here drops its embargo protection. That is a security decision,
# not a formality — do not do it lightly.
PUBLISH_WORKFLOWS = frozenset(
{
".github/workflows/manual-publish.yml",
".github/workflows/preview-publish.yml",
}
)

FIX_HINT = """Every job must run the Wix gateway proxy immediately after a checkout that
puts it on disk, or that job's npm installs bypass the Wix embargo gateway.

Expand Down Expand Up @@ -134,8 +147,13 @@ def main(repo_root: pathlib.Path = REPO_ROOT) -> int:
workflows = frozenset(p.relative_to(repo_root).as_posix() for p in paths)
problems = []
jobs = calls = 0
exempt_paths = set()

for path in paths:
rel = path.relative_to(repo_root).as_posix()
if rel in PUBLISH_WORKFLOWS:
exempt_paths.add(rel)
continue
text = path.read_text(encoding="utf-8")
lines = _job_lines(text)
for job_id, job in ((yaml.safe_load(text) or {}).get("jobs") or {}).items():
Expand All @@ -156,9 +174,11 @@ def main(repo_root: pathlib.Path = REPO_ROOT) -> int:

print(
f"Wix gateway proxy: verified {jobs - calls} of {jobs} jobs across "
f"{len(paths)} workflows ({calls} reusable-workflow calls delegate to "
f"the workflow they call)."
f"{len(paths) - len(exempt_paths)} workflows ({calls} reusable-workflow calls "
f"delegate to the workflow they call)."
)
if exempt_paths:
print("Publish workflows exempt by policy: " + ", ".join(sorted(exempt_paths)))
return 0


Expand Down
42 changes: 42 additions & 0 deletions .github/scripts/test_check_wix_proxy_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,48 @@ def test_job_with_no_body_is_reported_as_missing_the_proxy(self):
self.assertIn('Job "build" does not run the Wix gateway proxy.', output)


class PublishExemptionTests(unittest.TestCase):
"""Workflows listed in PUBLISH_WORKFLOWS are skipped; everything else is checked."""

PUBLISH_WITHOUT_PROXY = textwrap.dedent("""\
name: Manual Package Publish
on:
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm publish
""")

def test_publish_workflow_may_omit_the_proxy(self):
with fixture_repo(**{"manual-publish": self.PUBLISH_WITHOUT_PROXY}) as root:
code, output = run_main(root)

self.assertEqual(code, 0)
self.assertIn("exempt by policy", output)
self.assertIn(".github/workflows/manual-publish.yml", output)

def test_exempt_jobs_are_not_counted_as_verified(self):
with fixture_repo(
**{"manual-publish": self.PUBLISH_WITHOUT_PROXY, "good": COMPLIANT_WORKFLOW}
) as root:
code, output = run_main(root)

self.assertEqual(code, 0)
self.assertIn("verified 1 of 1 jobs across 1 workflows", output)

def test_a_workflow_not_on_the_list_still_needs_the_proxy(self):
with fixture_repo(**{"some-publish-helper": self.PUBLISH_WITHOUT_PROXY}) as root:
code, output = run_main(root)

self.assertEqual(code, 1)
self.assertIn("does not run the Wix gateway proxy", output)


class RepositoryTests(unittest.TestCase):
def test_every_job_in_this_repository_runs_the_proxy(self):
self.assertEqual(checker.main(), 0)
Expand Down
47 changes: 21 additions & 26 deletions .github/workflows/manual-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@ on:
env:
CLI_PACKAGE_DIR: packages/cli

# 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, so dropping the
# gateway does not widen what it can pull. Exemption lives in
# .github/scripts/check_wix_proxy_steps.py.
jobs:
publish:
runs-on: ubuntu-latest
permissions:
# contents: write for the release commit, tag, and GitHub Release.
# id-token: write for npm trusted publishing (OIDC).
contents: write
id-token: write

steps:
- name: Checkout for wix gateway proxy
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
sparse-checkout: .github

- name: Wix gateway proxy (mandatory)
uses: ./.github/actions/wix-gateway-proxy

- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v2
Expand All @@ -60,8 +63,8 @@ jobs:
node-version-file: ".node-version"
registry-url: "https://registry.npmjs.org"

- name: Update npm
run: npm install -g npm@latest
# 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
Expand All @@ -82,13 +85,11 @@ jobs:

- name: Set version
working-directory: ${{ env.CLI_PACKAGE_DIR }}
# `npm version` takes both a keyword (patch/minor/major) and an explicit
# version, so it replaces the old branch. It also replaces `bunx json-bump`,
# which fetched an undeclared package at run time, outside the cooldown.
run: |
VERSION_INPUT="${{ github.event.inputs.version }}"
if [[ "$VERSION_INPUT" =~ ^(patch|minor|major)$ ]]; then
bunx json-bump package.json --$VERSION_INPUT
else
bunx json-bump package.json --replace="$VERSION_INPUT"
fi
npm version "${{ github.event.inputs.version }}" --no-git-tag-version --no-workspaces
echo "NEW_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV

- name: Build package
Expand Down Expand Up @@ -120,11 +121,11 @@ jobs:
echo "NPM tag: ${{ github.event.inputs.npm_tag }}"
echo "Dry run: ${{ github.event.inputs.dry_run }}"

- name: Unpin npm registry for first-party publish
# The embargo would refuse the just-built version; installs above stayed gatewayed.
run: sudo sed -i '/registry\.npmjs\.org/d' /etc/hosts

- name: Publish to NPM
# Authenticates via npm trusted publishing (OIDC), so no NPM_TOKEN reaches
# the build. Needs a trusted publisher for `base44` on npmjs.com registered
# against this repo and this workflow filename — the registry keys on the
# filename, so each publish workflow needs its own entry.
working-directory: ${{ env.CLI_PACKAGE_DIR }}
run: |
# Remove devDependencies before publish (everything is bundled)
Expand Down Expand Up @@ -190,9 +191,3 @@ jobs:
"release_url": "${{ env.RELEASE_URL }}",
"release_name": "Release v${{ env.NEW_VERSION }}"
}

permissions:
contents: write
packages: write
pull-requests: read
id-token: write
45 changes: 23 additions & 22 deletions .github/workflows/preview-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@ on:
pull_request:
types: [opened, synchronize, reopened]

# 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, so dropping the
# gateway does not widen what it can pull. Exemption lives in
# .github/scripts/check_wix_proxy_steps.py.
jobs:
publish-preview:
runs-on: ubuntu-latest
permissions:
# id-token: write for npm trusted publishing (OIDC).
# pull-requests: write for the install-instructions comment.
contents: read
id-token: write
pull-requests: write
defaults:
run:
working-directory: packages/cli
Expand All @@ -15,19 +27,14 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Wix gateway proxy (mandatory)
uses: ./.github/actions/wix-gateway-proxy

# 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 Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".node-version"
registry-url: "https://registry.npmjs.org"

- name: Update npm
run: npm install -g npm@latest
working-directory: .

- name: Setup Bun
id: setup-bun
uses: oven-sh/setup-bun@v2
Expand Down Expand Up @@ -90,14 +97,14 @@ jobs:
exit 1
fi

# Update name with error handling
if ! bunx json-bump package.json --entry=name --replace="$PREVIEW_PACKAGE"; then
# `npm pkg set` replaces `bunx json-bump`, which fetched an undeclared
# package at run time, outside the cooldown.
if ! npm pkg set name="$PREVIEW_PACKAGE"; then
echo "❌ ERROR: Failed to set package name to $PREVIEW_PACKAGE"
exit 1
fi

# Update version with error handling
if ! bunx json-bump package.json --replace="${{ steps.preview_info.outputs.version }}"; then
if ! npm pkg set version="${{ steps.preview_info.outputs.version }}"; then
echo "❌ ERROR: Failed to set package version to ${{ steps.preview_info.outputs.version }}"
exit 1
fi
Expand All @@ -120,13 +127,12 @@ jobs:

echo "✅ Safety check passed. Package name is safe to publish."

- name: Unpin npm registry for first-party publish
# The embargo would refuse the just-built version; installs above stayed gatewayed.
run: sudo sed -i '/registry\.npmjs\.org/d' /etc/hosts

- name: Publish preview package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Authenticates via npm trusted publishing (OIDC), so no NPM_TOKEN reaches
# the build. Needs a trusted publisher for `@base44-preview/cli` on
# npmjs.com registered against this repo and this workflow filename — the
# registry keys on the filename, so this needs its own entry, separate from
# manual-publish.yml.
run: |
# Remove devDependencies before publish (everything is bundled)
jq 'del(.devDependencies)' package.json > package.json.tmp && mv package.json.tmp package.json
Expand Down Expand Up @@ -231,8 +237,3 @@ jobs:
await createOrUpdateComment(context.issue.number);
}
}

permissions:
contents: read
pull-requests: write
id-token: write
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ error-reports/
# Deno lockfile — generated when running deno against backend-runtime/ or fixtures.
# The repo uses Bun for dependencies; this is a local artifact.
deno.lock

# Python bytecode — from running the .github/scripts checks locally.
__pycache__/
*.pyc
7 changes: 7 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[install]
# Supply-chain cooldown: Bun resolves only versions published at least this long
# ago, so a compromised release cannot enter bun.lock inside the window. Seconds,
# unlike npm's min-release-age (days). Applies to `bun add`/`bun update`, not to
# `bun install --frozen-lockfile` (resolves nothing) and not to `bunx`, which
# accepts the flag without enforcing it (oven-sh/bun#30748).
minimumReleaseAge = 1209600 # 14 days
2 changes: 1 addition & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ bun run lint:fix # Biome - auto-fix

These apply to every task. See topic guides below for domain-specific rules.

1. **Bun for everything** - Use `bun` commands for install, test, build, run
1. **Bun for everything** - Use `bun` commands for install, test, build, run. `bunfig.toml` sets a 14-day `minimumReleaseAge` supply-chain cooldown on `bun add`/`bun update`, so a freshly published version cannot enter `bun.lock`. Never fetch tooling with `bunx` in a release path — it ignores the cooldown ([oven-sh/bun#30748](https://github.com/oven-sh/bun/issues/30748))
2. **Zod validation** - Required for all external data (API responses, config files)
3. **@clack/prompts only** - For all user interaction (prompts, spinners, logs). No `console.log`. Under the global `--json` flag the lifecycle runs **silent** (prompts and spinners suppressed, logs routed to stderr) — never assume a TTY
4. **ES Modules** - Use `.js` extensions in all imports
Expand Down