Skip to content
Open
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
46 changes: 46 additions & 0 deletions .github/actions/wix-gateway-proxy/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: wix gateway proxy
description: >-
Mandatory first step of every job: routes registry.npmjs.org through the
Wix npm embargo gateway by pinning the registry hostname to the gateway IP,
trusting the gateway CA system-wide, and pointing npm at the CA bundle.
Being a local action, the repository must be checked out before it runs.
Enforced for every job by .github/workflows/check-wix-proxy.yml.

inputs:
proxy-ip:
description: IP address of the Wix npm embargo gateway
required: false
default: "23.21.39.196"

runs:
using: composite
steps:
- name: Point registry.npmjs.org at the gateway
shell: bash
run: |
echo "${{ inputs.proxy-ip }} registry.npmjs.org" | sudo tee -a /etc/hosts
getent hosts registry.npmjs.org

- name: Trust the gateway CA
shell: bash
run: |
sudo cp "$GITHUB_ACTION_PATH/../../certs/wix-embargo.pem" /usr/local/share/ca-certificates/wix-embargo.crt
sudo update-ca-certificates

- name: Trust the gateway CA in Node
# Node ignores the OS trust store; NODE_EXTRA_CA_CERTS covers npm, pnpm,
# corepack, and npx. Never use `npm config set cafile` here — it REPLACES
# the trust bundle and breaks other registries (npm.jsr.io), whereas
# NODE_EXTRA_CA_CERTS appends. The /usr/local/share copy survives later
# checkouts and workspace cleans, unlike the in-workspace pem.
shell: bash
run: echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/wix-embargo.crt" >> "$GITHUB_ENV"

- name: Trust the gateway CA in Deno
# Deno (rustls) reads neither the OS store nor NODE_EXTRA_CA_CERTS by
# default; "system" points it at the OS store where the gateway CA is
# installed, "mozilla" keeps public roots for every other host.
# NOTE: rustls still rejects the gateway until it serves a CA-signed
# leaf cert instead of the CA cert itself (CaUsedAsEndEntity).
shell: bash
run: echo "DENO_TLS_CA_STORE=system,mozilla" >> "$GITHUB_ENV"
23 changes: 23 additions & 0 deletions .github/certs/wix-embargo.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDzjCCAragAwIBAgIUVpbbERZ/oqaGxJAznohpw7hc/9swDQYJKoZIhvcNAQEL
BQAwZzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCFNlY3VyaXR5MQ0wCwYDVQQHDARN
SVRNMRkwFwYDVQQKDBBOUE0gUG9saWN5IFByb3h5MRswGQYDVQQDDBJyZWdpc3Ry
eS5ucG1qcy5vcmcwHhcNMjYwMTE0MTUwMDQwWhcNMjcwMTE0MTUwMDQwWjBnMQsw
CQYDVQQGEwJVUzERMA8GA1UECAwIU2VjdXJpdHkxDTALBgNVBAcMBE1JVE0xGTAX
BgNVBAoMEE5QTSBQb2xpY3kgUHJveHkxGzAZBgNVBAMMEnJlZ2lzdHJ5Lm5wbWpz
Lm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKBX6fToCIZ4mSvQ
02/sSVh8xn82DHXW33a5MpVRqMTNa9r7S5TeCLJTmdfJQYi1GYnjRjgIiUK7UwBn
cmyV4fK9PACGsdzmstbGsBQNBmLrCNpFIPG96EmE7u93CyZgVQzzoM0WxTkKmCFR
e4WbhsY2DGSvOQwa/Bj9rusEb3WwHvbzDLfF2BOfiauizHl6dVdRcj8xmN748Kxp
H2AZ3Q3sTe3sfeJVkFKbQ61JBE8YYLe5PtXwZGsumT6K6MjRtlFeXK59D9pXJKmC
rS3zVsExx/NaZmqXUBx/ikdStBbTNEqAkuEHXMArAZYMXXCEbPChCkn/pDCvBofV
S/WAvnUCAwEAAaNyMHAwHQYDVR0OBBYEFG9FHU6u08PGgzDhY3X6RSnUXz1OMB8G
A1UdIwQYMBaAFG9FHU6u08PGgzDhY3X6RSnUXz1OMA8GA1UdEwEB/wQFMAMBAf8w
HQYDVR0RBBYwFIIScmVnaXN0cnkubnBtanMub3JnMA0GCSqGSIb3DQEBCwUAA4IB
AQAG6u7BXGfLe+VttUVZYbqOV3uW0skwAsjK6wcbwW7WEKk0k5oJRCbRGYzSe7hc
fRXkezENYfgsWJOXhOrIm89F2e0dbJTNmefFzS+56RRllHmBEQxxI2f446qBx4w0
/N6QqpE1QxzuvAJDs/wki3CMsnz5Eu3IdM/1Els6Ap794xBXJAwgh7fNa0V5NMLT
hknLtKy6nu1nfoyfZ9fTLr8IKuEfB6vYJ00FDVyVmWGfXam5yAfL8uhCQcBd1AI9
pRHzoBDRNqIWaRF2lWQCWV4pt132oxln0n3iG/TSodbAXp3WFJaKQ0HwXSb5NXjI
nbDI4K0FXvrAhfXRomBSQ3WM
-----END CERTIFICATE-----
166 changes: 166 additions & 0 deletions .github/scripts/check_wix_proxy_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/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.
"""

from __future__ import annotations

import pathlib
import sys

import yaml

REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
PROXY_ACTION = "./.github/actions/wix-gateway-proxy"
# The action copies .github/certs/wix-embargo.pem via a path relative to itself,
# so a sparse checkout has to materialize both directories.
REQUIRED_PATHS = (".github/actions/wix-gateway-proxy", ".github/certs")

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.

Job that already checks out this repo first:

- uses: actions/checkout@v7

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

Job with no leading same-repo checkout -- prepend a bootstrap one. The action
installs the CA under /usr/local/share, so a later full checkout does not undo it:

- 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

Non-cone sparse checkout -- list both paths explicitly:

with:
sparse-checkout: |
<your existing paths>
.github/actions/wix-gateway-proxy
.github/certs
sparse-checkout-cone-mode: false
"""


