Skip to content
Closed
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
260 changes: 260 additions & 0 deletions .github/workflows/agentforce-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# This workflow is an example used during the Paris Dev Group meetup on November 5th 2025.
# Une CI/CD pour vos Agentforce : état des lieux
# By @nabondance

name: Agentforce Validate
# This workflow runs Salesforce Agent tests and aggregates the results.
# It is split into several steps for better readability and understanding.

on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
branches: [ main ]

jobs:
setup-deploy:
runs-on: ubuntu-latest
container: salesforce/cli:latest-slim

steps:
- name: Checkout Code
uses: actions/checkout@v5

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
cache-dependency-path: 'pnpm-lock.yaml'

- name: Install Dependencies
id: pnpm-install
run: pnpm install

- name: 'Authenticate to Dev Hub'
run: |
echo "${{ secrets.DEVHUB_SFDX_AUTH_URL }}" > ./authfile
sf org login sfdx-url --sfdxurlfile=authfile --alias=devhub

- name: 'Get org from pool'
run: |
pnpm sfp pool fetch --targetdevhubusername=devhub --tag=ci-pool --alias=so-ci

- name: 'Deploy Agentforce to org'
run: |
sf project deploy start --target-org=so-ci --manifest=manifestAgent.xml --wait=30

- name: 'Capture org auth URL'
id: capture-org
run: |
echo "=== DEBUG: Extracting sfdxAuthUrl ==="
AUTH_URL=$(sf org display --target-org=so-ci --verbose --json | tr -d '\000-\037' | jq -r '.result.sfdxAuthUrl')

# Save auth URL to artifact
mkdir -p auth-artifact
echo "$AUTH_URL" > auth-artifact/auth-url.txt
echo "=== DEBUG: Auth URL saved to artifact ==="

- name: Upload auth URL artifact
uses: actions/upload-artifact@v4
with:
name: org-auth-url
path: auth-artifact/

list-tests:
name: List Agent Tests
needs: setup-deploy
runs-on: ubuntu-latest
container: salesforce/cli:latest-slim
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}

steps:
- uses: actions/checkout@v5

- name: Download auth URL artifact
uses: actions/download-artifact@v4
with:
name: org-auth-url
path: auth-artifact

- name: Authenticate to org
run: |
echo "=== DEBUG: Reading auth URL from artifact ==="
AUTH_URL=$(cat auth-artifact/auth-url.txt)
echo "AUTH_URL from artifact: '$AUTH_URL'"

if [ -z "$AUTH_URL" ] || [ "$AUTH_URL" = "null" ]; then
echo "ERROR: Auth URL is empty or null from artifact"
exit 1
fi

echo "$AUTH_URL" > ./authfileci
sf org login sfdx-url --sfdxurlfile=authfileci --alias=so-ci

- name: List agent tests and set matrix
id: set-matrix
run: |
TESTS=$(sf agent test list --target-org=so-ci --json | jq -c '[.result[].fullName]')
if [ "$TESTS" == "[]" ]; then
echo "No tests found. Failing early."
exit 1
fi
echo "matrix={\"test\":$TESTS}" >> "$GITHUB_OUTPUT"

run-agent-test:
name: Run Agent Test - ${{ matrix.test }}
needs: [setup-deploy, list-tests]
runs-on: ubuntu-latest
container: salesforce/cli:latest-slim
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.list-tests.outputs.matrix) }}

steps:
- uses: actions/checkout@v5

- name: Download auth URL artifact
uses: actions/download-artifact@v4
with:
name: org-auth-url
path: auth-artifact

- name: Authenticate to org
run: |
AUTH_URL=$(cat auth-artifact/auth-url.txt)
echo "$AUTH_URL" > ./authfileci
sf org login sfdx-url --sfdxurlfile=authfileci --alias=so-ci

- name: Run test and get result
id: test
run: |
mkdir -p test-results
RUN_ID=$(sf agent test run --target-org=so-ci --api-name="${{ matrix.test }}" --wait 10 --json | jq -r '.result.runId')
RESULT=$(sf agent test results --target-org=so-ci --job-id="$RUN_ID" --json)
# Clean control characters before saving JSON
echo "$RESULT" | tr -d '\000-\037' > "test-results/${{ matrix.test }}.json"

- name: Upload individual result
uses: actions/upload-artifact@v4
with:
name: agent-test-results-${{ matrix.test }}
path: test-results/

validate-results:
name: Validate Results
needs: run-agent-test
runs-on: ubuntu-latest

steps:
- name: Download all test results
uses: actions/download-artifact@v4
with:
path: all-results
pattern: agent-test-results-*
merge-multiple: true

- name: Summarize test outcomes
id: summary
run: |
total=0
passed=0
failed=0

ls -al all-results/

# Check if any JSON files exist
if ! ls all-results/ 1> /dev/null 2>&1; then
echo "No test result files found"
exit 1
fi

