CI workflow (trailheadapps/lwc-recipes)
The CI workflow from trailheadapps/lwc-recipes, explained and optimized by Latchkey.
CI health: C - fair
Run this on Latchkey for self-healing, caching, and up to 58% lower cost.
Grade your own workflow free or run it on Latchkey →What it does
This is the CI workflow from the trailheadapps/lwc-recipes repository, a real project running GitHub Actions. It is shown here with attribution under its CC0-1.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
# Unique name for this workflow
name: CI
# Definition when the workflow should run
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- 'sfdx-project.json'
- 'README.md'
# Jobs to be executed
jobs:
format-lint-lwc-tests:
runs-on: trailheadapps-Ubuntu
steps:
# Checkout the source code
- name: 'Checkout source code'
uses: actions/checkout@v4
# Install Volta to enforce proper node and package manager versions
- name: 'Install Volta'
uses: volta-cli/action@v4
# Cache node_modules to speed up the process
- name: 'Restore node_modules cache'
id: cache-npm
uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ env.cache-name }}-
npm-
# Install npm dependencies for Prettier and Jest
- name: 'Install npm dependencies'
if: steps.cache-npm.outputs.cache-hit != 'true'
run: HUSKY=0 npm ci
# Prettier formatting
- name: 'Code formatting verification with Prettier'
run: npm run prettier:verify
# Install Salesforce CLI
- name: 'Install Salesforce CLI'
run: |
npm install @salesforce/cli --location=global
nodeInstallPath=$(npm config get prefix)
echo "$nodeInstallPath/bin" >> $GITHUB_PATH
cd "$nodeInstallPath/bin"
./sf --version
# Install Salesforce CLI Code Analyzer plugin
- name: 'Install Salesforce CLI Code Analyzer plugin'
run: sf plugins install code-analyzer
# Run Code Analyzer
- name: 'Run Code Analyzer'
id: run-code-analyzer
uses: forcedotcom/run-code-analyzer@v2
with:
run-arguments: --workspace "force-app" --view detail --output-file "sca-results.csv" --config-file "code-analyzer.yml"
results-artifact-name: code-analyzer-results
# Check for Code Analyzer critical or high severity violations
- name: 'Check for Code Analyzer critical or high severity violations'
if: |
steps.run-code-analyzer.outputs.exit-code > 0 ||
steps.run-code-analyzer.outputs.num-sev1-violations > 0 ||
steps.run-code-analyzer.outputs.num-sev2-violations > 0
run: |
echo One of more Code Analyzer critical or high severity violations found
exit 1
# LWC unit tests
- name: 'Unit test Lightning Web Components'
run: npm run test:unit:coverage
# Upload code coverage data
- name: 'Upload code coverage for LWC to Codecov.io'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: LWC
scratch-org-test:
runs-on: trailheadapps-Ubuntu
needs: format-lint-lwc-tests
if: github.actor != 'dependabot[bot]'
steps:
# Checkout the source code
- name: 'Checkout source code'
uses: actions/checkout@v4
# Install Salesforce CLI
- name: 'Install Salesforce CLI'
run: |
npm install @salesforce/cli --location=global
nodeInstallPath=$(npm config get prefix)
echo "$nodeInstallPath/bin" >> $GITHUB_PATH
sf --version
# Store secret for dev hub
- name: 'Populate auth file with DEVHUB_SFDX_URL secret'
shell: bash
run: |
echo ${{ secrets.DEVHUB_SFDX_URL }} > ./DEVHUB_SFDX_URL.txt
secretFileSize=$(wc -c "./DEVHUB_SFDX_URL.txt" | awk '{print $1}')
if [ $secretFileSize == 1 ]; then
echo "Missing DEVHUB_SFDX_URL secret. Is this workflow running on a fork?";
exit 1;
fi
# Authenticate dev hub
- name: 'Authenticate Dev Hub'
run: sf org login sfdx-url -f ./DEVHUB_SFDX_URL.txt -a devhub -d
# Create scratch org
- name: 'Create scratch org'
run: sf org create scratch -f config/project-scratch-def.json -a scratch-org -d -y 1
# Deploy source to scratch org
- name: 'Push source to scratch org'
run: sf project deploy start
# Assign permissionset
- name: 'Assign permissionset to default user'
run: sf org assign permset -n recipes
# Import sample data
- name: 'Import sample data'
run: sf data tree import -p ./data/data-plan.json
# Run Apex tests in scratch org
- name: 'Run Apex tests'
run: sf apex test run -c -r human -d ./tests/apex -w 20
# Upload code coverage data
- name: 'Upload code coverage for Apex to Codecov.io'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: Apex
# Housekeeping
- name: 'Delete scratch org'
if: always()
run: sf org delete scratch -p -o scratch-org
trigger-packaging:
runs-on: trailheadapps-Ubuntu
needs: scratch-org-test
steps:
# Checkout the source code
- name: 'Checkout source code'
uses: actions/checkout@v4
# Check for package changes using git diff
- name: 'Check for package changes'
id: checkForChanges
run: |
git fetch origin ${{ github.event.before }} --depth=1
changedPaths=$( git diff-tree --name-only ${{ github.event.before }} $GITHUB_SHA )
set +e
hasChanges='false'
if [ $(echo "$changedPaths" | grep -c '^force-app') == 1 ]; then
hasChanges='true'
fi
echo "hasChanges=$hasChanges" >> $GITHUB_OUTPUT
# Trigger packaging workflow if needed
- name: 'Trigger packaging workflow if needed'
uses: peter-evans/repository-dispatch@v3
if: steps.checkForChanges.outputs.hasChanges == 'true'
with:
token: ${{ secrets.BOT_ACCESS_TOKEN }}
event-type: start-packaging
client-payload: '{ "ref": "${{ github.ref }}", "sha": "${{ github.sha }}" }'
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
# Unique name for this workflow name: CI # Definition when the workflow should run on: workflow_dispatch: push: branches: - main paths-ignore: - 'sfdx-project.json' - 'README.md' # Jobs to be executed concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: format-lint-lwc-tests: timeout-minutes: 30 runs-on: trailheadapps-Ubuntu steps: # Checkout the source code - name: 'Checkout source code' uses: actions/checkout@v4 # Install Volta to enforce proper node and package manager versions - name: 'Install Volta' uses: volta-cli/action@v4 # Cache node_modules to speed up the process - name: 'Restore node_modules cache' id: cache-npm uses: actions/cache@v4 with: path: node_modules key: npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | npm-${{ env.cache-name }}- npm- # Install npm dependencies for Prettier and Jest - name: 'Install npm dependencies' if: steps.cache-npm.outputs.cache-hit != 'true' run: HUSKY=0 npm ci # Prettier formatting - name: 'Code formatting verification with Prettier' run: npm run prettier:verify # Install Salesforce CLI - name: 'Install Salesforce CLI' run: | npm install @salesforce/cli --location=global nodeInstallPath=$(npm config get prefix) echo "$nodeInstallPath/bin" >> $GITHUB_PATH cd "$nodeInstallPath/bin" ./sf --version # Install Salesforce CLI Code Analyzer plugin - name: 'Install Salesforce CLI Code Analyzer plugin' run: sf plugins install code-analyzer # Run Code Analyzer - name: 'Run Code Analyzer' id: run-code-analyzer uses: forcedotcom/run-code-analyzer@v2 with: run-arguments: --workspace "force-app" --view detail --output-file "sca-results.csv" --config-file "code-analyzer.yml" results-artifact-name: code-analyzer-results # Check for Code Analyzer critical or high severity violations - name: 'Check for Code Analyzer critical or high severity violations' if: | steps.run-code-analyzer.outputs.exit-code > 0 || steps.run-code-analyzer.outputs.num-sev1-violations > 0 || steps.run-code-analyzer.outputs.num-sev2-violations > 0 run: | echo One of more Code Analyzer critical or high severity violations found exit 1 # LWC unit tests - name: 'Unit test Lightning Web Components' run: npm run test:unit:coverage # Upload code coverage data - name: 'Upload code coverage for LWC to Codecov.io' uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} flags: LWC scratch-org-test: timeout-minutes: 30 runs-on: trailheadapps-Ubuntu needs: format-lint-lwc-tests if: github.actor != 'dependabot[bot]' steps: # Checkout the source code - name: 'Checkout source code' uses: actions/checkout@v4 # Install Salesforce CLI - name: 'Install Salesforce CLI' run: | npm install @salesforce/cli --location=global nodeInstallPath=$(npm config get prefix) echo "$nodeInstallPath/bin" >> $GITHUB_PATH sf --version # Store secret for dev hub - name: 'Populate auth file with DEVHUB_SFDX_URL secret' shell: bash run: | echo ${{ secrets.DEVHUB_SFDX_URL }} > ./DEVHUB_SFDX_URL.txt secretFileSize=$(wc -c "./DEVHUB_SFDX_URL.txt" | awk '{print $1}') if [ $secretFileSize == 1 ]; then echo "Missing DEVHUB_SFDX_URL secret. Is this workflow running on a fork?"; exit 1; fi # Authenticate dev hub - name: 'Authenticate Dev Hub' run: sf org login sfdx-url -f ./DEVHUB_SFDX_URL.txt -a devhub -d # Create scratch org - name: 'Create scratch org' run: sf org create scratch -f config/project-scratch-def.json -a scratch-org -d -y 1 # Deploy source to scratch org - name: 'Push source to scratch org' run: sf project deploy start # Assign permissionset - name: 'Assign permissionset to default user' run: sf org assign permset -n recipes # Import sample data - name: 'Import sample data' run: sf data tree import -p ./data/data-plan.json # Run Apex tests in scratch org - name: 'Run Apex tests' run: sf apex test run -c -r human -d ./tests/apex -w 20 # Upload code coverage data - name: 'Upload code coverage for Apex to Codecov.io' uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} flags: Apex # Housekeeping - name: 'Delete scratch org' if: always() run: sf org delete scratch -p -o scratch-org trigger-packaging: timeout-minutes: 30 runs-on: trailheadapps-Ubuntu needs: scratch-org-test steps: # Checkout the source code - name: 'Checkout source code' uses: actions/checkout@v4 # Check for package changes using git diff - name: 'Check for package changes' id: checkForChanges run: | git fetch origin ${{ github.event.before }} --depth=1 changedPaths=$( git diff-tree --name-only ${{ github.event.before }} $GITHUB_SHA ) set +e hasChanges='false' if [ $(echo "$changedPaths" | grep -c '^force-app') == 1 ]; then hasChanges='true' fi echo "hasChanges=$hasChanges" >> $GITHUB_OUTPUT # Trigger packaging workflow if needed - name: 'Trigger packaging workflow if needed' uses: peter-evans/repository-dispatch@v3 if: steps.checkForChanges.outputs.hasChanges == 'true' with: token: ${{ secrets.BOT_ACCESS_TOKEN }} event-type: start-packaging client-payload: '{ "ref": "${{ github.ref }}", "sha": "${{ github.sha }}" }'
What changed
- Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
4 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 3 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.