def _uses(step: dict) -> str:
return str(step.get("uses", "")).strip().rstrip("/")


def _covers(pattern: str, path: str) -> bool:
return path == pattern or path.startswith(pattern + "/")


def _overlaps(pattern: str, path: str) -> bool:
# An exclusion breaks the action whether it removes the whole directory or a
# single file inside it, so overlap in either direction disqualifies.
return _covers(pattern, path) or _covers(path, pattern)


def _sparse_covers_action(patterns: str) -> bool:
listed = [p.strip().rstrip("/") for p in patterns.splitlines() if p.strip()]
included = [p for p in listed if not p.startswith("!")]
excluded = [p[1:] for p in listed if p.startswith("!")]
return all(
any(_covers(p, required) for p in included)
and not any(_overlaps(p, required) for p in excluded)
for required in REQUIRED_PATHS
)


def _checkout_problem(step: dict) -> str | None:
if not _uses(step).startswith("actions/checkout"):
return f'is preceded by "{_uses(step) or "a run step"}" instead of a checkout'
with_ = step.get("with") or {}
if with_.get("repository"):
return f'is preceded by a checkout of {with_["repository"]}'
if with_.get("path"):
return f'is preceded by a checkout into {with_["path"]}/, not the workspace root'
patterns = with_.get("sparse-checkout")
if patterns and not _sparse_covers_action(str(patterns)):
return "is preceded by a sparse checkout that omits " + " or ".join(REQUIRED_PATHS)
return None


def job_problem(job: dict, workflows: frozenset[str]) -> str | None:
"""Describe why this job fails to run the proxy, or None if it runs it correctly."""
if "uses" in job:
target = str(job["uses"]).strip()
if target.removeprefix("./") in workflows:
return None
return f"calls {target}, whose jobs this check cannot verify"

steps = job.get("steps") or []
index = next((i for i, s in enumerate(steps) if _uses(s) == PROXY_ACTION), None)
if index is None:
return "does not run the Wix gateway proxy"
if index == 0:
return "runs the Wix gateway proxy first, but a local action needs a checkout before it"
if index > 1:
return f"runs the Wix gateway proxy at step {index + 1}; it must be step 2"
step = steps[index]
# Presence, not truthiness: `if: false` parses to False and would skip the step.
if "if" in step:
return "guards the Wix gateway proxy behind an if:, but it is mandatory"
if step.get("continue-on-error"):
return "lets the Wix gateway proxy fail via continue-on-error, but it is mandatory"
with_ = step.get("with") or {}
if "proxy-ip" in with_ and not str(with_["proxy-ip"]).strip():
return "passes an empty proxy-ip, leaving registry.npmjs.org resolving publicly"
return _checkout_problem(steps[0])


def _job_lines(text: str) -> dict[str, int]:
document = yaml.compose(text)
if document is None:
return {}
for key, value in document.value:
if key.value == "jobs":
return {job.value: job.start_mark.line + 1 for job, _ in value.value}
return {}


def main(repo_root: pathlib.Path = REPO_ROOT) -> int:
workflows_dir = repo_root / ".github" / "workflows"
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

for path in paths:
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 problem:
problems.append((path.relative_to(repo_root), lines[job_id], job_id, problem))

for path, line, job_id, problem in problems:
print(f'::error file={path},line={line}::Job "{job_id}" {problem}.')

if problems:
print(f"\n{len(problems)} job(s) skip the Wix gateway proxy.\n")
print(FIX_HINT)
return 1

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)."
)
return 0


if __name__ == "__main__":
sys.exit(main())
78 changes: 78 additions & 0 deletions .github/scripts/requirements-check-wix-proxy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# PyYAML for .github/scripts/check_wix_proxy_steps.py, hash-pinned so the
# supply-chain guard does not itself install an unverified package.
# PyYAML has no dependencies; hashes are PyPI's published sha256 set for
# pyyaml 6.0.3, ported verbatim from base44-dev/vite-plugin.
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
Loading