Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 60 additions & 9 deletions .github/scripts/check_wix_proxy_steps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!/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 still 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.

The rule is bidirectional: non-publish jobs must run the proxy, and publish jobs
must not.
"""

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

# Workflows that must NOT route npm through the embargo gateway.
#
# secplatform's interim policy for OSS repos (Dima Ryskin): use embargo for
# non-publish tasks, and protect publish tasks with an enforced lockfile
# (`bun install --frozen-lockfile`) plus a package manager that honors a
# minimal-age directive (`minimumReleaseAge` in bunfig.toml) instead. Those jobs
# resolve nothing, so dropping the gateway does not widen what they can pull.
# The gateway cannot carry a publish today — `npm publish` sends
# `PUT /<package>`, which misses its `^~ /-/` passthrough block, and it sets no
# client_max_body_size so nginx's 1 MB default rejects a packument carrying the
# base64 tarball.
#
# Keyed on exact filename, and every exemption is printed on success, so this
# cannot quietly grow. Revisit once the embargo publish bug is fixed: these
# workflows should go back to being gatewayed like everything else.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets remove slop, enough to write that adding files here should not be taken easily and can pose a security concern

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 @@ -118,6 +143,20 @@ def job_problem(job: dict, workflows: frozenset[str]) -> str | None:
return _checkout_problem(steps[0])


def publish_job_problem(job: dict) -> str | None:
"""Describe why this publish job wrongly runs the proxy, or None if it abstains."""
if "uses" in job:
return None
steps = job.get("steps") or []
if any(_uses(step) == PROXY_ACTION for step in steps):
return (
"runs the Wix gateway proxy, but publish workflows must not: the gateway "
"cannot carry `npm publish`, so these workflows rely on the committed "
"lockfile plus min-release-age in .npmrc instead"
)
return None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this is really needed, maybe its enough to simply not check the publishing actions set explicitly in the list?



def _job_lines(text: str) -> dict[str, int]:
document = yaml.compose(text)
if document is None:
Expand All @@ -133,16 +172,23 @@ def main(repo_root: pathlib.Path = REPO_ROOT) -> int:
paths = sorted(p for p in workflows_dir.iterdir() if p.suffix in (".yml", ".yaml"))
workflows = frozenset(p.relative_to(repo_root).as_posix() for p in paths)
problems = []
jobs = calls = 0
jobs = calls = exempt = 0
exempt_paths = set()

for path in paths:
rel = path.relative_to(repo_root).as_posix()
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():
job = job or {}
jobs += 1
calls += "uses" in job
problem = job_problem(job, workflows)
if rel in PUBLISH_WORKFLOWS:
exempt += 1
exempt_paths.add(rel)
problem = publish_job_problem(job)
else:
jobs += 1
calls += "uses" in job
problem = job_problem(job, workflows)
if problem:
problems.append((path.relative_to(repo_root), lines[job_id], job_id, problem))

Expand All @@ -156,9 +202,14 @@ 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:
print(
f"Publish workflows exempt by policy, and verified to abstain "
f"({exempt} job(s)): " + ", ".join(sorted(exempt_paths))
)
return 0


Expand Down
64 changes: 64 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,70 @@ 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):
"""Publish workflows must abstain from the proxy; everything else must run it."""

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
""")

