-
Notifications
You must be signed in to change notification settings - Fork 699
Expand file tree
/
Copy pathkeystore-manager.sh
More file actions
2117 lines (1821 loc) · 73.3 KB
/
Copy pathkeystore-manager.sh
File metadata and controls
2117 lines (1821 loc) · 73.3 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
#!/bin/bash
# Android Keystore Generator and GitHub Secrets Management Script
# This script generates Android keystores and manages GitHub secrets
set -e # Exit on any error
# Colors for better readability
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Print helper functions
print_success() {
echo -e "${GREEN}✓ $1${NC}"
}
print_error() {
echo -e "${RED}✗ $1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠ $1${NC}"
}
print_info() {
echo -e "${CYAN}ℹ $1${NC}"
}
# Default environment file paths (secrets/ is preferred, root is legacy)
SECRETS_DIR_ENV_FILE="secrets/secrets.env"
ROOT_ENV_FILE="secrets.env"
ENV_FILE="" # Will be resolved after argument parsing
# Default values
COMMAND="generate"
REPO=""
ENV=""
SECRET_NAME=""
ENV_FILE_OVERRIDE="" # User-specified --env-file path
# Keys that should not be sent to GitHub
EXCLUDED_GITHUB_KEYS=(
"COMPANY_NAME"
"DEPARTMENT"
"ORGANIZATION"
"CITY"
"STATE"
"COUNTRY"
"VALIDITY"
"KEYALG"
"KEYSIZE"
"OVERWRITE"
"ORIGINAL_KEYSTORE_NAME"
"UPLOAD_KEYSTORE_NAME"
"CN"
"OU"
"O"
"L"
"ST"
"C"
)
# Global associative array for iOS string secrets
declare -A IOS_STRING_SECRETS
# Function to strip quotes from values
strip_quotes() {
local value="$1"
# Remove surrounding double quotes if present
value="${value#\"}"
value="${value%\"}"
# Remove surrounding single quotes if present
value="${value#\'}"
value="${value%\'}"
echo "$value"
}
# Resolve which secrets.env file to use
# Priority: --env-file override > secrets/secrets.env > root secrets.env
resolve_env_file() {
# If user specified --env-file, use that
if [[ -n "$ENV_FILE_OVERRIDE" ]]; then
ENV_FILE="$ENV_FILE_OVERRIDE"
return 0
fi
local secrets_dir_exists=false
local root_exists=false
[[ -f "$SECRETS_DIR_ENV_FILE" ]] && secrets_dir_exists=true
[[ -f "$ROOT_ENV_FILE" ]] && root_exists=true
# Both exist - ask user to choose
if [[ "$secrets_dir_exists" = true ]] && [[ "$root_exists" = true ]]; then
echo -e "${YELLOW}Found secrets.env in two locations:${NC}"
echo -e " ${CYAN}[1]${NC} secrets/secrets.env (recommended)"
echo -e " ${CYAN}[2]${NC} secrets.env (legacy/root)"
echo ""
read -r -p "Which file should be used? [1/2] (default: 1): " choice
case "$choice" in
2)
ENV_FILE="$ROOT_ENV_FILE"
print_info "Using root: $ROOT_ENV_FILE"
;;
*)
ENV_FILE="$SECRETS_DIR_ENV_FILE"
print_info "Using secrets dir: $SECRETS_DIR_ENV_FILE"
;;
esac
elif [[ "$secrets_dir_exists" = true ]]; then
ENV_FILE="$SECRETS_DIR_ENV_FILE"
elif [[ "$root_exists" = true ]]; then
ENV_FILE="$ROOT_ENV_FILE"
else
# Neither exists - default to secrets/ (will be created by generate/sync)
ENV_FILE="$SECRETS_DIR_ENV_FILE"
fi
}
# Load variables from secrets.env if it exists (simple variables only)
load_env_vars() {
local env_file="$1"
local show_message="$2"
if [ -f "$env_file" ]; then
if [ "$show_message" = "true" ]; then
echo -e "${BLUE}Loading configuration from $env_file${NC}"
fi
# Only load simple variables (KEY=VALUE format), ignore multiline blocks
local in_multiline=false
local multiline_end=""
while IFS= read -r line; do
# Skip comments and blank lines
if [ "$in_multiline" = false ] && [[ -z "$line" || "$line" == \#* ]]; then
continue
fi
# Check if we're entering a multiline block
if [ "$in_multiline" = false ] && [[ "$line" == *"<<"* ]]; then
multiline_end=$(echo "$line" | sed 's/.*<<\(.*\)/\1/')
in_multiline=true
continue
fi
# Check if we're exiting a multiline block
if [ "$in_multiline" = true ] && [[ "$line" == "$multiline_end" ]]; then
in_multiline=false
continue
fi
# Skip lines inside multiline blocks
if [ "$in_multiline" = true ]; then
continue
fi
# Process regular KEY=VALUE pairs
if [[ "$line" == *"="* ]]; then
# Extract the variable name
local key=$(echo "$line" | cut -d '=' -f1 | xargs)
# Extract the value (anything after the first =)
local value=$(echo "$line" | cut -d '=' -f2-)
# Export the variable
export "$key"="$value"
fi
done < "$env_file"
fi
}
# Function to display help
show_help() {
echo -e "${BLUE}Android Keystore Generator and GitHub Secrets Management Script${NC}"
echo ""
echo "Usage:"
echo " ./keystore-manager.sh [COMMAND] [OPTIONS]"
echo ""
echo "Commands:"
echo " generate - Generate Android keystores and update secrets.env (default)"
echo " encode-secrets - Encode files from secrets/ directory and update secrets.env"
echo " sync - Validate secrets.env format and completeness"
echo " view - View all secrets in the secrets.env file as a formatted table"
echo " add - Add secrets to a GitHub repository from secrets.env"
echo " list - List all secrets in a GitHub repository"
echo " delete - Delete a secret from a GitHub repository"
echo " delete-all - Delete all secrets from a GitHub repository that are in secrets.env"
echo " Use --include-excluded flag to also delete excluded secrets"
echo " help - Show this help message"
echo ""
echo "Options:"
echo " --repo=username/repo - GitHub repository name"
echo " --env=environment - GitHub environment name"
echo " --name=SECRET_NAME - Secret name (for delete command)"
echo " --env-file=path - Override secrets.env file path"
echo " (default: secrets/secrets.env, fallback: secrets.env)"
echo ""
echo "Examples:"
echo " ./keystore-manager.sh generate"
echo " ./keystore-manager.sh encode-secrets"
echo " ./keystore-manager.sh sync"
echo " ./keystore-manager.sh view"
echo " ./keystore-manager.sh add --repo=username/repo"
echo " ./keystore-manager.sh list --repo=username/repo"
echo " ./keystore-manager.sh delete --repo=username/repo --name=SECRET_NAME"
echo " ./keystore-manager.sh delete-all --repo=username/repo [--env=environment]"
echo " ./keystore-manager.sh delete-all --repo=username/repo [--env=environment] --include-excluded"
}
# Function to view secrets from secrets.env in a table
view_secrets() {
if [ ! -f "$ENV_FILE" ]; then
echo -e "${RED}Error: $ENV_FILE file not found.${NC}"
exit 1
fi
echo -e "${BLUE}Loading configuration from $ENV_FILE${NC}"
echo -e "${BLUE}Viewing secrets from $ENV_FILE${NC}"
echo ""
# Calculate column widths
KEY_WIDTH=30
VALUE_WIDTH=50
TOTAL_WIDTH=$((KEY_WIDTH + VALUE_WIDTH + 5)) # 5 for borders and spacing
# Function to print horizontal border
print_border() {
local char=$1
local width=$2
printf "${CYAN}%*s${NC}\n" "$width" | tr " " "$char"
}
# Print table header
print_border "═" $TOTAL_WIDTH
printf "${CYAN}║${BOLD} %-${KEY_WIDTH}s ${CYAN}║${BOLD} %-${VALUE_WIDTH}s ${CYAN}║${NC}\n" "SECRET KEY" "VALUE"
print_border "═" $TOTAL_WIDTH
# Process the file line by line with support for multiline values
local multiline_mode=false
local multiline_end=""
while IFS= read -r line || [ -n "$line" ]; do
# Skip empty lines and comments when not in multiline mode
if [ "$multiline_mode" = false ] && [[ -z "$line" || "$line" == \#* ]]; then
continue
fi
# Check if we're exiting a multiline block
if [ "$multiline_mode" = true ] && [[ "$line" == "$multiline_end" ]]; then
multiline_mode=false
continue
fi
# Skip content lines inside multiline blocks
if [ "$multiline_mode" = true ]; then
continue
fi
# Check if this is the start of a multiline value
if [[ "$line" == *"<<"* ]]; then
# Extract the key (part before <<)
local key=$(echo "$line" | cut -d '<' -f1 | xargs)
# Extract the delimiter (part after <<)
multiline_end=$(echo "$line" | sed 's/.*<<\(.*\)/\1/')
multiline_mode=true
# Print the multiline value immediately
printf "${CYAN}║${NC} ${YELLOW}%-${KEY_WIDTH}s${NC} ${CYAN}║${NC} ${GREEN}%-${VALUE_WIDTH}s${NC} ${CYAN}║${NC}\n" "$key" "[MULTILINE VALUE]"
elif [[ "$line" == *"="* ]]; then
# This is a regular key=value line
local key=$(echo "$line" | cut -d '=' -f1 | xargs)
local value=$(echo "$line" | cut -d '=' -f2-)
# Strip quotes for display
value=$(strip_quotes "$value")
# Truncate value if too long
local display_value=""
if [ ${#value} -gt $VALUE_WIDTH ]; then
display_value="${value:0:$((VALUE_WIDTH-5))}..."
else
display_value="$value"
fi
# Print the regular key-value pair
printf "${CYAN}║${NC} ${YELLOW}%-${KEY_WIDTH}s${NC} ${CYAN}║${NC} ${GREEN}%-${VALUE_WIDTH}s${NC} ${CYAN}║${NC}\n" "$key" "$display_value"
fi
done < "$ENV_FILE"
# Print table footer
print_border "═" $TOTAL_WIDTH
# Help message for multiline values
echo -e "${BLUE}Note: For multiline values, the content is displayed as [MULTILINE VALUE]${NC}"
}
# Function to check if keytool is available
check_keytool() {
if ! command -v keytool &> /dev/null; then
echo -e "${RED}Error: keytool command not found.${NC}"
echo -e "Please ensure you have Java Development Kit (JDK) installed and that keytool is in your PATH."
exit 1
fi
}
# Function to check if gh CLI is available
check_gh_cli() {
if ! command -v gh &> /dev/null; then
echo -e "${RED}GitHub CLI (gh) is not installed. Please install it first:${NC}"
echo -e "https://cli.github.com/manual/installation"
exit 1
fi
# Check if user is authenticated
if ! gh auth status &> /dev/null; then
echo -e "${RED}You are not logged in to GitHub CLI. Please run:${NC}"
echo -e "${BLUE}gh auth login${NC}"
exit 1
fi
}
# Function to create keystores directory
create_keystores_dir() {
if [ ! -d "keystores" ]; then
echo -e "${BLUE}Creating 'keystores' directory...${NC}"
mkdir -p keystores
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to create 'keystores' directory.${NC}"
exit 1
fi
fi
}
# Function to encode file to base64
encode_base64() {
local file_path=$1
if [ -f "$file_path" ]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS requires -i flag for input file
base64 -i "$file_path"
else
# Linux accepts positional argument and -w 0 for no wrapping
base64 -w 0 "$file_path"
fi
else
echo -e "${RED}Error: File not found: $file_path${NC}"
return 1
fi
}
# Function to create secrets directory if it doesn't exist
create_secrets_dir() {
if [ ! -d "secrets" ]; then
echo -e "${BLUE}Creating 'secrets' directory...${NC}"
mkdir -p secrets
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to create 'secrets' directory.${NC}"
exit 1
fi
fi
}
# Parse iOS string secrets from shared_keys.env
parse_shared_keys_env() {
local SHARED_KEYS_FILE="secrets/shared_keys.env"
# Skip if file doesn't exist (Android-only setup)
if [[ ! -f "$SHARED_KEYS_FILE" ]]; then
print_info "shared_keys.env not found - skipping iOS secrets (Android-only project)"
return 0
fi
print_info "Parsing iOS secrets from shared_keys.env..."
# Read MATCH_PASSWORD from .match_password file if it exists
local MATCH_PWD=""
if [[ -f "secrets/.match_password" ]]; then
MATCH_PWD=$(head -n1 secrets/.match_password 2>/dev/null | tr -d '\n\r')
print_success "Loaded MATCH_PASSWORD from .match_password file"
else
print_warning "secrets/.match_password not found - MATCH_PASSWORD will be empty"
fi
# Extract values from shared_keys.env
# Format: export KEY="value"
local APPSTORE_KEY_ID=$(grep '^export APPSTORE_KEY_ID=' "$SHARED_KEYS_FILE" 2>/dev/null | cut -d'"' -f2)
local APPSTORE_ISSUER_ID=$(grep '^export APPSTORE_ISSUER_ID=' "$SHARED_KEYS_FILE" 2>/dev/null | cut -d'"' -f2)
local NOTARIZATION_TEAM_ID=$(grep '^export TEAM_ID=' "$SHARED_KEYS_FILE" 2>/dev/null | cut -d'"' -f2)
local NOTARIZATION_APPLE_ID=$(grep '^export NOTARIZATION_APPLE_ID=' "$SHARED_KEYS_FILE" 2>/dev/null | cut -d'"' -f2)
local NOTARIZATION_PASSWORD=$(grep '^export NOTARIZATION_PASSWORD=' "$SHARED_KEYS_FILE" 2>/dev/null | cut -d'"' -f2)
# Validate critical values
if [[ -z "$APPSTORE_KEY_ID" ]]; then
print_warning "APPSTORE_KEY_ID is empty - App Store Connect API key may not be configured"
fi
if [[ -z "$APPSTORE_ISSUER_ID" ]]; then
print_warning "APPSTORE_ISSUER_ID is empty - App Store Connect API issuer may not be configured"
fi
# Populate global associative array (declared at top of script)
IOS_STRING_SECRETS["APPSTORE_KEY_ID"]="$APPSTORE_KEY_ID"
IOS_STRING_SECRETS["APPSTORE_ISSUER_ID"]="$APPSTORE_ISSUER_ID"
IOS_STRING_SECRETS["MATCH_PASSWORD"]="$MATCH_PWD"
IOS_STRING_SECRETS["NOTARIZATION_APPLE_ID"]="$NOTARIZATION_APPLE_ID"
IOS_STRING_SECRETS["NOTARIZATION_PASSWORD"]="$NOTARIZATION_PASSWORD"
IOS_STRING_SECRETS["NOTARIZATION_TEAM_ID"]="$NOTARIZATION_TEAM_ID"
# Print summary
local count=0
for key in "${!IOS_STRING_SECRETS[@]}"; do
if [[ -n "${IOS_STRING_SECRETS[$key]}" ]]; then
count=$((count + 1))
fi
done
print_success "Found $count of 6 iOS string secrets"
}
# Global associative array for macOS password secrets
declare -g -A MACOS_PASSWORD_SECRETS
# Parse macOS password secrets from dotfiles in secrets/
parse_macos_password_files() {
print_info "Parsing macOS password files from secrets/..."
local count=0
# Read KEYCHAIN_PASSWORD from .keychain_password file
if [[ -f "secrets/.keychain_password" ]]; then
local val
val=$(head -n1 secrets/.keychain_password 2>/dev/null | tr -d '\n\r')
if [[ -n "$val" ]]; then
MACOS_PASSWORD_SECRETS["KEYCHAIN_PASSWORD"]="$val"
count=$((count + 1))
print_success "Loaded KEYCHAIN_PASSWORD from .keychain_password file"
else
print_warning ".keychain_password file is empty"
fi
else
print_info "secrets/.keychain_password not found - KEYCHAIN_PASSWORD will remain as-is"
fi
# Read CERTIFICATES_PASSWORD from .certificates_password file
if [[ -f "secrets/.certificates_password" ]]; then
local val
val=$(head -n1 secrets/.certificates_password 2>/dev/null | tr -d '\n\r')
if [[ -n "$val" ]]; then
MACOS_PASSWORD_SECRETS["CERTIFICATES_PASSWORD"]="$val"
count=$((count + 1))
print_success "Loaded CERTIFICATES_PASSWORD from .certificates_password file"
else
print_warning ".certificates_password file is empty"
fi
else
print_info "secrets/.certificates_password not found - CERTIFICATES_PASSWORD will remain as-is"
fi
print_success "Found $count of 2 macOS password secrets"
}
# Update macOS password secrets in secrets.env
# Always ensures KEYCHAIN_PASSWORD and CERTIFICATES_PASSWORD exist in the file.
# Populates from password files if available, otherwise adds empty placeholders.
update_macos_password_secrets() {
local SECRETS_FILE="$ENV_FILE"
if [[ ! -f "$SECRETS_FILE" ]]; then
print_info "No secrets file to update macOS passwords in"
return 0
fi
print_info "Updating macOS password secrets in $SECRETS_FILE..."
# Keys we must ensure exist
local required_keys=("KEYCHAIN_PASSWORD" "CERTIFICATES_PASSWORD")
for key in "${required_keys[@]}"; do
# Get value from parsed password files (may be empty)
local value="${MACOS_PASSWORD_SECRETS[$key]:-}"
if grep -q "^${key}=" "$SECRETS_FILE" 2>/dev/null; then
# Key exists - update only if we have a non-empty value
if [[ -n "$value" ]]; then
local escaped_value
escaped_value=$(printf '%s\n' "$value" | sed 's/[&/\]/\\&/g')
sed -i.bak "s|^${key}=.*|${key}=\"${escaped_value}\"|" "$SECRETS_FILE"
print_success "Updated $key"
else
print_info "Preserving existing $key (no password file found)"
fi
else
# Key doesn't exist - add after macOS App Store section header (or end of file)
local escaped_value=""
[[ -n "$value" ]] && escaped_value=$(printf '%s\n' "$value" | sed 's/[&/\]/\\&/g')
local section_line
section_line=$(grep -n "^# macOS App Store" "$SECRETS_FILE" 2>/dev/null | head -1 | cut -d: -f1)
if [[ -n "$section_line" ]]; then
# Find end of comments block after section header
local insert_line=$((section_line + 1))
local total_lines
total_lines=$(wc -l < "$SECRETS_FILE")
# Skip past comment lines to find insertion point
while [[ $insert_line -le $total_lines ]]; do
local line_content
line_content=$(sed -n "${insert_line}p" "$SECRETS_FILE")
if [[ "$line_content" != \#* ]] && [[ -n "$line_content" ]]; then
break
fi
insert_line=$((insert_line + 1))
done
{
head -n $((insert_line - 1)) "$SECRETS_FILE"
echo "${key}=\"${escaped_value}\""
tail -n +${insert_line} "$SECRETS_FILE"
} > "${SECRETS_FILE}.tmp" && mv "${SECRETS_FILE}.tmp" "$SECRETS_FILE"
print_success "Added $key to macOS App Store section"
else
echo "${key}=\"${escaped_value}\"" >> "$SECRETS_FILE"
print_success "Appended $key to end of file"
fi
fi
done
rm -f "${SECRETS_FILE}.bak"
}
# Function to encode secrets directory files and update secrets.env
encode_secrets_directory_files() {
echo -e "${BLUE}==================================================================${NC}"
echo -e "${BLUE}Encoding files from secrets/ directory${NC}"
echo -e "${BLUE}==================================================================${NC}"
# Define mapping of file names to secret names
declare -A FILE_TO_SECRET_MAP
FILE_TO_SECRET_MAP["firebaseAppDistributionServiceCredentialsFile.json"]="FIREBASECREDS"
FILE_TO_SECRET_MAP["google-services.json"]="GOOGLESERVICES"
FILE_TO_SECRET_MAP["playStorePublishServiceCredentialsFile.json"]="PLAYSTORECREDS"
FILE_TO_SECRET_MAP["AuthKey.p8"]="APPSTORE_AUTH_KEY"
FILE_TO_SECRET_MAP["match_ci_key"]="MATCH_SSH_PRIVATE_KEY"
# macOS App Store certificates and provisioning profiles
FILE_TO_SECRET_MAP["mac_app_distribution.p12"]="MAC_APP_DISTRIBUTION_CERTIFICATE_B64"
FILE_TO_SECRET_MAP["mac_installer_distribution.p12"]="MAC_INSTALLER_DISTRIBUTION_CERTIFICATE_B64"
FILE_TO_SECRET_MAP["mac_embedded.provisionprofile"]="MAC_EMBEDDED_PROVISION_B64"
FILE_TO_SECRET_MAP["mac_runtime.provisionprofile"]="MAC_RUNTIME_PROVISION_B64"
local secrets_found=0
local secrets_encoded=0
declare -A ENCODED_SECRETS
# Check if secrets directory exists
if [ ! -d "secrets" ]; then
echo -e "${YELLOW}No 'secrets' directory found. Skipping secrets encoding.${NC}"
return 0
fi
# Scan secrets directory for known files
for file_name in "${!FILE_TO_SECRET_MAP[@]}"; do
local file_path="secrets/$file_name"
local secret_name="${FILE_TO_SECRET_MAP[$file_name]}"
if [ -f "$file_path" ]; then
secrets_found=$((secrets_found + 1))
echo -e "${BLUE}Found: $file_name${NC}"
echo -e "${BLUE}Encoding as: $secret_name${NC}"
local encoded=$(encode_base64 "$file_path")
if [ $? -eq 0 ]; then
ENCODED_SECRETS["$secret_name"]="$encoded"
secrets_encoded=$((secrets_encoded + 1))
echo -e "${GREEN}✓ Successfully encoded $file_name${NC}"
else
echo -e "${RED}✗ Failed to encode $file_name${NC}"
fi
fi
done
if [ $secrets_found -eq 0 ]; then
echo -e "${YELLOW}No known secret files found in secrets/ directory${NC}"
echo -e "${YELLOW}Looking for: firebaseAppDistributionServiceCredentialsFile.json, google-services.json, playStorePublishServiceCredentialsFile.json, AuthKey.p8, match_ci_key${NC}"
return 0
fi
if [ $secrets_encoded -eq 0 ]; then
echo -e "${RED}Failed to encode any secret files${NC}"
return 1
fi
# Update secrets.env file
echo -e "${BLUE}Updating $ENV_FILE with encoded files...${NC}"
update_secrets_env_with_files
echo -e "${GREEN}Encoded $secrets_encoded out of $secrets_found secret files${NC}"
return 0
}
# Function to update secrets.env with encoded secret files
update_secrets_env_with_files() {
if [ ! -f "$ENV_FILE" ]; then
echo -e "${YELLOW}$ENV_FILE not found. Secret files will not be added.${NC}"
return 0
fi
# Access the ENCODED_SECRETS array from parent scope
local temp_file="${ENV_FILE}.tmp"
local in_multiline=false
local multiline_end=""
local current_key=""
# Read existing secrets.env and track which sections exist
declare -A existing_sections
while IFS= read -r line || [ -n "$line" ]; do
if [ "$in_multiline" = false ] && [[ "$line" == *"<<EOF" ]]; then
current_key=$(echo "$line" | cut -d '<' -f1 | xargs)
existing_sections["$current_key"]=1
multiline_end="EOF"
in_multiline=true
elif [ "$in_multiline" = true ] && [[ "$line" == "$multiline_end" ]]; then
in_multiline=false
fi
done < "$ENV_FILE"
# Copy existing file and update/append sections
cp "$ENV_FILE" "$temp_file"
in_multiline=false
# For each encoded secret, update or append
for secret_name in "${!ENCODED_SECRETS[@]}"; do
local encoded_value="${ENCODED_SECRETS[$secret_name]}"
if [ -n "${existing_sections[$secret_name]}" ]; then
# Update existing section
echo -e "${BLUE}Updating existing section: $secret_name${NC}"
local temp_file2="${temp_file}.2"
local in_target_section=false
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" == "${secret_name}<<EOF" ]]; then
in_target_section=true
echo "$line" >> "$temp_file2"
echo "$encoded_value" >> "$temp_file2"
continue
fi
if [ "$in_target_section" = true ] && [[ "$line" == "EOF" ]]; then
in_target_section=false
echo "$line" >> "$temp_file2"
continue
fi
if [ "$in_target_section" = false ]; then
echo "$line" >> "$temp_file2"
fi
done < "$temp_file"
mv "$temp_file2" "$temp_file"
else
# Append new section
echo -e "${BLUE}Adding new section: $secret_name${NC}"
echo "" >> "$temp_file"
echo "${secret_name}<<EOF" >> "$temp_file"
echo "$encoded_value" >> "$temp_file"
echo "EOF" >> "$temp_file"
fi
done
# Replace original file
mv "$temp_file" "$ENV_FILE"
echo -e "${GREEN}$ENV_FILE updated successfully${NC}"
}
# Update iOS string secrets in secrets.env
update_ios_string_secrets() {
local SECRETS_FILE="$ENV_FILE"
# Check if secrets.env exists
if [[ ! -f "$SECRETS_FILE" ]]; then
print_warning "$SECRETS_FILE not found. Creating new file..."
mkdir -p "$(dirname "$SECRETS_FILE")"
touch "$SECRETS_FILE"
fi
# Skip if no iOS secrets extracted
if [[ ${#IOS_STRING_SECRETS[@]} -eq 0 ]]; then
print_info "No iOS secrets to update"
return 0
fi
print_info "Updating iOS string secrets in $SECRETS_FILE..."
# Check if iOS Configuration section exists
if grep -q "^# iOS Configuration" "$SECRETS_FILE" 2>/dev/null; then
print_info "iOS section exists - updating individual keys..."
update_ios_section
else
print_info "iOS section doesn't exist - appending new section..."
append_ios_section
fi
}
# Helper function to update existing iOS section
update_ios_section() {
local SECRETS_FILE="$ENV_FILE"
for key in "${!IOS_STRING_SECRETS[@]}"; do
local value="${IOS_STRING_SECRETS[$key]}"
# Check if key exists in file
if grep -q "^${key}=" "$SECRETS_FILE"; then
# Update existing key
if [[ -n "$value" ]]; then
# Replace with new value (escape special characters)
local escaped_value=$(printf '%s\n' "$value" | sed 's/[&/\]/\\&/g')
sed -i.bak "s|^${key}=.*|${key}=\"${escaped_value}\"|" "$SECRETS_FILE"
print_success "Updated $key"
else
# Keep existing value if new value is empty
print_info "Preserving existing $key (new value empty)"
fi
else
# Key doesn't exist - add it after iOS section header
local section_line=$(grep -n "^# iOS Configuration" "$SECRETS_FILE" | cut -d: -f1)
if [[ -n "$section_line" ]]; then
# Insert after the separator line following the header (portable approach)
local insert_line=$((section_line + 2))
local escaped_value=$(printf '%s\n' "$value" | sed 's/[&/\]/\\&/g')
{
head -n $((insert_line - 1)) "$SECRETS_FILE"
echo "${key}=\"${escaped_value}\""
tail -n +${insert_line} "$SECRETS_FILE"
} > "${SECRETS_FILE}.tmp" && mv "${SECRETS_FILE}.tmp" "$SECRETS_FILE"
print_success "Added $key to iOS section"
fi
fi
done
# Remove backup file
rm -f "${SECRETS_FILE}.bak"
}
# Helper function to append new iOS section
append_ios_section() {
local SECRETS_FILE="$ENV_FILE"
# Append new iOS section
cat >> "$SECRETS_FILE" << EOF
# ==============================================================================
# iOS Configuration
# ==============================================================================
# App Store Connect API Keys
APPSTORE_KEY_ID="${IOS_STRING_SECRETS[APPSTORE_KEY_ID]}"
APPSTORE_ISSUER_ID="${IOS_STRING_SECRETS[APPSTORE_ISSUER_ID]}"
# Fastlane Match
MATCH_PASSWORD="${IOS_STRING_SECRETS[MATCH_PASSWORD]}"
# macOS Notarization (for Desktop app distribution)
NOTARIZATION_APPLE_ID="${IOS_STRING_SECRETS[NOTARIZATION_APPLE_ID]}"
NOTARIZATION_PASSWORD="${IOS_STRING_SECRETS[NOTARIZATION_PASSWORD]}"
NOTARIZATION_TEAM_ID="${IOS_STRING_SECRETS[NOTARIZATION_TEAM_ID]}"
EOF
print_success "Appended iOS Configuration section"
}
# Add Desktop signing placeholders to secrets.env
add_desktop_placeholders() {
local SECRETS_FILE="$ENV_FILE"
# Check if file exists
if [[ ! -f "$SECRETS_FILE" ]]; then
print_error "File $SECRETS_FILE does not exist"
return 1
fi
# Check if Desktop Signing section exists
if grep -q "^# Desktop Signing" "$SECRETS_FILE" 2>/dev/null; then
print_info "Desktop Signing section already exists - skipping"
else
print_info "Adding Desktop Signing placeholder section..."
# Append Desktop section
if ! cat >> "$SECRETS_FILE" << 'EOF'
# ==============================================================================
# Desktop Signing (Optional)
# ==============================================================================
# These are optional for Desktop app distribution outside app stores.
# Populate when setting up code signing for Windows/macOS/Linux desktop apps.
# Windows Signing
WINDOWS_SIGNING_KEY=""
WINDOWS_SIGNING_PASSWORD=""
WINDOWS_SIGNING_CERTIFICATE=""
# macOS Signing (Desktop app, not iOS)
MACOS_SIGNING_KEY=""
MACOS_SIGNING_PASSWORD=""
MACOS_SIGNING_CERTIFICATE=""
# Linux Signing
LINUX_SIGNING_KEY=""
LINUX_SIGNING_PASSWORD=""
LINUX_SIGNING_CERTIFICATE=""
EOF
then
print_error "Failed to append Desktop Signing section"
return 1
fi
print_success "Added Desktop Signing placeholder section"
fi
# Add macOS App Store section if not present
if grep -q "^# macOS App Store" "$SECRETS_FILE" 2>/dev/null; then
print_info "macOS App Store section already exists - skipping"
else
print_info "Adding macOS App Store placeholder section..."
if ! cat >> "$SECRETS_FILE" << 'EOF'
# ==============================================================================
# macOS App Store (Required for macOS TestFlight & App Store deployment)
# ==============================================================================
# Keychain and certificate passwords for CI code signing.
# Place .p12 and .provisionprofile files in secrets/ directory, then run sync.
#
# Password files (read automatically by sync):
# secrets/.keychain_password → KEYCHAIN_PASSWORD
# secrets/.certificates_password → CERTIFICATES_PASSWORD
#
# Certificate/profile files (base64 encoded by sync):
# secrets/mac_app_distribution.p12 → MAC_APP_DISTRIBUTION_CERTIFICATE_B64
# secrets/mac_installer_distribution.p12 → MAC_INSTALLER_DISTRIBUTION_CERTIFICATE_B64
# secrets/mac_embedded.provisionprofile → MAC_EMBEDDED_PROVISION_B64
# secrets/mac_runtime.provisionprofile → MAC_RUNTIME_PROVISION_B64
EOF
then
print_error "Failed to append macOS App Store section"
return 1
fi
print_success "Added macOS App Store placeholder section"
fi
}
# Validate secrets.env format and completeness
validate_sync_result() {
local SECRETS_FILE="$ENV_FILE"
local exit_code=0
print_info "Validating $SECRETS_FILE..."
# Check if file exists
if [[ ! -f "$SECRETS_FILE" ]]; then
print_error "File $SECRETS_FILE does not exist"
return 1
fi
# Track validation issues
local format_errors=()
local missing_secrets=()
local invalid_base64=()
# ============================================================================
# 1. Check file format
# ============================================================================
print_info "Checking file format..."
# Check for GitHub Secrets Environment File header (generated by update_secrets_env)
if ! grep -q "^# GitHub Secrets Environment File" "$SECRETS_FILE"; then
format_errors+=("Missing GitHub Secrets Environment File header")
fi
# Check for iOS Configuration section header (if iOS project)
if [[ -f "secrets/shared_keys.env" ]]; then
if ! grep -q "^# iOS Configuration" "$SECRETS_FILE"; then
format_errors+=("Missing iOS configuration section header (iOS project detected)")
fi
fi
# Validate heredoc blocks are properly formatted
local in_heredoc=false
local heredoc_key=""
local heredoc_delimiter=""
local line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# Check for heredoc start
if [[ "$line" =~ ^([A-Z_]+)\<\<([A-Z]+)$ ]]; then
if [[ "$in_heredoc" = true ]]; then
format_errors+=("Line $line_num: Nested heredoc detected (unclosed $heredoc_key)")
fi
heredoc_key="${BASH_REMATCH[1]}"
heredoc_delimiter="${BASH_REMATCH[2]}"
in_heredoc=true
# Check for heredoc end
elif [[ "$in_heredoc" = true ]] && [[ "$line" == "$heredoc_delimiter" ]]; then
in_heredoc=false
heredoc_key=""
heredoc_delimiter=""
fi
done < "$SECRETS_FILE"
# Check if any heredoc was left unclosed
if [[ "$in_heredoc" = true ]]; then
format_errors+=("Unclosed heredoc block: $heredoc_key (missing $heredoc_delimiter)")
fi
# Check for duplicate keys using process substitution
local duplicates
duplicates=$(while IFS= read -r line; do
# Extract keys from both regular and heredoc formats
if [[ "$line" =~ ^([A-Z_]+)= ]] || [[ "$line" =~ ^([A-Z_]+)\<\< ]]; then
echo "${BASH_REMATCH[1]}"
fi
done < "$SECRETS_FILE" | sort | uniq -d)
if [[ -n "$duplicates" ]]; then
while IFS= read -r dup_key; do
if [[ -n "$dup_key" ]]; then
format_errors+=("Duplicate key found: $dup_key")
fi
done <<< "$duplicates"
fi
# Report format errors
if [[ ${#format_errors[@]} -gt 0 ]]; then
print_error "Format validation failed:"
for error in "${format_errors[@]}"; do
echo -e " ${RED}- $error${NC}"
done
if [[ $exit_code -eq 0 ]]; then exit_code=1; fi
else
print_success "File format is valid"
fi
# ============================================================================
# 2. Check required secrets
# ============================================================================
print_info "Checking required secrets..."
# Define required Android secrets
local required_android=(
"KEYSTORE_PASSWORD"
"KEYALIAS"
"KEY_PASSWORD"
"GOOGLESERVICES"
"PLAYSTORECREDS"
"FIREBASECREDS"
)
# Map alternative key names used in this project
declare -A key_aliases
key_aliases["KEYSTORE_PASSWORD"]="ORIGINAL_KEYSTORE_FILE_PASSWORD|UPLOAD_KEYSTORE_FILE_PASSWORD"
key_aliases["KEYALIAS"]="ORIGINAL_KEYSTORE_ALIAS|UPLOAD_KEYSTORE_ALIAS"
key_aliases["KEY_PASSWORD"]="ORIGINAL_KEYSTORE_ALIAS_PASSWORD|UPLOAD_KEYSTORE_ALIAS_PASSWORD"
# Check Android secrets
for secret in "${required_android[@]}"; do
local found=false
# Check direct key name
if grep -q "^${secret}=" "$SECRETS_FILE" || grep -q "^${secret}<<" "$SECRETS_FILE"; then
found=true
# Check alternative names
elif [[ -n "${key_aliases[$secret]}" ]]; then
IFS='|' read -ra alternatives <<< "${key_aliases[$secret]}"
for alt in "${alternatives[@]}"; do
if grep -q "^${alt}=" "$SECRETS_FILE" || grep -q "^${alt}<<" "$SECRETS_FILE"; then
found=true
break
fi
done
fi
if [[ "$found" = false ]]; then
missing_secrets+=("Android: $secret")
fi
done
# Check iOS secrets if iOS project detected
if [[ -f "secrets/shared_keys.env" ]]; then
local required_ios=(
"APPSTORE_KEY_ID"
"APPSTORE_ISSUER_ID"
"APPSTORE_AUTH_KEY"