From 62b02b04e198eb72cb56284752a97dbd4f477172 Mon Sep 17 00:00:00 2001 From: Mark DeLaVergne Date: Tue, 18 Aug 2026 14:36:20 -0400 Subject: [PATCH 1/6] Fix SF CLI integration mock server --- .github/workflows/sf_cli_integration.yml | 17 +++++++-- scripts/mock_sf_server.py | 44 +++++++++++++++++++++--- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sf_cli_integration.yml b/.github/workflows/sf_cli_integration.yml index f3c5fb3..7580095 100644 --- a/.github/workflows/sf_cli_integration.yml +++ b/.github/workflows/sf_cli_integration.yml @@ -10,6 +10,12 @@ jobs: env: SF_AUTOUPDATE_DISABLE: true NO_COLOR: '1' + # The mock server serves TLS with a self-signed cert written to this path. + # Point both the CLI (Node) and the SDK (Python requests) at it so they + # trust the specific cert rather than disabling verification wholesale. + MOCK_SF_CERT_FILE: ${{ github.workspace }}/mock_sf_cert.pem + NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem + REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem steps: # ── Setup ───────────────────────────────────────────────────────────────── @@ -56,7 +62,14 @@ jobs: # ── Mock Salesforce server + fake org auth ──────────────────────────────── - name: Start mock Salesforce server - run: python scripts/mock_sf_server.py & + run: | + python scripts/mock_sf_server.py & + # Wait for the TLS cert the server writes at startup so clients can trust it. + for _ in $(seq 1 30); do + [ -f "$MOCK_SF_CERT_FILE" ] && break + sleep 0.2 + done + test -f "$MOCK_SF_CERT_FILE" || { echo "::error::mock server never wrote $MOCK_SF_CERT_FILE"; exit 1; } env: MOCK_SF_PORT: '8888' @@ -73,7 +86,7 @@ jobs: sfdx_dir.mkdir(exist_ok=True) auth = { "accessToken": "00D000000000001AAA!fakeTokenForCITesting", - "instanceUrl": "http://localhost:8888", + "instanceUrl": "https://localhost:8888", "loginUrl": "https://login.salesforce.com", "orgId": "00D000000000001AAA", "userId": "005000000000001AAA", diff --git a/scripts/mock_sf_server.py b/scripts/mock_sf_server.py index b296577..0fa279e 100644 --- a/scripts/mock_sf_server.py +++ b/scripts/mock_sf_server.py @@ -40,14 +40,21 @@ python scripts/mock_sf_server.py # listens on port 8888 MOCK_SF_PORT=9000 python scripts/mock_sf_server.py python scripts/mock_sf_server.py 9000 + +Serves TLS with a throwaway self-signed cert (the deploy path requires an HTTPS +upload URL). Set ``MOCK_SF_CERT_FILE`` to a path the clients can trust via +``NODE_EXTRA_CA_CERTS`` (CLI) and ``REQUESTS_CA_BUNDLE`` (SDK). """ from __future__ import annotations -from http.server import BaseHTTPRequestHandler, HTTPServer import json import os +import ssl +import subprocess import sys +import tempfile +from http.server import BaseHTTPRequestHandler, HTTPServer PORT = ( int(sys.argv[1]) @@ -55,6 +62,28 @@ else int(os.environ.get("MOCK_SF_PORT", "8888")) ) + +def _self_signed_cert(dirpath: str) -> tuple[str, str]: + """Generate a throwaway self-signed cert for localhost via openssl. + + The plugin's deploy path requires an HTTPS upload URL, so the server must + speak TLS. Set ``MOCK_SF_CERT_FILE`` to write the cert to a known path so + clients can trust it (``REQUESTS_CA_BUNDLE`` / ``NODE_EXTRA_CA_CERTS``). + """ + cert_path = os.environ.get("MOCK_SF_CERT_FILE") or os.path.join(dirpath, "cert.pem") + key_path = os.path.join(dirpath, "key.pem") + subprocess.run( + [ + "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", key_path, "-out", cert_path, "-days", "1", + "-subj", "/CN=localhost", + "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", + ], + check=True, + capture_output=True, + ) + return cert_path, key_path + _USERINFO = { "sub": "https://test.salesforce.com/id/00D000000000001AAA/005000000000001AAA", "user_id": "005000000000001AAA", @@ -68,7 +97,7 @@ _TOKEN_RESPONSE = { "access_token": "00D000000000001AAA!fakeAccessTokenForCITesting", - "instance_url": f"http://localhost:{PORT}", + "instance_url": f"https://localhost:{PORT}", "token_type": "Bearer", "scope": "api", } @@ -135,7 +164,7 @@ def do_POST(self) -> None: elif path == _DATA_CUSTOM_CODE_PATH: # create_deployment() — return a presigned upload URL self._send_json( - {"fileUploadUrl": f"http://localhost:{PORT}/upload/fake-deployment.zip"} + {"fileUploadUrl": f"https://localhost:{PORT}/upload/fake-deployment.zip"} ) elif path == _DATA_TRANSFORMS_PATH: # create_data_transform() — script packages only @@ -152,5 +181,10 @@ def do_PUT(self) -> None: if __name__ == "__main__": server = HTTPServer(("localhost", PORT), MockSFHandler) server.allow_reuse_address = True - print(f"[MOCK SF] Listening on http://localhost:{PORT}", flush=True) - server.serve_forever() + with tempfile.TemporaryDirectory() as certdir: + cert_path, key_path = _self_signed_cert(certdir) + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(cert_path, key_path) + server.socket = ctx.wrap_socket(server.socket, server_side=True) + print(f"[MOCK SF] Listening on https://localhost:{PORT}", flush=True) + server.serve_forever() From 9fe6d768804ef51e3addd44cc6456e8952d7dca1 Mon Sep 17 00:00:00 2001 From: Mark DeLaVergne Date: Tue, 18 Aug 2026 14:42:07 -0400 Subject: [PATCH 2/6] Selective fix for SF CLI mock server --- .github/workflows/sf_cli_integration.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sf_cli_integration.yml b/.github/workflows/sf_cli_integration.yml index 7580095..d309d0e 100644 --- a/.github/workflows/sf_cli_integration.yml +++ b/.github/workflows/sf_cli_integration.yml @@ -10,12 +10,11 @@ jobs: env: SF_AUTOUPDATE_DISABLE: true NO_COLOR: '1' - # The mock server serves TLS with a self-signed cert written to this path. - # Point both the CLI (Node) and the SDK (Python requests) at it so they - # trust the specific cert rather than disabling verification wholesale. + # Path the mock server writes its self-signed TLS cert to. The CA-trust + # env vars (NODE_EXTRA_CA_CERTS / REQUESTS_CA_BUNDLE) are set per-step on + # only the run/deploy steps — setting them job-wide would point pip/poetry + # at a cert file that does not exist yet during setup, breaking installs. MOCK_SF_CERT_FILE: ${{ github.workspace }}/mock_sf_cert.pem - NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem - REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem steps: # ── Setup ───────────────────────────────────────────────────────────────── @@ -177,6 +176,9 @@ jobs: # ── Script: run ─────────────────────────────────────────────────────────── - name: '[script] run — sf data-code-extension script run --entrypoint testScript/payload/entrypoint.py -o dev1' + env: + NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem + REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem run: | sf data-code-extension script run \ --entrypoint testScript/payload/entrypoint.py \ @@ -188,6 +190,9 @@ jobs: # ── Script: deploy ─────────────────────────────────────────────────────── - name: '[script] deploy — sf data-code-extension script deploy' + env: + NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem + REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem run: | sf data-code-extension script deploy \ --name test-script-deploy \ @@ -275,6 +280,9 @@ jobs: # ── Function: run ───────────────────────────────────────────────────────── - name: '[function] run — sf data-code-extension function run --entrypoint testFunction/payload/entrypoint.py --test-with testFunction/payload/tests/test.json -o dev1' + env: + NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem + REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem run: | sf data-code-extension function run \ --entrypoint testFunction/payload/entrypoint.py \ @@ -286,6 +294,9 @@ jobs: # ── Function: deploy ───────────────────────────────────────────────────── - name: '[function] deploy — sf data-code-extension function deploy' + env: + NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem + REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem run: | sf data-code-extension function deploy \ --name test-function-deploy \ From e35cdb1e44544c01359147070442c3f166516b3a Mon Sep 17 00:00:00 2001 From: Mark DeLaVergne Date: Tue, 18 Aug 2026 14:58:46 -0400 Subject: [PATCH 3/6] Fix lint issues --- scripts/mock_sf_server.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/scripts/mock_sf_server.py b/scripts/mock_sf_server.py index 0fa279e..0f48d5d 100644 --- a/scripts/mock_sf_server.py +++ b/scripts/mock_sf_server.py @@ -48,13 +48,13 @@ from __future__ import annotations +from http.server import BaseHTTPRequestHandler, HTTPServer import json import os import ssl import subprocess import sys import tempfile -from http.server import BaseHTTPRequestHandler, HTTPServer PORT = ( int(sys.argv[1]) @@ -74,16 +74,29 @@ def _self_signed_cert(dirpath: str) -> tuple[str, str]: key_path = os.path.join(dirpath, "key.pem") subprocess.run( [ - "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", - "-keyout", key_path, "-out", cert_path, "-days", "1", - "-subj", "/CN=localhost", - "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", + "openssl", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + key_path, + "-out", + cert_path, + "-days", + "1", + "-subj", + "/CN=localhost", + "-addext", + "subjectAltName=DNS:localhost,IP:127.0.0.1", ], check=True, capture_output=True, ) return cert_path, key_path + _USERINFO = { "sub": "https://test.salesforce.com/id/00D000000000001AAA/005000000000001AAA", "user_id": "005000000000001AAA", @@ -164,7 +177,9 @@ def do_POST(self) -> None: elif path == _DATA_CUSTOM_CODE_PATH: # create_deployment() — return a presigned upload URL self._send_json( - {"fileUploadUrl": f"https://localhost:{PORT}/upload/fake-deployment.zip"} + { + "fileUploadUrl": f"https://localhost:{PORT}/upload/fake-deployment.zip" + } ) elif path == _DATA_TRANSFORMS_PATH: # create_data_transform() — script packages only From 650be6bcdf554658be141a69cf7dd710f53a97b4 Mon Sep 17 00:00:00 2001 From: Mark DeLaVergne Date: Wed, 19 Aug 2026 10:28:25 -0400 Subject: [PATCH 4/6] Generate mock certs as a dedicated step --- .github/workflows/sf_cli_integration.yml | 38 ++++++------- scripts/mock_sf_server.py | 70 ++++++++---------------- 2 files changed, 41 insertions(+), 67 deletions(-) diff --git a/.github/workflows/sf_cli_integration.yml b/.github/workflows/sf_cli_integration.yml index d309d0e..7dbf65b 100644 --- a/.github/workflows/sf_cli_integration.yml +++ b/.github/workflows/sf_cli_integration.yml @@ -10,11 +10,9 @@ jobs: env: SF_AUTOUPDATE_DISABLE: true NO_COLOR: '1' - # Path the mock server writes its self-signed TLS cert to. The CA-trust - # env vars (NODE_EXTRA_CA_CERTS / REQUESTS_CA_BUNDLE) are set per-step on - # only the run/deploy steps — setting them job-wide would point pip/poetry - # at a cert file that does not exist yet during setup, breaking installs. + # Mock server's TLS cert/key pair. MOCK_SF_CERT_FILE: ${{ github.workspace }}/mock_sf_cert.pem + MOCK_SF_KEY_FILE: ${{ github.workspace }}/mock_sf_key.pem steps: # ── Setup ───────────────────────────────────────────────────────────────── @@ -22,6 +20,13 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Generate mock server TLS cert + run: | + openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout "$MOCK_SF_KEY_FILE" -out "$MOCK_SF_CERT_FILE" \ + -days 1 -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + - name: Set up Python 3.11 uses: actions/setup-python@v5 with: @@ -61,14 +66,7 @@ jobs: # ── Mock Salesforce server + fake org auth ──────────────────────────────── - name: Start mock Salesforce server - run: | - python scripts/mock_sf_server.py & - # Wait for the TLS cert the server writes at startup so clients can trust it. - for _ in $(seq 1 30); do - [ -f "$MOCK_SF_CERT_FILE" ] && break - sleep 0.2 - done - test -f "$MOCK_SF_CERT_FILE" || { echo "::error::mock server never wrote $MOCK_SF_CERT_FILE"; exit 1; } + run: python scripts/mock_sf_server.py & env: MOCK_SF_PORT: '8888' @@ -177,8 +175,8 @@ jobs: - name: '[script] run — sf data-code-extension script run --entrypoint testScript/payload/entrypoint.py -o dev1' env: - NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem - REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem + NODE_EXTRA_CA_CERTS: ${{ env.MOCK_SF_CERT_FILE }} + REQUESTS_CA_BUNDLE: ${{ env.MOCK_SF_CERT_FILE }} run: | sf data-code-extension script run \ --entrypoint testScript/payload/entrypoint.py \ @@ -191,8 +189,8 @@ jobs: - name: '[script] deploy — sf data-code-extension script deploy' env: - NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem - REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem + NODE_EXTRA_CA_CERTS: ${{ env.MOCK_SF_CERT_FILE }} + REQUESTS_CA_BUNDLE: ${{ env.MOCK_SF_CERT_FILE }} run: | sf data-code-extension script deploy \ --name test-script-deploy \ @@ -281,8 +279,8 @@ jobs: - name: '[function] run — sf data-code-extension function run --entrypoint testFunction/payload/entrypoint.py --test-with testFunction/payload/tests/test.json -o dev1' env: - NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem - REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem + NODE_EXTRA_CA_CERTS: ${{ env.MOCK_SF_CERT_FILE }} + REQUESTS_CA_BUNDLE: ${{ env.MOCK_SF_CERT_FILE }} run: | sf data-code-extension function run \ --entrypoint testFunction/payload/entrypoint.py \ @@ -295,8 +293,8 @@ jobs: - name: '[function] deploy — sf data-code-extension function deploy' env: - NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/mock_sf_cert.pem - REQUESTS_CA_BUNDLE: ${{ github.workspace }}/mock_sf_cert.pem + NODE_EXTRA_CA_CERTS: ${{ env.MOCK_SF_CERT_FILE }} + REQUESTS_CA_BUNDLE: ${{ env.MOCK_SF_CERT_FILE }} run: | sf data-code-extension function deploy \ --name test-function-deploy \ diff --git a/scripts/mock_sf_server.py b/scripts/mock_sf_server.py index 0f48d5d..8012f89 100644 --- a/scripts/mock_sf_server.py +++ b/scripts/mock_sf_server.py @@ -41,9 +41,16 @@ MOCK_SF_PORT=9000 python scripts/mock_sf_server.py python scripts/mock_sf_server.py 9000 -Serves TLS with a throwaway self-signed cert (the deploy path requires an HTTPS -upload URL). Set ``MOCK_SF_CERT_FILE`` to a path the clients can trust via -``NODE_EXTRA_CA_CERTS`` (CLI) and ``REQUESTS_CA_BUNDLE`` (SDK). +Serves TLS (the deploy path requires an HTTPS upload URL) using a pre-generated +cert/key pair — this script does not generate one. Set ``MOCK_SF_CERT_FILE`` / +``MOCK_SF_KEY_FILE`` to the pair's paths; generate a throwaway one with: + + openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \\ + -days 1 -subj "/CN=localhost" \\ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + +Point clients at the cert so they trust it: ``NODE_EXTRA_CA_CERTS`` (CLI) and +``REQUESTS_CA_BUNDLE`` (SDK). """ from __future__ import annotations @@ -52,9 +59,7 @@ import json import os import ssl -import subprocess import sys -import tempfile PORT = ( int(sys.argv[1]) @@ -62,41 +67,6 @@ else int(os.environ.get("MOCK_SF_PORT", "8888")) ) - -def _self_signed_cert(dirpath: str) -> tuple[str, str]: - """Generate a throwaway self-signed cert for localhost via openssl. - - The plugin's deploy path requires an HTTPS upload URL, so the server must - speak TLS. Set ``MOCK_SF_CERT_FILE`` to write the cert to a known path so - clients can trust it (``REQUESTS_CA_BUNDLE`` / ``NODE_EXTRA_CA_CERTS``). - """ - cert_path = os.environ.get("MOCK_SF_CERT_FILE") or os.path.join(dirpath, "cert.pem") - key_path = os.path.join(dirpath, "key.pem") - subprocess.run( - [ - "openssl", - "req", - "-x509", - "-newkey", - "rsa:2048", - "-nodes", - "-keyout", - key_path, - "-out", - cert_path, - "-days", - "1", - "-subj", - "/CN=localhost", - "-addext", - "subjectAltName=DNS:localhost,IP:127.0.0.1", - ], - check=True, - capture_output=True, - ) - return cert_path, key_path - - _USERINFO = { "sub": "https://test.salesforce.com/id/00D000000000001AAA/005000000000001AAA", "user_id": "005000000000001AAA", @@ -194,12 +164,18 @@ def do_PUT(self) -> None: if __name__ == "__main__": + cert_path = os.environ.get("MOCK_SF_CERT_FILE") + key_path = os.environ.get("MOCK_SF_KEY_FILE") + if not cert_path or not key_path: + sys.exit( + "MOCK_SF_CERT_FILE and MOCK_SF_KEY_FILE must both be set to an " + "existing TLS cert/key pair — see the module docstring." + ) + server = HTTPServer(("localhost", PORT), MockSFHandler) server.allow_reuse_address = True - with tempfile.TemporaryDirectory() as certdir: - cert_path, key_path = _self_signed_cert(certdir) - ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - ctx.load_cert_chain(cert_path, key_path) - server.socket = ctx.wrap_socket(server.socket, server_side=True) - print(f"[MOCK SF] Listening on https://localhost:{PORT}", flush=True) - server.serve_forever() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(cert_path, key_path) + server.socket = ctx.wrap_socket(server.socket, server_side=True) + print(f"[MOCK SF] Listening on https://localhost:{PORT}", flush=True) + server.serve_forever() From a847b20dbf196a9948401a18b40d25743b04fa34 Mon Sep 17 00:00:00 2001 From: Mark DeLaVergne Date: Wed, 19 Aug 2026 10:31:30 -0400 Subject: [PATCH 5/6] Utilize temp dir instead of workspace --- .github/workflows/sf_cli_integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sf_cli_integration.yml b/.github/workflows/sf_cli_integration.yml index 7dbf65b..d2bde49 100644 --- a/.github/workflows/sf_cli_integration.yml +++ b/.github/workflows/sf_cli_integration.yml @@ -11,8 +11,8 @@ jobs: SF_AUTOUPDATE_DISABLE: true NO_COLOR: '1' # Mock server's TLS cert/key pair. - MOCK_SF_CERT_FILE: ${{ github.workspace }}/mock_sf_cert.pem - MOCK_SF_KEY_FILE: ${{ github.workspace }}/mock_sf_key.pem + MOCK_SF_CERT_FILE: ${{ runner.temp }}/mock_sf_cert.pem + MOCK_SF_KEY_FILE: ${{ runner.temp }}/mock_sf_key.pem steps: # ── Setup ───────────────────────────────────────────────────────────────── From 448188d7d573d796f69e1f87bf5dc842510a4d53 Mon Sep 17 00:00:00 2001 From: Mark DeLaVergne Date: Wed, 19 Aug 2026 10:34:24 -0400 Subject: [PATCH 6/6] Fix runner temp usage --- .github/workflows/sf_cli_integration.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sf_cli_integration.yml b/.github/workflows/sf_cli_integration.yml index d2bde49..c320f8b 100644 --- a/.github/workflows/sf_cli_integration.yml +++ b/.github/workflows/sf_cli_integration.yml @@ -10,9 +10,6 @@ jobs: env: SF_AUTOUPDATE_DISABLE: true NO_COLOR: '1' - # Mock server's TLS cert/key pair. - MOCK_SF_CERT_FILE: ${{ runner.temp }}/mock_sf_cert.pem - MOCK_SF_KEY_FILE: ${{ runner.temp }}/mock_sf_key.pem steps: # ── Setup ───────────────────────────────────────────────────────────────── @@ -20,6 +17,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set mock server TLS cert paths + run: | + echo "MOCK_SF_CERT_FILE=$RUNNER_TEMP/mock_sf_cert.pem" >> "$GITHUB_ENV" + echo "MOCK_SF_KEY_FILE=$RUNNER_TEMP/mock_sf_key.pem" >> "$GITHUB_ENV" + - name: Generate mock server TLS cert run: | openssl req -x509 -newkey rsa:2048 -nodes \