Github Action file .yml for multi Tenant with file Target.txt
Github Action file .yml for multi Tenant with file Target.txt
note: this article is a continuation of Infrastructure {Server, CI/ CD, multi Tenant}}
Situation
I have a system {Product} with several Tenants:
- Tenant 1
- Tenant A
- Tenant X
- …
- Tenant n
each Tenant:
- has a different
DEPLOY_PATHon the server - has a different
SERVICE_NAME - has a different configuration {
app-settings}
Problem:
I want to perform Deployment in a way that is:
consistent— for all Tenantsflexible— able to choose the target Tenantsimple— no need for many different workflow files
What-if: no mechanism like this exists?
- create 1 workflow file per Tenant → many files, hard to maintain
- hardcode target in workflow → need to edit
.ymlfile every time you deploy - human error: accidentally deploying to the wrong Tenant
Solution: Target.txt file
Main idea:
- 1
.ymlfile for all Tenants - deployment target is determined by the
__Deploy/Target.txtfile - simply change the content of
Target.txt, thengit push - workflow reads
Target.txt→ deploys to the correct Tenant
app-settings.json file for UI {externalized configuration}
I want to have a dynamic UI that can connect to a BackEnd, by means of:
app-settings.jsonfile at the root- this file must not be included in the build
- UI will fetch this file
- this file contains the URL end-point of the BackEnd
- UI will connect to that URL
__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
Target.txt format:
tenant1.SoftwareDevelopeRx.com
tenantA.SoftwareDevelopeRx.com
tenantX.SoftwareDevelopeRx.com
first line = deployment target
tenant1.SoftwareDevelopeRx.com
dotnet_restore
second line = optional flag {example: dotnet_restore}
Special value:
no_deployment
if the content of Target.txt is no_deployment:
- workflow still runs
- but does not perform deployment
- only
git push - use case: update documentation, update non-deployment configuration
Component: Github Action files .yml
I have 4 workflow files:
| File | Trigger Branch | Function |
|---|---|---|
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 uses branch
master, FrontEnd uses branchmain
Anatomy: workflow file
every workflow file has a consistent structure:
1. Read deployment target ← read 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:
Target.txtfile must exist- first line must not be empty
- trim whitespace — to avoid human error {hidden spaces}
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
this pattern:
casestatement → mapping from domain to variablesGITHUB_ENV→ variables available to all subsequent stepsno_deployment→ skip deployment, no error*→ catch invalid target → fail fast, not silent
Step 3: dotnet restore {optional}
specifically for 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
why optional?
dotnet restoretakes time- if packages have not changed → skip restore → faster deployment
- simply add the second line
dotnet_restoreinTarget.txtwhen needed
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 on publish → does not rebuild, since it was already built in the previous step
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!"
difference between BackEnd vs FrontEnd:
- BackEnd → stop service → deploy → start service {because there is a running
.dll} - FrontEnd → copy files directly {static files, no service restart needed}
Concurrency: one deployment at a time
concurrency:
group: deploy-app-engine
cancel-in-progress: false
cancel-in-progress: false → a deployment that is already running is not cancelled by the next push
- use case: push to
mastertwice in a row → first deployment finishes first, then the second one runs
Special case: App Engine with DLL copy
# 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
why is this step needed?
- App Engine {plugin} is loaded by Framework Engine
- after deploying App Engine → copy
.dllto the Framework Engine folder - Framework Engine will use the new
.dllon next restart
Workflow: how to deploy to a specific Tenant
1. edit file __Deploy/Target.txt
→ write the domain of the Tenant to deploy to
2. git add __Deploy/Target.txt
git commit -m "deploy: Tenant X"
git push
3. Github Action runs automatically
→ reads Target.txt
→ deploys to the correct Tenant
if you only want to git push without deployment:
1. edit __Deploy/Target.txt → fill in: no_deployment
2. git push
Notes
self-hostedrunner → workflow runs on your own server, not on Github’s serveractions/checkout@v4withclean: true→ workspace is always clean before deploy|| trueonsystemctl stop→ no error if service is not yet running {first time deploy}- permission fix on
github.workspace→ for cleanup purposes by the Github runner