-
Notifications
You must be signed in to change notification settings - Fork 0
498 lines (444 loc) · 19.5 KB
/
gitops-update.yml
File metadata and controls
498 lines (444 loc) · 19.5 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
name: "GitOps Update"
# Reusable workflow for updating GitOps repository with new image tags
# Supports multiple servers (Firmino, Clotilde) and environments: dev (beta), stg (rc), prd (production), sandbox
# Handles single or multiple components with flexible YAML key mapping
on:
workflow_call:
inputs:
gitops_repository:
description: 'GitOps repository to update (org/repo format)'
type: string
default: 'LerianStudio/midaz-firmino-gitops'
app_name:
description: 'Application name (defaults to repository name if not provided)'
type: string
required: false
deploy_in_firmino:
description: 'Deploy to Firmino server'
type: boolean
default: true
deploy_in_clotilde:
description: 'Deploy to Clotilde server'
type: boolean
default: true
artifact_pattern:
description: 'Pattern to download artifacts (defaults to "gitops-tags-<repo-name>-*" if not provided)'
type: string
required: false
yaml_key_mappings:
description: 'JSON object mapping artifact names to YAML keys (e.g., {"backend.tag": ".auth.image.tag", "frontend.tag": ".frontend.image.tag"})'
type: string
required: true
commit_message_prefix:
description: 'Prefix for commit message (defaults to repository name if not provided)'
type: string
required: false
runner_type:
description: 'GitHub runner type to use'
type: string
default: 'blacksmith-4vcpu-ubuntu-2404'
enable_argocd_sync:
description: 'Enable ArgoCD sync after GitOps update'
type: boolean
default: true
use_dynamic_mapping:
description: 'Use dynamic mapping for multiple components (like midaz)'
type: boolean
default: false
yq_version:
description: 'Version of yq to install'
type: string
default: 'v4.44.3'
enable_docker_login:
description: 'Enable Docker Hub login to avoid rate limits. Disabled by default since GitOps updates do not require Docker registry access.'
type: boolean
default: false
configmap_updates:
description: 'JSON object mapping artifact names to configmap keys (e.g., {"pix.tag": ".pix.configmap.VERSION"})'
type: string
required: false
jobs:
update_gitops:
runs-on: ${{ inputs.runner_type }}
outputs:
app_name: ${{ steps.setup.outputs.app_name }}
sync_matrix: ${{ steps.apply_tags.outputs.sync_matrix }}
has_sync_targets: ${{ steps.apply_tags.outputs.has_sync_targets }}
env:
IS_RC: ${{ contains(github.ref, '-rc.') }}
IS_BETA: ${{ contains(github.ref, '-beta.') }}
IS_SANDBOX: ${{ contains(github.ref, '-sandbox.') }}
IS_PRODUCTION: ${{ !contains(github.ref, '-rc.') && !contains(github.ref, '-beta.') && !contains(github.ref, '-sandbox.') && startsWith(github.ref, 'refs/tags/') }}
steps:
- name: Log in to Docker Hub
if: ${{ inputs.enable_docker_login }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_IMAGE_PULL_TOKEN }}
- name: Checkout GitOps Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ inputs.gitops_repository }}
token: ${{ secrets.MANAGE_TOKEN }}
path: gitops
fetch-depth: 0
- name: Setup application name and paths
id: setup
shell: bash
run: |
# Extract app name from repository if not provided
if [[ -z "${{ inputs.app_name }}" ]]; then
APP_NAME="${GITHUB_REPOSITORY##*/}"
else
APP_NAME="${{ inputs.app_name }}"
fi
echo "app_name=$APP_NAME" >> "$GITHUB_OUTPUT"
echo "Application name: $APP_NAME"
# Determine which servers to deploy to
DEPLOY_FIRMINO="${{ inputs.deploy_in_firmino }}"
DEPLOY_CLOTILDE="${{ inputs.deploy_in_clotilde }}"
echo "deploy_firmino=$DEPLOY_FIRMINO" >> "$GITHUB_OUTPUT"
echo "deploy_clotilde=$DEPLOY_CLOTILDE" >> "$GITHUB_OUTPUT"
echo "Deploy to Firmino: $DEPLOY_FIRMINO"
echo "Deploy to Clotilde: $DEPLOY_CLOTILDE"
# Generate commit message prefix if not provided
if [[ -z "${{ inputs.commit_message_prefix }}" ]]; then
COMMIT_PREFIX="$APP_NAME"
else
COMMIT_PREFIX="${{ inputs.commit_message_prefix }}"
fi
echo "commit_prefix=$COMMIT_PREFIX" >> "$GITHUB_OUTPUT"
echo "Commit message prefix: $COMMIT_PREFIX"
# Generate artifact pattern if not provided
if [[ -z "${{ inputs.artifact_pattern }}" ]]; then
ARTIFACT_PATTERN="gitops-tags-${APP_NAME}-*"
else
ARTIFACT_PATTERN="${{ inputs.artifact_pattern }}"
fi
echo "artifact_pattern=$ARTIFACT_PATTERN" >> "$GITHUB_OUTPUT"
echo "Artifact pattern: $ARTIFACT_PATTERN"
- name: Install yq
shell: bash
run: |
set -euo pipefail
YQ_VERSION="${{ inputs.yq_version }}"
YQ_BIN="yq_linux_amd64"
YQ_URL="https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BIN}"
sudo curl -L "$YQ_URL" -o /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
yq --version
- name: Git pull before update
shell: bash
run: |
set -e
cd gitops || exit 1
git pull origin main
- name: Cleanup old artifacts (self-hosted runner safety)
shell: bash
run: |
# Remove any existing artifacts from previous runs (important for self-hosted runners)
echo "Cleaning up old artifacts directory..."
rm -rf .gitops-tags
echo "Cleanup complete"
- name: Download GitOps tag artifacts (pattern-based)
id: download-pattern
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: ${{ steps.setup.outputs.artifact_pattern }}
path: .gitops-tags
merge-multiple: true
# Only download from this workflow run (default behavior, but explicit for safety)
run-id: ${{ github.run_id }}
continue-on-error: true
- name: Fallback to legacy artifact name
if: steps.download-pattern.outcome == 'failure'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: gitops-tags
path: .gitops-tags
run-id: ${{ github.run_id }}
- name: Validate artifacts exist
id: validate_artifacts
shell: bash
run: |
echo "Checking for downloaded artifacts..."
# Check if directory exists and has files
if [[ ! -d ".gitops-tags" ]]; then
echo "::error::No artifacts directory found. Build job may have been skipped or failed to produce artifacts."
echo "artifacts_valid=false" >> "$GITHUB_OUTPUT"
exit 1
fi
# Count actual artifact files (not directories)
ARTIFACT_COUNT=$(find .gitops-tags -type f -name "*.tag" 2>/dev/null | wc -l || echo "0")
if [[ "$ARTIFACT_COUNT" -eq 0 ]]; then
echo "::error::No artifact files found in .gitops-tags/. Build job may have been skipped or failed."
echo "Directory contents:"
ls -laR .gitops-tags/ 2>/dev/null || echo "Directory is empty or doesn't exist"
echo "artifacts_valid=false" >> "$GITHUB_OUTPUT"
exit 1
fi
echo "Found $ARTIFACT_COUNT artifact file(s):"
ls -la .gitops-tags/
echo "artifacts_valid=true" >> "$GITHUB_OUTPUT"
- name: Apply tags to values.yaml (multi-server)
id: apply_tags
shell: bash
run: |
set -euo pipefail
# Get app name
APP_NAME="${{ steps.setup.outputs.app_name }}"
# Determine environments to update based on tag type
if [[ "${{ env.IS_BETA }}" == "true" ]]; then
ENVIRONMENTS="dev"
ENV_LABEL="beta/dev"
elif [[ "${{ env.IS_RC }}" == "true" ]]; then
ENVIRONMENTS="stg"
ENV_LABEL="rc/stg"
elif [[ "${{ env.IS_PRODUCTION }}" == "true" ]]; then
ENVIRONMENTS="dev stg prd sandbox"
ENV_LABEL="production"
elif [[ "${{ env.IS_SANDBOX }}" == "true" ]]; then
ENVIRONMENTS="sandbox"
ENV_LABEL="sandbox"
else
echo "Unable to detect environment from tag: ${{ github.ref }}"
exit 1
fi
echo "env_label=$ENV_LABEL" >> "$GITHUB_OUTPUT"
echo "Detected tag type: $ENV_LABEL"
echo "Environments to update: $ENVIRONMENTS"
# Determine servers to deploy to
SERVERS=""
if [[ "${{ steps.setup.outputs.deploy_firmino }}" == "true" ]]; then
SERVERS="firmino"
fi
if [[ "${{ steps.setup.outputs.deploy_clotilde }}" == "true" ]]; then
if [[ -n "$SERVERS" ]]; then
SERVERS="$SERVERS clotilde"
else
SERVERS="clotilde"
fi
fi
if [[ -z "$SERVERS" ]]; then
echo "No servers selected for deployment. Enable deploy_in_firmino or deploy_in_clotilde."
exit 1
fi
echo "Servers to deploy to: $SERVERS"
# First, check which artifacts actually exist
echo ""
echo "Checking available artifacts..."
AVAILABLE_ARTIFACTS=""
for artifact_file in .gitops-tags/*; do
if [[ -f "$artifact_file" ]]; then
artifact_name=$(basename "$artifact_file")
echo " Found artifact: $artifact_name"
AVAILABLE_ARTIFACTS="${AVAILABLE_ARTIFACTS}${artifact_name} "
fi
done
if [[ -z "$AVAILABLE_ARTIFACTS" ]]; then
echo "No artifacts found in .gitops-tags/"
echo "sync_matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_sync_targets=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Track results
UPDATED_FILES=""
MISSING_FILES=""
UPDATED_SERVERS_ENVS=""
# Function to update a single YAML key using sed (preserves formatting)
update_yaml_tag() {
local file="$1"
local yaml_path="$2"
local new_tag="$3"
# Extract the key name from yaml path (e.g., ".image.tag" -> "tag")
local key_name="${yaml_path##*.}"
# Use sed to replace only the tag value, preserving all formatting
# This handles patterns like "tag: v1.0.0" or "tag: 'v1.0.0'" or 'tag: "v1.0.0"'
if grep -q "^[[:space:]]*${key_name}:" "$file"; then
# Replace the value after the key, preserving indentation and quotes
sed -i -E "s/^([[:space:]]*${key_name}:[[:space:]]*)(['\"]?)([^'\"[:space:]]+)(['\"]?)$/\1\2${new_tag}\4/" "$file"
return 0
fi
return 1
}
# Process each server and environment
for SERVER in $SERVERS; do
for ENV in $ENVIRONMENTS; do
VALUES_FILE="gitops/environments/${SERVER}/helmfile/applications/${ENV}/${APP_NAME}/values.yaml"
echo ""
echo "Processing: $SERVER/$ENV"
echo " Path: $VALUES_FILE"
# Check if file exists
if [[ ! -f "$VALUES_FILE" ]]; then
echo " WARNING: Values file not found for ${SERVER}/${ENV}: ${VALUES_FILE}"
MISSING_FILES="${MISSING_FILES}${SERVER}/${ENV}:${VALUES_FILE}\n"
continue
fi
# Track if any changes were made to this file
FILE_CHANGED=false
# Apply mappings from inputs - only if artifact exists
MAPPINGS='${{ inputs.yaml_key_mappings }}'
while IFS='|' read -r artifact_key yaml_key; do
ARTIFACT_FILE=".gitops-tags/${artifact_key}"
if [[ -f "$ARTIFACT_FILE" ]]; then
TAG="$(cut -d= -f2 < "$ARTIFACT_FILE" | tr -d '[:space:]')"
if [[ -n "$TAG" ]]; then
# Get current value to check if update is needed
CURRENT_TAG=$(yq e "$yaml_key" "$VALUES_FILE" 2>/dev/null || echo "")
if [[ "$CURRENT_TAG" != "$TAG" ]]; then
# Use yq with explicit output to preserve formatting
TAG="$TAG" yq e "$yaml_key = strenv(TAG)" "$VALUES_FILE" > "${VALUES_FILE}.tmp"
mv "${VALUES_FILE}.tmp" "$VALUES_FILE"
echo " Updated $yaml_key: $CURRENT_TAG -> $TAG"
FILE_CHANGED=true
else
echo " Skipped $yaml_key: already set to $TAG"
fi
fi
fi
done < <(echo "$MAPPINGS" | jq -r 'to_entries[] | "\(.key)|\(.value)"')
# Apply configmap updates if configured - only if artifact exists
if [[ -n "${{ inputs.configmap_updates }}" ]]; then
CONFIGMAP_MAPPINGS='${{ inputs.configmap_updates }}'
while IFS='|' read -r artifact_key configmap_key; do
ARTIFACT_FILE=".gitops-tags/${artifact_key}"
if [[ -f "$ARTIFACT_FILE" ]]; then
TAG="$(cut -d= -f2 < "$ARTIFACT_FILE" | tr -d '[:space:]')"
if [[ -n "$TAG" ]]; then
CURRENT_TAG=$(yq e "$configmap_key" "$VALUES_FILE" 2>/dev/null || echo "")
if [[ "$CURRENT_TAG" != "$TAG" ]]; then
TAG="$TAG" yq e "$configmap_key = strenv(TAG)" "$VALUES_FILE" > "${VALUES_FILE}.tmp"
mv "${VALUES_FILE}.tmp" "$VALUES_FILE"
echo " Updated $configmap_key: $CURRENT_TAG -> $TAG"
FILE_CHANGED=true
else
echo " Skipped $configmap_key: already set to $TAG"
fi
fi
fi
done < <(echo "$CONFIGMAP_MAPPINGS" | jq -r 'to_entries[] | "\(.key)|\(.value)"')
fi
# Only track as updated if changes were actually made
if [[ "$FILE_CHANGED" == "true" ]]; then
UPDATED_FILES="${UPDATED_FILES}${VALUES_FILE}\n"
UPDATED_SERVERS_ENVS="${UPDATED_SERVERS_ENVS}${SERVER}:${ENV}\n"
else
echo " No changes needed for this file"
fi
done
done
# Report results
echo ""
echo "========================================="
echo "Summary:"
echo "========================================="
if [[ -n "$UPDATED_FILES" ]]; then
echo "Updated files:"
echo -e "$UPDATED_FILES"
else
echo "No files were updated"
fi
if [[ -n "$MISSING_FILES" ]]; then
echo ""
echo "Missing files (skipped):"
echo -e "$MISSING_FILES"
fi
# Save updated servers/envs for ArgoCD sync - only include files that actually changed
if [[ -n "$UPDATED_SERVERS_ENVS" ]]; then
echo -e "$UPDATED_SERVERS_ENVS" | grep -v '^$' > /tmp/updated_servers_envs.txt
# Output sync targets as JSON array for matrix job
SYNC_MATRIX=$(echo -e "$UPDATED_SERVERS_ENVS" | grep -v '^$' | while IFS=: read -r s e; do
[[ -n "$s" && -n "$e" ]] && echo "{\"server\":\"$s\",\"env\":\"$e\"}"
done | jq -s -c '.')
echo "sync_matrix=$SYNC_MATRIX" >> "$GITHUB_OUTPUT"
echo "has_sync_targets=true" >> "$GITHUB_OUTPUT"
echo "Sync matrix: $SYNC_MATRIX"
else
echo "sync_matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_sync_targets=false" >> "$GITHUB_OUTPUT"
fi
COUNT=0
if [[ -n "$UPDATED_FILES" ]]; then
COUNT=$(echo -e "$UPDATED_FILES" | grep -c -v '^$' || true)
fi
echo "updated_count=$COUNT" >> "$GITHUB_OUTPUT"
- name: Show git diff
shell: bash
run: |
cd gitops || exit 1
echo "Changes to be committed:"
git diff --stat
echo ""
echo "Detailed changes:"
git diff
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with:
gpg_private_key: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY }}
passphrase: ${{ secrets.LERIAN_CI_CD_USER_GPG_KEY_PASSWORD }}
git_committer_name: ${{ secrets.LERIAN_CI_CD_USER_NAME }}
git_committer_email: ${{ secrets.LERIAN_CI_CD_USER_EMAIL }}
git_config_global: true
git_user_signingkey: true
git_commit_gpgsign: true
workdir: gitops
- name: Commit & push (GitOps)
run: |
set -e
cd gitops || exit 1
# Get env label from apply_tags step
ENV_LABEL="${{ steps.apply_tags.outputs.env_label }}"
# Check if there are changes to commit
if git diff --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -am "ci(${{ steps.setup.outputs.commit_prefix }}): update image tags ($ENV_LABEL)" || echo "No changes to commit"
# Retry push with rebase and exponential backoff to handle concurrent updates
MAX_RETRIES=5
for i in $(seq 1 $MAX_RETRIES); do
if git push origin main; then
echo "Push succeeded on attempt $i"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "ERROR: Failed to push after $MAX_RETRIES attempts"
exit 1
fi
BACKOFF=$((2 ** i))
echo "Push failed (attempt $i/$MAX_RETRIES), rebasing and retrying in ${BACKOFF}s..."
sleep "$BACKOFF"
git pull --rebase origin main
done
# ArgoCD Sync Job - runs in parallel for each server/env combination
argocd_sync:
name: ArgoCD Sync (${{ matrix.target.server }}/${{ matrix.target.env }})
needs: [update_gitops]
if: needs.update_gitops.outputs.has_sync_targets == 'true' && inputs.enable_argocd_sync
runs-on: ${{ inputs.runner_type }}
strategy:
fail-fast: false
matrix:
target: ${{ fromJson(needs.update_gitops.outputs.sync_matrix) }}
steps:
- name: Execute ArgoCD Sync
uses: LerianStudio/github-actions-argocd-sync@main
with:
app-name: ${{ matrix.target.server }}-${{ needs.update_gitops.outputs.app_name }}
argo-cd-token: ${{ secrets.ARGOCD_GHUSER_TOKEN }}
argo-cd-url: ${{ secrets.ARGOCD_URL }}
env-prefix: ${{ matrix.target.env }}
skip-if-not-exists: 'true'
# Slack notification
notify:
name: Notify
needs: [update_gitops, argocd_sync]
if: always()
uses: ./.github/workflows/slack-notify.yml
with:
status: ${{ needs.update_gitops.result == 'failure' && 'failure' || needs.argocd_sync.result == 'failure' && 'failure' || 'success' }}
workflow_name: "GitOps Update"
failed_jobs: ${{ needs.update_gitops.result == 'failure' && 'Update GitOps' || '' }}${{ needs.argocd_sync.result == 'failure' && 'ArgoCD Sync' || '' }}
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}