Github Action file .yml for multi Tenant with file Target.txt
Github Action file .yml for multi Tenant with file Target.txt
catatan: tulisan ini adalah lanjutan dari artikel Infrastructure {Server, CI/ CD, multi Tenant}}
Situasi
saya mempunyai system {Product} dengan beberapa Tenant:
- Tenant 1
- Tenant A
- Tenant X
- …
- Tenant n
masing-masing Tenant:
- mempunyai
DEPLOY_PATHyang berbeda di server - mempunyai
SERVICE_NAMEyang berbeda - mempunyai konfigurasi {
app-settings} yang berbeda
Problem:
saya ingin melakukan Deployment dengan cara yang:
konsisten— untuk semua Tenantfleksibel— bisa memilih Tenant targetsimple— tidak perlu banyak workflow file yang berbeda
What-if: tidak ada mekanisme seperti ini?
- buat 1 workflow file per Tenant → banyak file, susah di-maintain
- hardcode target di workflow → perlu edit file
.ymlsetiap kali deploy - human error: salah deploy ke Tenant yang salah
Solusi: file Target.txt
Ide utama:
- 1 file
.ymluntuk semua Tenant - target deployment ditentukan oleh file
__Deploy/Target.txt - cukup ubah isi
Target.txt, lalugit push - workflow membaca
Target.txt→ deploy ke Tenant yang tepat
File app-settings.json untuk UI {externalized configuration}
Saya ingin mempunyai UI yang dynamic yang bisa melakukan koneksi ke suatu BackEnd, dengan cara:
- file
app-settings.jsonpada root - file ini tidak boleh included ke dalam build
- UI akan fetch file ini
- file ini berisi informasi URL end-point of BackEnd
- UI akan koneksi ke URL tersebut
__Deploy/
Target.txt
app-settings.json
app-settings--env_dev.json
app-settings--env_test.json
app-settings--Tenant_1.json
app-settings--Tenant_A.json
app-settings--Tenant_X.json
Format Target.txt:
tenant1.SoftwareDevelopeRx.com
tenantA.SoftwareDevelopeRx.com
tenantX.SoftwareDevelopeRx.com
baris pertama = target deployment
tenant1.SoftwareDevelopeRx.com
dotnet_restore
baris kedua = optional flag {contoh: dotnet_restore}
Special value:
no_deployment
jika isi Target.txt adalah no_deployment:
- workflow tetap berjalan
- tetapi tidak melakukan deployment
- hanya
git pushsaja - use case: update dokumentasi, update konfigurasi non-deployment
Component: Github Action files .yml
saya mempunyai 4 workflow files:
| File | Trigger Branch | Fungsi |
|---|---|---|
deploy_framework_engine.yml |
master | Deploy BackEnd → Framework Engine |
deploy_app_engine.yml |
master | Deploy BackEnd → App Engine {plugin} |
deploy_framework_UI.yml |
main | Deploy FrontEnd → Framework UI |
deploy_app_UI.yml |
main | Deploy FrontEnd → App UI |
Note: BackEnd menggunakan branch
master, FrontEnd menggunakan branchmain
Anatomy: workflow file
setiap workflow file mempunyai struktur yang konsisten:
1. Read deployment target ← baca Target.txt
2. Set deployment variables ← mapping target → variables
3. [optional] dotnet restore
4. Build / Publish
5. Deploy application ← stop service → copy files → start service
Step 1: Read deployment target
- name: Read deployment target
id: read-target
run: |
if [ ! -f "__Deploy/Target.txt" ]; then
echo "Error: __Deploy/Target.txt not found!"
exit 1
fi
# Read first line only
TARGET=$(head -n 1 __Deploy/Target.txt | tr -d '[:space:]')
if [ -z "$TARGET" ]; then
echo "Error: Target is empty!"
exit 1
fi
echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
echo "📍 Deployment target: $TARGET"
validation:
- file
Target.txtharus ada - baris pertama tidak boleh kosong
- trim whitespace — untuk menghindari human error {spasi tersembunyi}
Step 2: Set deployment variables
- name: Set deployment variables
run: |
TARGET="$"
case "$TARGET" in
tenantX.SoftwareDevelopeRx.com)
echo "SERVICE_NAME=Product_Tenant_X" >> $GITHUB_ENV
echo "DEPLOY_PATH=/var/www-custom/..." >> $GITHUB_ENV
;;
no_deployment)
echo "SHOULD_DEPLOY=false" >> $GITHUB_ENV
;;
*)
echo "Error: Invalid target '$TARGET'"
exit 1
;;
esac
pattern ini:
casestatement → mapping dari domain ke variablesGITHUB_ENV→ variables tersedia untuk semua step berikutnyano_deployment→ skip deployment, tidak error*→ catch invalid target → fail fast, tidak silent
Step 3: dotnet restore {optional}
khusus untuk BackEnd Engine:
# Read second line for dotnet_restore flag
RESTORE=$(sed -n '2p' __Deploy/Target.txt | tr -d '[:space:]')
if [ "$RESTORE" == "dotnet_restore" ]; then
echo "SHOULD_RESTORE=true" >> $GITHUB_OUTPUT
fi
- name: Restore packages
if: env.SHOULD_DEPLOY == 'true' && steps.read-target.outputs.SHOULD_RESTORE == 'true'
run: dotnet restore --verbosity minimal
mengapa optional?
dotnet restoremembutuhkan waktu- jika packages tidak berubah → skip restore → deployment lebih cepat
- cukup tambahkan baris kedua
dotnet_restorediTarget.txtjika diperlukan
Step 4: Build & Publish {BackEnd}
- name: Build application
if: env.SHOULD_DEPLOY == 'true'
run: |
dotnet build \
--configuration Release \
--verbosity minimal \
-p:UseSharedCompilation=true \
-p:BuildInParallel=true
- name: Publish application
if: env.SHOULD_DEPLOY == 'true'
run: |
dotnet publish \
--configuration Release \
--no-build \
--output ./publish-cache \
--verbosity minimal
--no-build pada publish → tidak build ulang, karena sudah di-build pada step sebelumnya
Step 5: Deploy application
BackEnd {Engine}:
- name: Deploy application
if: env.SHOULD_DEPLOY == 'true'
run: |
# Stop service gracefully
sudo systemctl stop $ || true
sleep 2
# Copy published files
sudo cp -rf ./publish-cache/* "$/"
# Fix ownership
sudo chown -R nginx:nginx $
sudo chmod -R 755 $
# Start service
sudo systemctl daemon-reload
sudo systemctl enable $
sudo systemctl start $
# Verify service started
sleep 3
if ! sudo systemctl is-active --quiet $; then
echo "❌ Service failed to start"
sudo journalctl -u $ --no-pager -n 10
exit 1
fi
echo "✅ Deployment completed successfully!"
FrontEnd {UI}:
- name: Deploy application
if: env.SHOULD_DEPLOY == 'true'
run: |
# Copy all files (excluding .git and __Deploy folder)
sudo rsync -av --exclude='.git' --exclude='__Deploy' ./ $/
# Fix ownership
sudo chown -R nginx:nginx $
sudo chmod -R 755 $
echo "✅ Deployment completed successfully!"
perbedaan BackEnd vs FrontEnd:
- BackEnd → stop service → deploy → start service {karena ada
.dllyang berjalan} - FrontEnd → langsung copy files {static files, tidak perlu restart service}
Concurrency: satu deployment dalam satu waktu
concurrency:
group: deploy-app-engine
cancel-in-progress: false
cancel-in-progress: false → deployment yang sedang berjalan tidak dibatalkan oleh push berikutnya
- use case: push ke
masterdua kali berturut-turut → deployment pertama selesai dulu, baru deployment kedua
Special case: App Engine dengan copy DLL
# Copy DLL to Framework Engine folder
sudo cp -f $/APP-Engine.dll $/
sudo chown nginx:nginx $/APP-Engine.dll
sudo chmod 755 $/APP-Engine.dll
mengapa ada step ini?
- App Engine {plugin} di-load oleh Framework Engine
- setelah deploy App Engine → copy
.dllke folder Framework Engine - Framework Engine akan menggunakan
.dllyang baru pada restart berikutnya
Workflow: cara deploy ke Tenant tertentu
1. edit file __Deploy/Target.txt
→ tulis domain Tenant yang ingin di-deploy
2. git add __Deploy/Target.txt
git commit -m "deploy: Tenant X"
git push
3. Github Action otomatis berjalan
→ baca Target.txt
→ deploy ke Tenant yang tepat
jika hanya ingin git push tanpa deployment:
1. edit __Deploy/Target.txt → isi: no_deployment
2. git push
Catatan
self-hostedrunner → workflow berjalan di server sendiri, bukan di Github serveractions/checkout@v4denganclean: true→ workspace selalu bersih sebelum deploy|| truepadasystemctl stop→ tidak error jika service belum berjalan {first time deploy}- permission fix pada
github.workspace→ untuk keperluan cleanup oleh Github runner