PUBLISH_WITH_PROXY = textwrap.dedent("""\
name: Package Preview Publish
on:
pull_request:

jobs:
publish-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Wix gateway proxy (mandatory)
uses: ./.github/actions/wix-gateway-proxy
- 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_publish_workflow_running_the_proxy_is_rejected(self):
with fixture_repo(**{"preview-publish": self.PUBLISH_WITH_PROXY}) as root:
code, output = run_main(root)

self.assertEqual(code, 1)
self.assertIn("but publish workflows must not", output)

def test_a_non_publish_workflow_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
63 changes: 37 additions & 26 deletions .github/workflows/manual-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,33 @@ on:
env:
CLI_PACKAGE_DIR: packages/cli

# This workflow deliberately does NOT run the Wix gateway proxy.
#
# secplatform's interim policy for OSS repos (Dima Ryskin): use embargo for
# non-publish tasks, and protect publish tasks with an enforced lockfile plus a
# package manager honoring a minimal-age directive — here `minimumReleaseAge` in
# bunfig.toml. The gateway cannot carry a publish today: `npm publish` sends
# `PUT /<package>`, which matches neither its `^~ /-/` passthrough block nor
# `~ \.tgz$` and so lands in `location /` (proxy_metadata, a read path with
# caching); it also sets no client_max_body_size, so nginx's 1 MB default rejects
# a packument carrying the base64 tarball. This workflow used to pin the registry
# and then strip the pin from /etc/hosts just before publishing; that hack is gone.
#
# `bun install --frozen-lockfile` below installs bun.lock verbatim and resolves
# nothing, so removing the gateway does not widen what this job can pull.
#
# check-wix-proxy.yml enforces the split: the proxy is mandatory everywhere
# except the two publish workflows, where it is forbidden.
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 +74,9 @@ 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
# setup-node resolves `.node-version` (24) to the newest 24.x, which bundles
# npm >= 11.17.0. The upgrade was also a needless registry fetch.

- name: Setup Bun
id: setup-bun
Expand All @@ -82,13 +97,13 @@ 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`:
# json-bump is declared in no manifest, so bunx fetched it from the registry
# at run time — outside bun.lock, and outside the cooldown, since bunx
# accepts --minimum-release-age without enforcing it (oven-sh/bun#30748).
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 +135,13 @@ 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) using `id-token: write`
# above — no NODE_AUTH_TOKEN/NPM_TOKEN, so no npm credential reaches the
# build. Requires a trusted publisher for `base44` registered on npmjs.com
# against this repo AND this workflow filename (the registry keys on the
# filename, so each publish workflow needs its own entry). Provenance is
# then generated automatically.
working-directory: ${{ env.CLI_PACKAGE_DIR }}
run: |
# Remove devDependencies before publish (everything is bundled)
Expand Down Expand Up @@ -190,9 +207,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
60 changes: 38 additions & 22 deletions .github/workflows/preview-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,32 @@ on:
pull_request:
types: [opened, synchronize, reopened]

# This workflow deliberately does NOT run the Wix gateway proxy.
#
# secplatform's interim policy for OSS repos (Dima Ryskin): use embargo for
# non-publish tasks, and protect publish tasks with an enforced lockfile plus a
# package manager honoring a minimal-age directive — here `minimumReleaseAge` in
# bunfig.toml. The gateway cannot carry a publish today: `npm publish` sends
# `PUT /<package>`, which matches neither its `^~ /-/` passthrough block nor
# `~ \.tgz$` and so lands in `location /` (proxy_metadata, a read path with
# caching); it also sets no client_max_body_size, so nginx's 1 MB default rejects
# a packument carrying the base64 tarball. This workflow used to pin the registry
# and then strip the pin from /etc/hosts just before publishing; that hack is gone.
#
# `bun install --frozen-lockfile` below installs bun.lock verbatim and resolves
# nothing, so removing the gateway does not widen what this job can pull.
#
# check-wix-proxy.yml enforces the split: the proxy is mandatory everywhere
# except the two publish workflows, where it is forbidden.
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 +38,15 @@ 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
# setup-node resolves `.node-version` (24) to the newest 24.x, which bundles
# npm >= 11.17.0. The upgrade was also a needless registry fetch.
- 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 +109,16 @@ 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`: json-bump is declared in no
# manifest, so bunx fetched it from the registry at run time — outside
# bun.lock, and outside the cooldown, since bunx accepts
# --minimum-release-age without enforcing it (oven-sh/bun#30748).
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 +141,13 @@ 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) using `id-token: write`
# above — NODE_AUTH_TOKEN/secrets.NPM_TOKEN removed so no npm credential
# reaches the build. Requires a trusted publisher for `@base44-preview/cli`
# registered on npmjs.com against this repo AND this workflow filename (the
# registry keys on the filename, so this workflow 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 +252,3 @@ jobs:
await createOrUpdateComment(context.issue.number);
}
}

permissions:
contents: read
pull-requests: write
id-token: write
Loading