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
277 changes: 277 additions & 0 deletions .github/workflows/sf_cli_integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
name: SF CLI Integration Test

on:
pull_request:

jobs:
sf-cli-integration:
runs-on: ubuntu-latest
env:
SF_AUTOUPDATE_DISABLE: true
NO_COLOR: '1'

steps:
# ── Setup ─────────────────────────────────────────────────────────────────

- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Poetry and Python SDK
run: |
python -m pip install --upgrade pip
pip install poetry
make develop

- name: Add Poetry venv to PATH
run: echo "$(poetry env info --path)/bin" >> $GITHUB_PATH

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install Salesforce CLI
run: npm install -g @salesforce/cli

- name: Install data-code-extension plugin
run: sf plugins install @salesforce/plugin-data-code-extension --force

- name: Set up Java 17 (required for PySpark during run)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

# ── Mock Salesforce server + fake org auth ────────────────────────────────

- name: Start mock Salesforce server
run: python scripts/mock_sf_server.py &
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will the & process be dangling if a test fails? Might want to add something to cleanup at the end, like:

  - name: Stop mock server
    if: always()
    run: kill $(cat /tmp/mock_server.pid) 2>/dev/null || true

env:
MOCK_SF_PORT: '8888'

- name: Create fake SF CLI auth for org alias 'dev1'
run: |
sleep 1
python - <<'PYEOF'
import json, pathlib
home = pathlib.Path.home()

# Auth file — @salesforce/core reads ~/.sfdx/<username>.json on Linux
# (plain-text storage, no OS keychain involved on CI runners)
sfdx_dir = home / ".sfdx"
sfdx_dir.mkdir(exist_ok=True)
auth = {
"accessToken": "00D000000000001AAA!fakeTokenForCITesting",
"instanceUrl": "http://localhost:8888",
"loginUrl": "https://login.salesforce.com",
"orgId": "00D000000000001AAA",
"userId": "005000000000001AAA",
"username": "dev1@example.com",
"clientId": "PlatformCLI",
"isDevHub": False,
"isSandbox": False,
"created": "2024-01-01T00:00:00.000Z",
"createdOrgInstance": "CS1",
}
(sfdx_dir / "dev1@example.com.json").write_text(json.dumps(auth, indent=2))

# Alias mapping — write to both locations for compat across sf versions
alias_data = {"orgs": {"dev1": "dev1@example.com"}}
sf_dir = home / ".sf"
sf_dir.mkdir(exist_ok=True)
(sf_dir / "alias.json").write_text(json.dumps(alias_data))
(sfdx_dir / "alias.json").write_text(json.dumps(alias_data))

print("Fake SF CLI org auth written to ~/.sfdx/dev1@example.com.json")
PYEOF
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For this command and all that follow- is there any reason to do them inline like this, as opposed to putting all of these into a python script or something and calling that from here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Migrating these commands to native Typescript will likely happen one at a time. Spelling them out this way allows for easy removal of the init commands as an example once that Typescript has been implemented leaving the other tests in place until they get migrated. Lastly if a particular test fails it's pretty easy to track down the where and why.

Probably some of the fake token logic and auth logic could be extracted for portability, interesting day after thought as I was pondering your questions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It there a way to run all these tests locally, via act maybe?


# ── Script: init ──────────────────────────────────────────────────────────

- name: '[script] init — sf data-code-extension script init --package-dir testScript'
run: |
sf data-code-extension script init --package-dir testScript || {
echo "::error::sf data-code-extension script init FAILED. Verify --package-dir is still a recognised flag and that the command exits 0 on success."
exit 1
}

- name: '[script] verify init — expected files exist'
run: |
test -f testScript/payload/entrypoint.py || {
echo "::error::testScript/payload/entrypoint.py not found after init. The init command may not have copied the script template."
exit 1
}
test -f testScript/.datacustomcode_proj/sdk_config.json || {
echo "::error::testScript/.datacustomcode_proj/sdk_config.json not found after init. The SDK config marker was not written."
exit 1
}

# ── Script: scan ──────────────────────────────────────────────────────────

- name: '[script] scan — sf data-code-extension script scan --entrypoint testScript/payload/entrypoint.py'
run: |
sf data-code-extension script scan --entrypoint testScript/payload/entrypoint.py || {
echo "::error::sf data-code-extension script scan FAILED. Verify --entrypoint is still a recognised flag and the command exits 0."
exit 1
}

- name: '[script] verify scan — config.json contains permissions'
run: |
python - <<'EOF'
import json, sys
path = "testScript/payload/config.json"
try:
with open(path) as f:
data = json.load(f)
except Exception as e:
print(f"::error::Could not read {path}: {e}")
sys.exit(1)
if "permissions" not in data:
print(f"::error::{path} is missing 'permissions' key after scan. Got: {json.dumps(data)}")
sys.exit(1)
print("config.json OK:", json.dumps(data, indent=2))
EOF

# ── Script: zip ───────────────────────────────────────────────────────────

