-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaction.yml
More file actions
1208 lines (1016 loc) · 43.1 KB
/
action.yml
File metadata and controls
1208 lines (1016 loc) · 43.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: "Setup Salesforce CLI"
description: "Enterprise-grade Salesforce CLI setup with authentication, caching, and optional tooling"
author: "Ryan Bumstead"
branding:
icon: "zap"
color: "blue"
inputs:
# ============================================================================
# CORE RUNTIME
# ============================================================================
node_version:
description: "Node.js version"
required: false
default: "20"
cli_version:
description: "Salesforce CLI version (latest or explicit version like 2.x.x)"
required: false
default: "latest"
cli_version_for_cache:
description: "CLI version granularity for cache key (major, minor, or exact). Controls cache reuse vs freshness."
required: false
default: "minor"
strict:
description: "Fail pipeline on optional tool installation errors"
required: false
default: "false"
dry_run:
description: "Dry-run mode: skip authentication and mutations, validate detection only"
required: false
default: "false"
debug:
description: "Enable verbose debug output for troubleshooting"
required: false
default: "false"
# ============================================================================
# AUTHENTICATION
# ============================================================================
skip_auth:
description: "Skip authentication (CLI installation only)"
required: false
default: "false"
auth_method:
description: "Authentication method: 'jwt', 'sfdx-url', or 'access-token' (required unless skip_auth is true)"
required: false
default: "jwt"
# JWT Authentication inputs
jwt_key:
description: "SFDX JWT Private Key (required when auth_method is 'jwt')"
required: false
client_id:
description: "Connected App Client ID (required for 'jwt' and 'access-token' auth methods)"
required: false
username:
description: "Salesforce Username (required when auth_method is 'jwt')"
required: false
# SFDX Auth URL inputs
sfdx_auth_url:
description: "SFDX Auth URL (required when auth_method is 'sfdx-url')"
required: false
# Direct Access Token inputs
allow_access_token_auth:
description: "Explicitly allow direct access token authentication. Advanced use only."
required: false
default: "true"
access_token:
description: "Salesforce Access Token (required when auth_method is 'access-token')"
required: false
alias:
description: "Org alias for authenticated org"
required: false
default: "TargetOrg"
is_dev_hub:
description: "Set org as default Dev Hub"
required: false
default: "false"
instance_url:
description: "Salesforce instance URL (required for 'jwt' and 'access-token' methods, e.g., https://login.salesforce.com)"
required: false
default: "https://login.salesforce.com"
# ============================================================================
# OPTIONAL TOOLING
# ============================================================================
install_delta:
description: "Install sfdx-git-delta for delta deployments"
required: false
default: "false"
install_scanner:
description: "Install Salesforce Code Analyzer for static analysis"
required: false
default: "false"
install_prettier:
description: "Install Prettier with Salesforce plugins for code formatting"
required: false
default: "false"
install_eslint:
description: "Install ESLint with Salesforce plugins for linting"
required: false
default: "false"
install_lwc_jest:
description: "Install @salesforce/sfdx-lwc-jest for LWC unit testing"
required: false
default: "false"
custom_sf_plugins:
description: "Comma-separated list of additional Salesforce CLI plugins to install (e.g., 'sfdx-hardis,@salesforce/plugin-packaging')"
required: false
default: ""
# ============================================================================
# SOURCE DIRECTORY RESOLUTION
# ============================================================================
source_dirs:
description: "Comma-separated source directories (e.g., 'force-app,packages/core')"
required: false
default: "force-app"
outputs:
org_id:
description: "Authenticated Salesforce Org ID"
value: ${{ steps.detect-org.outputs.org_id }}
org_edition:
description: "Salesforce Edition (Developer, Enterprise, Unlimited, etc.)"
value: ${{ steps.detect-org.outputs.org_edition }}
org_type:
description: "Organization type (Production, Sandbox, or Scratch)"
value: ${{ steps.detect-org.outputs.org_type }}
username:
description: "Authenticated username"
value: ${{ steps.detect-org.outputs.username }}
instance_url:
description: "Org instance URL"
value: ${{ steps.detect-org.outputs.instance_url }}
api_version:
description: "Salesforce API version for the org"
value: ${{ steps.detect-org.outputs.api_version }}
auth_performed:
description: "Whether authentication was performed (true) or skipped (false)"
value: ${{ steps.auth-status.outputs.auth_performed }}
sf_cli_version:
description: "Installed Salesforce CLI version"
value: ${{ steps.install-cli.outputs.cli_version }}
source_flags:
description: "Resolved source directory flags for SF CLI commands"
value: ${{ steps.resolve-source-dirs.outputs.source_flags }}
used_default_node:
description: "Whether the default Node.js version was used (true) or explicit version specified (false)"
value: ${{ steps.track-defaults.outputs.used_default_node }}
used_default_cli_version:
description: "Whether the default CLI version was used (true) or explicit version specified (false)"
value: ${{ steps.track-defaults.outputs.used_default_cli_version }}
used_default_api_version:
description: "Whether the API version was auto-detected (true) or explicitly provided (false)"
value: ${{ steps.track-defaults.outputs.used_default_api_version }}
cli_binary_path:
description: "Absolute path to the sf executable (for custom tooling integration)"
value: ${{ steps.install-cli.outputs.cli_path }}
validated_config:
description: "JSON summary of the final configuration (debug/audit use)"
value: ${{ steps.track-defaults.outputs.config_json }}
runs:
using: "composite"
steps:
# --------------------------------------------------------------------------
# CORE SETUP
# --------------------------------------------------------------------------
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Detect Operating System
id: detect-os
shell: bash
run: |
set -euo pipefail
echo "🔍 Detecting operating system..."
case "${{ runner.os }}" in
Linux)
echo "os_type=linux" >> $GITHUB_OUTPUT
echo "package_manager=apt" >> $GITHUB_OUTPUT
echo " ✅ OS: Linux (apt package manager)"
;;
macOS)
echo "os_type=macos" >> $GITHUB_OUTPUT
echo "package_manager=brew" >> $GITHUB_OUTPUT
echo " ✅ OS: macOS (brew package manager)"
;;
Windows)
echo "os_type=windows" >> $GITHUB_OUTPUT
echo "package_manager=choco" >> $GITHUB_OUTPUT
echo " ✅ OS: Windows (choco package manager)"
;;
*)
echo "os_type=unknown" >> $GITHUB_OUTPUT
echo "package_manager=none" >> $GITHUB_OUTPUT
echo " ⚠️ OS: Unknown"
;;
esac
- name: Ensure dependencies (jq) - Linux
if: steps.detect-os.outputs.os_type == 'linux'
shell: bash
run: |
set -euo pipefail
if ! command -v jq &> /dev/null; then
echo "📦 Installing jq on Linux..."
sudo apt-get update && sudo apt-get install -y jq
echo " ✅ jq installed successfully"
else
echo "✅ jq already installed"
fi
- name: Ensure dependencies (jq) - macOS
if: steps.detect-os.outputs.os_type == 'macos'
shell: bash
run: |
set -euo pipefail
if ! command -v jq &> /dev/null; then
echo "📦 Installing jq on macOS..."
brew install jq
echo " ✅ jq installed successfully"
else
echo "✅ jq already installed"
fi
- name: Ensure dependencies (jq) - Windows
if: steps.detect-os.outputs.os_type == 'windows'
shell: pwsh
run: |
if (!(Get-Command jq -ErrorAction SilentlyContinue)) {
Write-Host "📦 Installing jq on Windows..."
choco install jq -y
Write-Host " ✅ jq installed successfully"
} else {
Write-Host "✅ jq already installed"
}
- name: Generate cache key
id: cache-key
shell: bash
run: |
set -euo pipefail
echo "🔑 Generating cache key..."
# Tooling hash
if command -v sha256sum >/dev/null 2>&1; then
HASH_CMD="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
HASH_CMD="shasum -a 256"
else
HASH_CMD="echo"
fi
TOOLS_HASH=$(echo "${{ inputs.install_delta }}-${{ inputs.install_scanner }}-${{ inputs.install_prettier }}-${{ inputs.install_eslint }}-${{ inputs.install_lwc_jest }}-${{ inputs.custom_sf_plugins }}" \
| $HASH_CMD | awk '{print $1}' | cut -c1-8)
echo "tools_hash=$TOOLS_HASH" >> $GITHUB_OUTPUT
CLI_VERSION_INPUT="${{ inputs.cli_version }}"
CLI_CACHE_VERSION=""
if [ "$CLI_VERSION_INPUT" = "latest" ]; then
echo "📦 Resolving actual Salesforce CLI version for cache..."
# OPTIMIZATION: Fetch version from npm instead of installing
# This prevents a full install just to check the version number
# Check for timeout command (gnu coreutils)
if command -v timeout >/dev/null 2>&1; then
ACTUAL_VERSION=$(timeout 10 npm view @salesforce/cli version 2>/dev/null || true)
else
ACTUAL_VERSION=$(npm view @salesforce/cli version 2>/dev/null || true)
fi
if [ -z "$ACTUAL_VERSION" ]; then
# Use monthly rotation for cache when npm is unreachable
ACTUAL_VERSION="latest-$(date +%Y-%m)"
echo "⚠️ Could not resolve version from npm. Using time-based cache: $ACTUAL_VERSION"
echo "💡 Cache will rotate monthly to ensure freshness"
fi
echo " Detected CLI version: $ACTUAL_VERSION"
case "${{ inputs.cli_version_for_cache }}" in
major)
CLI_CACHE_VERSION=$(echo "$ACTUAL_VERSION" | cut -d. -f1)
;;
minor)
CLI_CACHE_VERSION=$(echo "$ACTUAL_VERSION" | cut -d. -f1-2)
;;
exact)
CLI_CACHE_VERSION="$ACTUAL_VERSION"
;;
*)
CLI_CACHE_VERSION=$(echo "$ACTUAL_VERSION" | cut -d. -f1-2)
;;
esac
else
case "${{ inputs.cli_version_for_cache }}" in
major)
CLI_CACHE_VERSION=$(echo "$CLI_VERSION_INPUT" | cut -d. -f1)
;;
minor)
CLI_CACHE_VERSION=$(echo "$CLI_VERSION_INPUT" | cut -d. -f1-2)
;;
exact)
CLI_CACHE_VERSION="$CLI_VERSION_INPUT"
;;
*)
CLI_CACHE_VERSION=$(echo "$CLI_VERSION_INPUT" | cut -d. -f1-2)
;;
esac
fi
echo "cli_cache_version=$CLI_CACHE_VERSION" >> $GITHUB_OUTPUT
echo " ✅ Cache key uses CLI version: $CLI_CACHE_VERSION"
echo " ✅ Tools hash: $TOOLS_HASH"
- name: Cache Salesforce CLI and plugins (Unix)
if: runner.os != 'Windows'
uses: actions/cache@v4
with:
path: |
~/.npm
~/.local/share/sf
~/.cache/node-gyp
key: sf-v3-${{ runner.os }}-node${{ inputs.node_version }}-cli${{ steps.cache-key.outputs.cli_cache_version }}-tools${{ steps.cache-key.outputs.tools_hash }}
restore-keys: |
sf-v3-${{ runner.os }}-node${{ inputs.node_version }}-cli${{ steps.cache-key.outputs.cli_cache_version }}-
sf-v3-${{ runner.os }}-node${{ inputs.node_version }}-
- name: Cache Salesforce CLI and plugins (Windows)
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: |
~/AppData/Local/sf
~/AppData/Roaming/npm
key: sf-v3-${{ runner.os }}-node${{ inputs.node_version }}-cli${{ steps.cache-key.outputs.cli_cache_version }}-tools${{ steps.cache-key.outputs.tools_hash }}
restore-keys: |
sf-v3-${{ runner.os }}-node${{ inputs.node_version }}-cli${{ steps.cache-key.outputs.cli_cache_version }}-
sf-v3-${{ runner.os }}-node${{ inputs.node_version }}-
- name: Install Salesforce CLI
id: install-cli
shell: bash
run: |
set -euo pipefail
if ! command -v sf &> /dev/null; then
echo "📦 Installing Salesforce CLI..."
MAX_RETRIES=3
RETRY_COUNT=0
BASE_DELAY=5
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
# Calculate exponential backoff: 5s, 10s, 20s
DELAY=$((BASE_DELAY * (2 ** RETRY_COUNT)))
echo " Attempt $((RETRY_COUNT + 1)) of $MAX_RETRIES..."
if [ "${{ inputs.cli_version }}" = "latest" ]; then
INSTALL_CMD="npm install -g @salesforce/cli"
else
INSTALL_CMD="npm install -g @salesforce/cli@${{ inputs.cli_version }}"
fi
if $INSTALL_CMD; then
echo " ✅ Salesforce CLI installed successfully"
break
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo " ⚠️ Installation failed, waiting ${DELAY}s before retry..."
sleep $DELAY
else
echo "❌ Failed to install Salesforce CLI after $MAX_RETRIES attempts"
echo "💡 This may be due to:"
echo " - Network connectivity issues"
echo " - npm registry availability"
echo " - GitHub Actions rate limiting"
echo " - Retry the workflow or use a specific CLI version"
exit 1
fi
done
else
echo "✅ Salesforce CLI already installed"
fi
CLI_VERSION=$(sf --version | head -n 1)
echo "📊 Installed: $CLI_VERSION"
echo "cli_version=$CLI_VERSION" >> $GITHUB_OUTPUT
# Capture and output CLI binary path (Forward Compatibility for v4)
CLI_PATH=$(command -v sf)
echo "cli_path=$CLI_PATH" >> $GITHUB_OUTPUT
echo "📍 CLI Binary: $CLI_PATH"
# Inline Health Check
if ! sf plugins --core >/dev/null 2>&1; then
echo "❌ CLI installed but not responding correctly"
exit 1
fi
echo "✅ CLI verified and functional"
# --------------------------------------------------------------------------
# SOURCE DIRECTORY RESOLUTION
# --------------------------------------------------------------------------
- name: Resolve Source Directories
id: resolve-source-dirs
shell: bash
run: |
set -euo pipefail
RAW_INPUT="${{ inputs.source_dirs }}"
[ -z "$RAW_INPUT" ] && RAW_INPUT="force-app"
IFS=',' read -ra DIRS <<< "$RAW_INPUT"
FLAGS=""
HAS_VALID_DIR=false
echo "📁 Resolving source directories..."
for dir in "${DIRS[@]}"; do
dir=$(echo "$dir" | xargs)
if [ -d "$dir" ]; then
FLAGS="$FLAGS --source-dir $dir"
HAS_VALID_DIR=true
echo " ✅ Found: $dir"
elif [ "$dir" == "force-app" ]; then
echo " ⚠️ Default directory '$dir' not found. Creating it..."
mkdir -p "$dir"
FLAGS="$FLAGS --source-dir $dir"
HAS_VALID_DIR=true
echo " ✅ Created: $dir"
else
echo " ⚠️ Warning: Directory '$dir' not found"
fi
done
if [ "$HAS_VALID_DIR" = "false" ]; then
echo "❌ Error: No valid source directories found."
echo ""
echo "💡 Tips:"
echo " • Default 'force-app' is auto-created if missing"
echo " • Custom directories must exist in your repository"
echo " • Check your source_dirs input for typos"
exit 1
fi
echo "source_flags=$FLAGS" >> $GITHUB_OUTPUT
echo "✅ Source directories resolved"
# --------------------------------------------------------------------------
# INPUT VALIDATION
# --------------------------------------------------------------------------
- name: Validate authentication inputs
if: inputs.skip_auth != 'true' && inputs.dry_run != 'true'
shell: bash
run: |
set -euo pipefail
echo "🔐 Validating authentication inputs..."
echo " Auth method: ${{ inputs.auth_method }}"
MISSING_INPUTS=()
case "${{ inputs.auth_method }}" in
jwt)
echo " Using JWT authentication"
[ -z "${{ inputs.jwt_key }}" ] && MISSING_INPUTS+=("jwt_key")
[ -z "${{ inputs.client_id }}" ] && MISSING_INPUTS+=("client_id")
[ -z "${{ inputs.username }}" ] && MISSING_INPUTS+=("username")
;;
sfdx-url)
echo " Using SFDX Auth URL authentication"
[ -z "${{ inputs.sfdx_auth_url }}" ] && MISSING_INPUTS+=("sfdx_auth_url")
;;
access-token)
echo " Using Direct Access Token authentication"
if [ "${{ inputs.auth_method }}" = "access-token" ] && [ "${{ inputs.allow_access_token_auth }}" != "true" ]; then
echo "❌ Error: access-token authentication is disabled by default"
echo " Set allow_access_token_auth: true to explicitly enable it"
exit 1
fi
[ -z "${{ inputs.access_token }}" ] && MISSING_INPUTS+=("access_token")
[ -z "${{ inputs.instance_url }}" ] && MISSING_INPUTS+=("instance_url")
;;
*)
echo "❌ Error: Invalid auth_method '${{ inputs.auth_method }}'"
echo " Must be one of: jwt, sfdx-url, access-token"
exit 1
;;
esac
if [ ${#MISSING_INPUTS[@]} -gt 0 ]; then
echo "❌ Error: The following required inputs are missing for ${{ inputs.auth_method }} authentication:"
printf ' - %s\\n' "${MISSING_INPUTS[@]}"
exit 1
fi
echo "✅ Authentication inputs validated"
# --------------------------------------------------------------------------
# AUTHENTICATION
# --------------------------------------------------------------------------
- name: Authenticate Org (JWT)
if: inputs.skip_auth != 'true' && inputs.dry_run != 'true' && inputs.auth_method == 'jwt'
shell: bash
env:
JWT_KEY: ${{ inputs.jwt_key }}
run: |
set -euo pipefail
echo "🔐 Authenticating via JWT..."
echo " Client ID: ${{ inputs.client_id && '***' || '(not set)' }}"
echo " Username: ${{ inputs.username }}"
echo " Instance URL: ${{ inputs.instance_url }}"
echo " Alias: ${{ inputs.alias }}"
# Create JWT key file with secure permissions using env var
echo "$JWT_KEY" > jwt.key
# Set secure permissions
# Set secure permissions (Critical for Unix)
if [[ "${{ runner.os }}" != "Windows" ]]; then
chmod 600 jwt.key
fi
# Cleanup function
cleanup() {
echo " 🧹 Cleaning up JWT key file..."
rm -f jwt.key
}
trap cleanup EXIT
# Add dev hub or default flag
if [ "${{ inputs.is_dev_hub }}" = "true" ]; then
echo " 🔧 Setting as Dev Hub..."
sf org login jwt \
--client-id "${{ inputs.client_id }}" \
--jwt-key-file jwt.key \
--username "${{ inputs.username }}" \
--instance-url "${{ inputs.instance_url }}" \
--alias "${{ inputs.alias }}" \
--set-default-dev-hub
else
echo " 🔧 Setting as default org..."
sf org login jwt \
--client-id "${{ inputs.client_id }}" \
--jwt-key-file jwt.key \
--username "${{ inputs.username }}" \
--instance-url "${{ inputs.instance_url }}" \
--alias "${{ inputs.alias }}" \
--set-default
fi
echo "✅ JWT authentication successful"
- name: Authenticate Org (SFDX Auth URL)
if: inputs.skip_auth != 'true' && inputs.dry_run != 'true' && inputs.auth_method == 'sfdx-url'
shell: bash
env:
SFDX_AUTH_URL: ${{ inputs.sfdx_auth_url }}
run: |
set -euo pipefail
echo "🔐 Authenticating via SFDX Auth URL..."
echo " Alias: ${{ inputs.alias }}"
# Create auth URL file with secure permissions using env var
echo "$SFDX_AUTH_URL" > authurl.txt
# Set secure permissions
if [[ "${{ runner.os }}" != "Windows" ]]; then
chmod 600 authurl.txt
fi
# Cleanup function
cleanup() {
echo " 🧹 Cleaning up auth URL file..."
rm -f authurl.txt
}
trap cleanup EXIT
# Authenticate using SFDX auth URL
AUTH_CMD="sf org login sfdx-url \
--sfdx-url-file authurl.txt \
--alias ${{ inputs.alias }}"
# Add dev hub or default flag
if [ "${{ inputs.is_dev_hub }}" = "true" ]; then
echo " 🔧 Setting as Dev Hub..."
$AUTH_CMD --set-default-dev-hub
else
echo " 🔧 Setting as default org..."
$AUTH_CMD --set-default
fi
echo "✅ SFDX Auth URL authentication successful"
- name: Authenticate Org (Access Token)
if: inputs.skip_auth != 'true' && inputs.dry_run != 'true' && inputs.auth_method == 'access-token'
shell: bash
env:
SF_ACCESS_TOKEN: ${{ inputs.access_token }}
run: |
set -euo pipefail
echo "🔐 Authenticating via Direct Access Token..."
echo " ⚠️ WARNING: Access tokens are short-lived and recommended for advanced use only"
echo " ⚠️ For production workflows, use JWT or SFDX Auth URL authentication"
echo " Instance URL: ${{ inputs.instance_url }}"
echo " Alias: ${{ inputs.alias }}"
# Use SF_ACCESS_TOKEN environment variable (per Salesforce CLI documentation)
# The --no-prompt flag reads from SF_ACCESS_TOKEN env var
sf org login access-token \
--instance-url "${{ inputs.instance_url }}" \
--alias ${{ inputs.alias }} \
--no-prompt
# Add dev hub or default flag
if [ "${{ inputs.is_dev_hub }}" = "true" ]; then
echo " 🔧 Setting as Dev Hub..."
sf config set target-dev-hub=${{ inputs.alias }} --global
else
echo " 🔧 Setting as default org..."
sf config set target-org=${{ inputs.alias }} --global
fi
echo "✅ Access Token authentication successful"
- name: Detect Org Details
id: detect-org
if: inputs.skip_auth != 'true' && inputs.dry_run != 'true'
shell: bash
run: |
set -euo pipefail
echo "🔍 Retrieving org details..."
DETAILS=$(sf org display --target-org ${{ inputs.alias }} --json)
# Extract org information with better fallbacks
ORG_ID=$(echo "$DETAILS" | jq -r '.result.id // .result.orgId // "unknown"')
ORG_EDITION=$(echo "$DETAILS" | jq -r '.result.edition // "unknown"')
USERNAME=$(echo "$DETAILS" | jq -r '.result.username // "unknown"')
INSTANCE_URL=$(echo "$DETAILS" | jq -r '.result.instanceUrl // "unknown"')
API_VERSION=$(echo "$DETAILS" | jq -r '.result.apiVersion // "unknown"')
# Try multiple fields for sandbox detection
IS_SANDBOX=$(echo "$DETAILS" | jq -r '.result.isSandbox // false | tostring')
IS_SCRATCH=$(echo "$DETAILS" | jq -r '.result.isScratchOrg // false | tostring')
# Additional check: sandbox orgs often have instanceUrl containing "cs" or "sandbox"
if [ "$IS_SANDBOX" = "false" ] && [[ "$INSTANCE_URL" =~ (cs[0-9]+|sandbox) ]]; then
echo " ℹ️ Detected sandbox from instance URL pattern"
IS_SANDBOX="true"
fi
# Check org namespace/status which can indicate sandbox
ORG_STATUS=$(echo "$DETAILS" | jq -r '.result.status // "unknown"')
ORG_TYPE_RAW=$(echo "$DETAILS" | jq -r '.result.organizationType // "unknown"')
# Determine org type with improved logic
if [ "$IS_SCRATCH" = "true" ]; then
ORG_TYPE="Scratch"
elif [ "$IS_SANDBOX" = "true" ]; then
ORG_TYPE="Sandbox"
elif [[ "$ORG_TYPE_RAW" =~ [Ss]andbox ]]; then
ORG_TYPE="Sandbox"
elif [[ "$ORG_EDITION" =~ Developer ]]; then
# Developer editions are typically production but worth noting
ORG_TYPE="Production"
else
ORG_TYPE="Production"
fi
# Set outputs
echo "org_id=$ORG_ID" >> $GITHUB_OUTPUT
echo "org_edition=$ORG_EDITION" >> $GITHUB_OUTPUT
echo "org_type=$ORG_TYPE" >> $GITHUB_OUTPUT
echo "username=$USERNAME" >> $GITHUB_OUTPUT
echo "instance_url=$INSTANCE_URL" >> $GITHUB_OUTPUT
echo "api_version=$API_VERSION" >> $GITHUB_OUTPUT
# Display summary
echo ""
echo "📊 Org Details Summary:"
echo " ├─ Type: $ORG_TYPE"
echo " ├─ Edition: $ORG_EDITION"
echo " ├─ Org ID: $ORG_ID"
echo " ├─ Username: $USERNAME"
echo " ├─ Instance: $INSTANCE_URL"
echo " └─ API Version: $API_VERSION"
echo ""
if [ "$ORG_TYPE" = "Production" ] && [[ "$INSTANCE_URL" =~ (cs[0-9]+|sandbox) ]]; then
echo "⚠️ WARNING: Org type detected as Production but instance URL suggests Sandbox"
echo " If this is incorrect, please report this issue with your org details"
fi
- name: Set Authentication Status
id: auth-status
shell: bash
run: |
set -euo pipefail
if [ "${{ inputs.skip_auth }}" = "true" ] || [ "${{ inputs.dry_run }}" = "true" ]; then
echo "auth_performed=false" >> $GITHUB_OUTPUT
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "ℹ️ Authentication skipped (dry-run mode)"
else
echo "ℹ️ Authentication skipped"
fi
else
echo "auth_performed=true" >> $GITHUB_OUTPUT
echo "✅ Authentication completed"
fi
- name: Track Default Usage
id: track-defaults
shell: bash
run: |
set -euo pipefail
echo "📊 Tracking default usage..."
# Track Node.js version default
if [ "${{ inputs.node_version }}" = "20" ]; then
echo "used_default_node=true" >> $GITHUB_OUTPUT
echo " ⚠️ Using default Node.js version: 20"
else
echo "used_default_node=false" >> $GITHUB_OUTPUT
echo " ✅ Using explicit Node.js version: ${{ inputs.node_version }}"
fi
# Track CLI version default
if [ "${{ inputs.cli_version }}" = "latest" ]; then
echo "used_default_cli_version=true" >> $GITHUB_OUTPUT
echo " ⚠️ Using default CLI version: latest"
else
echo "used_default_cli_version=false" >> $GITHUB_OUTPUT
echo " ✅ Using explicit CLI version: ${{ inputs.cli_version }}"
fi
# API version is always auto-detected in current implementation
# API version is always auto-detected in current implementation
echo "used_default_api_version=true" >> $GITHUB_OUTPUT
echo " ℹ️ API version auto-detected from org"
# Generate JSON Configuration Summary (Forward Compatibility for v4)
# This provides a structured audit record of the final configuration
CONFIG_JSON="{\"node_version\":\"${{ inputs.node_version }}\",\"cli_version\":\"${{ inputs.cli_version }}\",\"auth_method\":\"${{ inputs.auth_method }}\",\"skip_auth\":${{ inputs.skip_auth }},\"dry_run\":${{ inputs.dry_run }}}"
echo "config_json=$CONFIG_JSON" >> $GITHUB_OUTPUT
# --------------------------------------------------------------------------
# OPTIONAL TOOLING (with strict mode awareness)
# --------------------------------------------------------------------------
- name: Install sfdx-git-delta
if: inputs.install_delta == 'true'
shell: bash
run: |
set -eu # Always fail on undefined variables
if [ "${{ inputs.strict }}" = "true" ]; then
set -o pipefail
fi
if ! sf plugins inspect sfdx-git-delta >/dev/null 2>&1; then
echo "📦 Installing sfdx-git-delta..."
MAX_RETRIES=3
RETRY_COUNT=0
BASE_DELAY=5
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
DELAY=$((BASE_DELAY * (2 ** RETRY_COUNT)))
echo " Attempt $((RETRY_COUNT + 1)) of $MAX_RETRIES..."
if echo y | sf plugins install sfdx-git-delta 2>&1; then
echo " ✅ sfdx-git-delta installed"
break
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo " ⚠️ Installation failed, waiting ${DELAY}s before retry..."
sleep $DELAY
else
echo " ❌ Failed to install sfdx-git-delta after $MAX_RETRIES attempts"
if [ "${{ inputs.strict }}" = "true" ]; then
exit 1
fi
fi
done
else
echo "✅ sfdx-git-delta already installed"
fi
- name: Install Salesforce Code Analyzer
if: inputs.install_scanner == 'true'
shell: bash
run: |
set -eu # Always fail on undefined variables
if [ "${{ inputs.strict }}" = "true" ]; then
set -o pipefail
fi
if ! sf plugins inspect @salesforce/plugin-code-analyzer >/dev/null 2>&1; then
echo "📦 Installing @salesforce/plugin-code-analyzer..."
MAX_RETRIES=3
RETRY_COUNT=0
BASE_DELAY=5
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
DELAY=$((BASE_DELAY * (2 ** RETRY_COUNT)))
echo " Attempt $((RETRY_COUNT + 1)) of $MAX_RETRIES..."
if echo y | sf plugins install @salesforce/plugin-code-analyzer 2>&1; then
echo " ✅ Code Analyzer installed"
break
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo " ⚠️ Installation failed, waiting ${DELAY}s before retry..."
sleep $DELAY
else
echo " ❌ Failed to install Code Analyzer after $MAX_RETRIES attempts"
if [ "${{ inputs.strict }}" = "true" ]; then
exit 1
fi
fi
done
else
echo "✅ Code Analyzer already installed"
fi
- name: Install Custom Salesforce CLI Plugins
if: inputs.custom_sf_plugins != ''
shell: bash
run: |
set -eu # Always fail on undefined variables
if [ "${{ inputs.strict }}" = "true" ]; then
set -o pipefail
fi
echo "📦 Installing custom Salesforce CLI plugins..."
IFS=',' read -ra PLUGIN_LIST <<< "${{ inputs.custom_sf_plugins }}"
FAILED_PLUGINS=()
INSTALLED_PLUGINS=()
SKIPPED_PLUGINS=()
for plugin in "${PLUGIN_LIST[@]}"; do
# Trim whitespace
plugin=$(echo "$plugin" | xargs)
# Skip empty entries
if [ -z "$plugin" ]; then
continue
fi
# Validate plugin name format
# Accepts: @scope/plugin-name OR plugin-name
# Rejects: @scope/, @scope, @@invalid, @/name, etc.
# Supports dots in package names (e.g. my.plugin)
if ! echo "$plugin" | grep -qE '^(@[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+|[a-zA-Z0-9@._-]+)$'; then
echo " ⚠️ Invalid plugin name format: '$plugin'"
echo " Valid formats:"
echo " - Scoped: @scope/plugin-name"
echo " - Unscoped: plugin-name"
echo " Must contain only: letters, numbers, hyphens, underscores"
FAILED_PLUGINS+=("$plugin (invalid format)")
if [ "${{ inputs.strict }}" = "true" ]; then
exit 1
fi
continue
fi
# Check if already installed
if sf plugins inspect "$plugin" >/dev/null 2>&1; then
echo " ✅ $plugin already installed"
SKIPPED_PLUGINS+=("$plugin")
continue
fi
# Install the plugin with retry logic
echo " 📦 Installing: $plugin"
PLUGIN_RETRIES=3
PLUGIN_ATTEMPT=0
PLUGIN_DELAY=5
INSTALL_SUCCESS=false
while [ $PLUGIN_ATTEMPT -lt $PLUGIN_RETRIES ]; do
CURRENT_DELAY=$((PLUGIN_DELAY * (2 ** PLUGIN_ATTEMPT)))
if echo y | sf plugins install "$plugin" 2>&1; then
echo " ✅ Installed successfully"
INSTALLED_PLUGINS+=("$plugin")
INSTALL_SUCCESS=true
break
fi
PLUGIN_ATTEMPT=$((PLUGIN_ATTEMPT + 1))
if [ $PLUGIN_ATTEMPT -lt $PLUGIN_RETRIES ]; then
echo " ⚠️ Attempt $PLUGIN_ATTEMPT failed, retrying in ${CURRENT_DELAY}s..."
sleep $CURRENT_DELAY
fi
done
if [ "$INSTALL_SUCCESS" = "false" ]; then
echo " ❌ Failed to install after $PLUGIN_RETRIES attempts"
FAILED_PLUGINS+=("$plugin")
if [ "${{ inputs.strict }}" = "true" ]; then
exit 1
fi
fi
done
# Summary
echo ""
echo "📊 Plugin Installation Summary:"
echo " ├─ Installed: ${#INSTALLED_PLUGINS[@]}"
echo " ├─ Already Present: ${#SKIPPED_PLUGINS[@]}"
echo " └─ Failed: ${#FAILED_PLUGINS[@]}"
if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then
echo ""
echo "⚠️ Failed Plugins:"
printf ' - %s\n' "${FAILED_PLUGINS[@]}"
if [ "${{ inputs.strict }}" = "true" ]; then
echo ""
echo "❌ Strict mode enabled: Failing workflow due to plugin errors"
exit 1
else
echo ""
echo "💡 Non-strict mode: Continuing despite failures"
echo " Set strict: 'true' to fail on plugin errors"
fi