1+ # This workflow is an example used during the Paris Dev Group meetup on November 5th 2025.
2+ # Une CI/CD pour vos Agentforce : état des lieux
3+ # By @nabondance
4+
5+ name : Agentforce Validate
6+ # This workflow runs Salesforce Agent tests and aggregates the results.
7+ # It is split into several steps for better readability and understanding.
8+
9+ on :
10+ pull_request :
11+ types : [opened, synchronize, ready_for_review, reopened]
12+ branches : [ main ]
13+
14+ jobs :
15+ setup-deploy :
16+ runs-on : ubuntu-latest
17+ container : salesforce/cli:latest-slim
18+
19+ steps :
20+ - name : Checkout Code
21+ uses : actions/checkout@v5
22+
23+ - name : Install pnpm
24+ uses : pnpm/action-setup@v4
25+ with :
26+ version : 10
27+
28+ - name : Setup Node.js
29+ id : setup-node
30+ uses : actions/setup-node@v4
31+ with :
32+ node-version-file : .node-version
33+ cache : pnpm
34+ cache-dependency-path : ' pnpm-lock.yaml'
35+
36+ - name : Install Dependencies
37+ id : pnpm-install
38+ run : pnpm install
39+
40+ - name : ' Authenticate to Dev Hub'
41+ run : |
42+ echo "${{ secrets.DEVHUB_SFDX_AUTH_URL }}" > ./authfile
43+ sf org login sfdx-url --sfdxurlfile=authfile --alias=devhub
44+
45+ - name : ' Get org from pool'
46+ run : |
47+ pnpm sfp pool fetch --targetdevhubusername=devhub --tag=ci-pool --alias=so-ci
48+
49+ - name : ' Deploy Agentforce to org'
50+ run : |
51+ sf project deploy start --target-org=so-ci --manifest=manifestAgent.xml --wait=30
52+
53+ - name : ' Capture org auth URL'
54+ id : capture-org
55+ run : |
56+ echo "=== DEBUG: Extracting sfdxAuthUrl ==="
57+ AUTH_URL=$(sf org display --target-org=so-ci --verbose --json | tr -d '\000-\037' | jq -r '.result.sfdxAuthUrl')
58+
59+ # Save auth URL to artifact
60+ mkdir -p auth-artifact
61+ echo "$AUTH_URL" > auth-artifact/auth-url.txt
62+ echo "=== DEBUG: Auth URL saved to artifact ==="
63+
64+ - name : Upload auth URL artifact
65+ uses : actions/upload-artifact@v4
66+ with :
67+ name : org-auth-url
68+ path : auth-artifact/
69+
70+ list-tests :
71+ name : List Agent Tests
72+ needs : setup-deploy
73+ runs-on : ubuntu-latest
74+ container : salesforce/cli:latest-slim
75+ outputs :
76+ matrix : ${{ steps.set-matrix.outputs.matrix }}
77+
78+ steps :
79+ - uses : actions/checkout@v5
80+
81+ - name : Download auth URL artifact
82+ uses : actions/download-artifact@v4
83+ with :
84+ name : org-auth-url
85+ path : auth-artifact
86+
87+ - name : Authenticate to org
88+ run : |
89+ echo "=== DEBUG: Reading auth URL from artifact ==="
90+ AUTH_URL=$(cat auth-artifact/auth-url.txt)
91+ echo "AUTH_URL from artifact: '$AUTH_URL'"
92+
93+ if [ -z "$AUTH_URL" ] || [ "$AUTH_URL" = "null" ]; then
94+ echo "ERROR: Auth URL is empty or null from artifact"
95+ exit 1
96+ fi
97+
98+ echo "$AUTH_URL" > ./authfileci
99+ sf org login sfdx-url --sfdxurlfile=authfileci --alias=so-ci
100+
101+ - name : List agent tests and set matrix
102+ id : set-matrix
103+ run : |
104+ TESTS=$(sf agent test list --target-org=so-ci --json | jq -c '[.result[].fullName]')
105+ if [ "$TESTS" == "[]" ]; then
106+ echo "No tests found. Failing early."
107+ exit 1
108+ fi
109+ echo "matrix={\"test\":$TESTS}" >> "$GITHUB_OUTPUT"
110+
111+ run-agent-test :
112+ name : Run Agent Test - ${{ matrix.test }}
113+ needs : [setup-deploy, list-tests]
114+ runs-on : ubuntu-latest
115+ container : salesforce/cli:latest-slim
116+ strategy :
117+ fail-fast : false
118+ matrix : ${{ fromJson(needs.list-tests.outputs.matrix) }}
119+
120+ steps :
121+ - uses : actions/checkout@v5
122+
123+ - name : Download auth URL artifact
124+ uses : actions/download-artifact@v4
125+ with :
126+ name : org-auth-url
127+ path : auth-artifact
128+
129+ - name : Authenticate to org
130+ run : |
131+ AUTH_URL=$(cat auth-artifact/auth-url.txt)
132+ echo "$AUTH_URL" > ./authfileci
133+ sf org login sfdx-url --sfdxurlfile=authfileci --alias=so-ci
134+
135+ - name : Run test and get result
136+ id : test
137+ run : |
138+ mkdir -p test-results
139+ RUN_ID=$(sf agent test run --target-org=so-ci --api-name="${{ matrix.test }}" --wait 10 --json | jq -r '.result.runId')
140+ RESULT=$(sf agent test results --target-org=so-ci --job-id="$RUN_ID" --json)
141+ # Clean control characters before saving JSON
142+ echo "$RESULT" | tr -d '\000-\037' > "test-results/${{ matrix.test }}.json"
143+
144+ - name : Upload individual result
145+ uses : actions/upload-artifact@v4
146+ with :
147+ name : agent-test-results-${{ matrix.test }}
148+ path : test-results/
149+
150+ validate-results :
151+ name : Validate Results
152+ needs : run-agent-test
153+ runs-on : ubuntu-latest
154+
155+ steps :
156+ - name : Download all test results
157+ uses : actions/download-artifact@v4
158+ with :
159+ path : all-results
160+ pattern : agent-test-results-*
161+ merge-multiple : true
162+
163+ - name : Summarize test outcomes
164+ id : summary
165+ run : |
166+ total=0
167+ passed=0
168+ failed=0
169+
170+ ls -al all-results/
171+
172+ # Check if any JSON files exist
173+ if ! ls all-results/ 1> /dev/null 2>&1; then
174+ echo "No test result files found"
175+ exit 1
176+ fi
177+
178+ for file in all-results/*; do
179+ echo "Processing file: $file"
180+
181+ # Check if file is valid JSON and has expected structure
182+ if ! jq -e '.result.testCases' "$file" > /dev/null 2>&1; then
183+ echo "Skipping invalid or malformed JSON file: $file"
184+ continue
185+ fi
186+
187+ p=$(jq '[.result.testCases[]? | .testResults[]? | select(.result == "PASS")] | length' "$file" 2>/dev/null || echo "0")
188+ f=$(jq '[.result.testCases[]? | .testResults[]? | select(.result == "FAILURE")] | length' "$file" 2>/dev/null || echo "0")
189+
190+ total=$((total + p + f))
191+ passed=$((passed + p))
192+ failed=$((failed + f))
193+
194+ echo "File $file: $p passed, $f failed"
195+ done
196+
197+ if [ $total -eq 0 ]; then
198+ echo "No test results found in any files"
199+ exit 1
200+ fi
201+
202+ percentage=$((passed * 100 / total))
203+ echo "TOTAL=$total" >> "$GITHUB_OUTPUT"
204+ echo "PASSED=$passed" >> "$GITHUB_OUTPUT"
205+ echo "FAILED=$failed" >> "$GITHUB_OUTPUT"
206+ echo "PERCENT=$percentage" >> "$GITHUB_OUTPUT"
207+
208+ - name : Display summary
209+ run : |
210+ echo "=================================="
211+ echo " Agent Test Summary "
212+ echo "=================================="
213+ echo "Total Tests : ${{ steps.summary.outputs.TOTAL }}"
214+ echo "Tests Passed: ${{ steps.summary.outputs.PASSED }}"
215+ echo "Tests Failed: ${{ steps.summary.outputs.FAILED }}"
216+ echo "Pass : ${{ steps.summary.outputs.PERCENT }}%"
217+ echo "=================================="
218+
219+ - name : Enforce pass threshold
220+ if : ${{ steps.summary.outputs.PERCENT < 75 }}
221+ run : |
222+ echo "❌ Agent tests failed threshold (75%). Only ${{ steps.summary.outputs.PERCENT }}% passed."
223+ exit 1
224+
225+ cleanup :
226+ name : Return Org to Pool
227+ needs : [setup-deploy, validate-results]
228+ if : always()
229+ runs-on : ubuntu-latest
230+ container : salesforce/cli:latest-slim
231+
232+ steps :
233+ - name : Checkout Code
234+ uses : actions/checkout@v5
235+
236+ - name : Install pnpm
237+ uses : pnpm/action-setup@v4
238+ with :
239+ version : 10
240+
241+ - name : Install Dependencies
242+ run : pnpm install
243+
244+ - name : Download auth URL artifact
245+ uses : actions/download-artifact@v4
246+ with :
247+ name : org-auth-url
248+ path : auth-artifact
249+
250+ - name : Authenticate to DevHub
251+ run : |
252+ echo "${{ secrets.DEVHUB_SFDX_AUTH_URL }}" > ./authfile
253+ sf org login sfdx-url --sfdxurlfile=authfile --alias=devhub
254+
255+ - name : Return org to pool
256+ run : |
257+ AUTH_URL=$(cat auth-artifact/auth-url.txt)
258+ echo "$AUTH_URL" > ./org-auth.txt
259+ sf org login sfdx-url --sfdxurlfile=org-auth.txt --alias=so-ci
260+ pnpm sfp pool delete --targetdevhubusername=devhub --tag=ci-pool --myorg=so-ci
0 commit comments