- name: '[script] prepare for zip — clear requirements.txt to skip native-dep Docker build'
run: echo "" > testScript/payload/requirements.txt

- name: '[script] zip — sf data-code-extension script zip --package-dir testScript'
run: |
sf data-code-extension script zip --package-dir testScript || {
echo "::error::sf data-code-extension script zip FAILED. Verify --package-dir is still recognised and the command exits 0."
exit 1
}

- name: '[script] verify zip — deployment.zip exists'
run: |
test -f deployment.zip || {
echo "::error::deployment.zip not found after sf data-code-extension script zip. The zip command may have written to a different path or failed silently."
exit 1
}

# ── Script: run ───────────────────────────────────────────────────────────

- name: '[script] run — sf data-code-extension script run --entrypoint testScript/payload/entrypoint.py -o dev1'
run: |
sf data-code-extension script run \
--entrypoint testScript/payload/entrypoint.py \
-o dev1 || {
echo "::error::sf data-code-extension script run FAILED. Check mock server output above for which endpoint failed. The --entrypoint flag or SF CLI org auth contract may have changed."
exit 1
}

# ── Script: deploy ───────────────────────────────────────────────────────

- name: '[script] deploy — sf data-code-extension script deploy'
run: |
sf data-code-extension script deploy \
--name test-script-deploy \
--package-version 0.0.1 \
--description "Test script deploy" \
--package-dir testScript/payload \
--cpu-size CPU_2XL \
-o dev1 || {
echo "::error::sf data-code-extension script deploy FAILED. Check mock server output above for which endpoint failed. The deploy command flags or API contract may have changed."
exit 1
}

# ── Function: init ────────────────────────────────────────────────────────

- name: '[function] init — sf data-code-extension function init --package-dir testFunction'
run: |
sf data-code-extension function init --package-dir testFunction || {
echo "::error::sf data-code-extension function init FAILED. Verify --package-dir is still recognised and the function template copies correctly."
exit 1
}

- name: '[function] verify init — expected files exist'
run: |
test -f testFunction/payload/entrypoint.py || {
echo "::error::testFunction/payload/entrypoint.py not found after function init."
exit 1
}
test -f testFunction/.datacustomcode_proj/sdk_config.json || {
echo "::error::testFunction/.datacustomcode_proj/sdk_config.json not found after function init."
exit 1
}

# ── Function: scan ────────────────────────────────────────────────────────

- name: '[function] scan — sf data-code-extension function scan --entrypoint testFunction/payload/entrypoint.py'
run: |
sf data-code-extension function scan --entrypoint testFunction/payload/entrypoint.py || {
echo "::error::sf data-code-extension function scan FAILED."
exit 1
}

- name: '[function] verify scan — config.json contains entryPoint'
run: |
python - <<'EOF'
import json, sys
path = "testFunction/payload/config.json"
try:
with open(path) as f:
data = json.load(f)
except Exception as e:
print(f"::error::Could not read {path}: {e}")
sys.exit(1)
if "entryPoint" not in data:
print(f"::error::{path} is missing 'entryPoint' key after scan. Got: {json.dumps(data)}")
sys.exit(1)
print("config.json OK:", json.dumps(data, indent=2))
EOF

# ── Function: zip ─────────────────────────────────────────────────────────

- name: '[function] prepare for zip — clear requirements.txt to skip native-dep Docker build'
run: echo "" > testFunction/payload/requirements.txt

- name: '[function] clean up previous deployment.zip before function zip'
run: rm -f deployment.zip

- name: '[function] zip — sf data-code-extension function zip --package-dir testFunction'
run: |
sf data-code-extension function zip --package-dir testFunction || {
echo "::error::sf data-code-extension function zip FAILED."
exit 1
}

- name: '[function] verify zip — deployment.zip exists'
run: |
test -f deployment.zip || {
echo "::error::deployment.zip not found after sf data-code-extension function zip."
exit 1
}

# ── Function: run ─────────────────────────────────────────────────────────

- name: '[function] run — sf data-code-extension function run --entrypoint testFunction/payload/entrypoint.py -o dev1'
run: |
sf data-code-extension function run \
--entrypoint testFunction/payload/entrypoint.py \
-o dev1 || {
echo "::error::sf data-code-extension function run FAILED. Check mock server output above; the --entrypoint flag or SF CLI org auth contract may have changed."
exit 1
}

# ── Function: deploy ─────────────────────────────────────────────────────

- name: '[function] deploy — sf data-code-extension function deploy'
run: |
sf data-code-extension function deploy \
--name test-function-deploy \
--package-version 0.0.1 \
--description "Test function deploy" \
--package-dir testFunction/payload \
--cpu-size CPU_2XL \
--function-invoke-opt UnstructuredChunking \
-o dev1 || {
echo "::error::sf data-code-extension function deploy FAILED. Check mock server output above for which endpoint failed. The deploy command flags or API contract may have changed."
exit 1
}
Loading
Loading