for file in all-results/*; do
echo "Processing file: $file"

# Check if file is valid JSON and has expected structure
if ! jq -e '.result.testCases' "$file" > /dev/null 2>&1; then
echo "Skipping invalid or malformed JSON file: $file"
continue
fi

p=$(jq '[.result.testCases[]? | .testResults[]? | select(.result == "PASS")] | length' "$file" 2>/dev/null || echo "0")
f=$(jq '[.result.testCases[]? | .testResults[]? | select(.result == "FAILURE")] | length' "$file" 2>/dev/null || echo "0")

total=$((total + p + f))
passed=$((passed + p))
failed=$((failed + f))

echo "File $file: $p passed, $f failed"
done

if [ $total -eq 0 ]; then
echo "No test results found in any files"
exit 1
fi

percentage=$((passed * 100 / total))
echo "TOTAL=$total" >> "$GITHUB_OUTPUT"
echo "PASSED=$passed" >> "$GITHUB_OUTPUT"
echo "FAILED=$failed" >> "$GITHUB_OUTPUT"
echo "PERCENT=$percentage" >> "$GITHUB_OUTPUT"

- name: Display summary
run: |
echo "=================================="
echo " Agent Test Summary "
echo "=================================="
echo "Total Tests : ${{ steps.summary.outputs.TOTAL }}"
echo "Tests Passed: ${{ steps.summary.outputs.PASSED }}"
echo "Tests Failed: ${{ steps.summary.outputs.FAILED }}"
echo "Pass : ${{ steps.summary.outputs.PERCENT }}%"
echo "=================================="

- name: Enforce pass threshold
if: ${{ steps.summary.outputs.PERCENT < 75 }}
run: |
echo "❌ Agent tests failed threshold (75%). Only ${{ steps.summary.outputs.PERCENT }}% passed."
exit 1

cleanup:
name: Return Org to Pool
needs: [setup-deploy, validate-results]
if: always()
runs-on: ubuntu-latest
container: salesforce/cli:latest-slim

steps:
- name: Checkout Code
uses: actions/checkout@v5

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install Dependencies
run: pnpm install

- name: Download auth URL artifact
uses: actions/download-artifact@v4
with:
name: org-auth-url
path: auth-artifact

- name: Authenticate to DevHub
run: |
echo "${{ secrets.DEVHUB_SFDX_AUTH_URL }}" > ./authfile
sf org login sfdx-url --sfdxurlfile=authfile --alias=devhub

- name: Return org to pool
run: |
AUTH_URL=$(cat auth-artifact/auth-url.txt)
echo "$AUTH_URL" > ./org-auth.txt
sf org login sfdx-url --sfdxurlfile=org-auth.txt --alias=so-ci
pnpm sfp pool delete --targetdevhubusername=devhub --tag=ci-pool --myorg=so-ci
45 changes: 45 additions & 0 deletions .github/workflows/pool-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This workflow is an example of pool preparation based on @flxbl-io library.
# By @nabondance

name: Prepare Pool CI

on:
workflow_dispatch:
# Uncomment the following lines to enable scheduled runs
# schedule:
# - cron: '0 6 * * *'

jobs:
salesforce-agent:
runs-on: ubuntu-latest
container: salesforce/cli:latest-slim

steps:
- name: Checkout Code
uses: actions/checkout@v5

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
cache-dependency-path: 'pnpm-lock.yaml'

- name: Install Dependencies
id: pnpm-install
run: pnpm install

- name: 'Authenticate to Dev Hub'
run: |
echo "${{ secrets.DEVHUB_SFDX_AUTH_URL }}" > ./authfile
sf org login sfdx-url --sfdxurlfile=authfile --alias=devhub

- name: 'Prepare Pool Org'
run: |
pnpm sfp pool prepare --targetdevhubusername=devhub --poolconfig=./pools/ci-pool.json
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
node_modules
.DS_Store

notes.md
.sfpowerscripts
.claude

authFile.json
notes.md
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.14.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<AiEvaluationDefinition xmlns="http://soap.sforce.com/2006/04/metadata">
<name>JokeCsv</name>
<subjectName>Joke</subjectName>
<subjectType>AGENT</subjectType>
<subjectVersion>v1</subjectVersion>
<testCase>
<expectation>
<name>action_sequence_match</name>
</expectation>
<expectation>
<expectedValue>A joke is delivered</expectedValue>
<name>bot_response_rating</name>
</expectation>
<expectation>
<expectedValue>Joke_Telling_16j720000002ufd</expectedValue>
<name>topic_sequence_match</name>
</expectation>
<inputs>
<utterance>Tell me a joke</utterance>
</inputs>
<number>1</number>
</testCase>
<testCase>
<expectation>
<name>action_sequence_match</name>
</expectation>
<expectation>
<expectedValue>A joke is delivered</expectedValue>
<name>bot_response_rating</name>
</expectation>
<expectation>
<expectedValue>Joke_Telling_16j720000002ufd</expectedValue>
<name>topic_sequence_match</name>
</expectation>
<inputs>
<utterance>Can you make me laugh</utterance>
</inputs>
<number>2</number>
</testCase>
<testCase>
<expectation>
<expectedValue>[&apos;ThemedJoke&apos;]</expectedValue>
<name>action_sequence_match</name>
</expectation>
<expectation>
<expectedValue>A programming-related joke is delivered</expectedValue>
<name>bot_response_rating</name>
</expectation>
<expectation>
<expectedValue>Joke_Telling_16j720000002ufd</expectedValue>
<name>topic_sequence_match</name>
</expectation>
<inputs>
<utterance>I need a joke about programming</utterance>
</inputs>
<number>3</number>
</testCase>
<testCase>
<expectation>
<expectedValue>[&apos;ThemedJoke&apos;]</expectedValue>
<name>action_sequence_match</name>
</expectation>
<expectation>
<expectedValue>A dad joke is delivered</expectedValue>
<name>bot_response_rating</name>
</expectation>
<expectation>
<expectedValue>Joke_Telling_16j720000002ufd</expectedValue>
<name>topic_sequence_match</name>
</expectation>
<inputs>
<utterance>Tell me a dad joke</utterance>
</inputs>
<number>4</number>
</testCase>
</AiEvaluationDefinition>
Loading
Loading