Temporary environments for Azure Functions and containers.
Or GitHub pipeline automation approach without branch hell
TL;DR; Environment specific branches is antipattern, but environment specific pipeline is not ;). Sometimes you need to troubleshoot production code, sometimes allow external vendor to run integration and load tests. Infrastructure as code(IaC) solves the problem of temporary environments, but what about deployment and merging? This article will explain how to build GitHub pipeline to deploy particular version of your Azure Functions anywhere you need. Without manual intervention and branch hell!
Code for the article https://github.com/staslebedenko/custom_promotion.
Plan
Solution is build around Git commands, and I would argue that you need to keep it simple and while you can include infra deployment step into this pipeline, I would avoid that at all costs :).
- The problem with branching hell and extra environments
- Initial setup 2 variants of branches for pipeline
- Pipeline structure
- Summary + sample repository
The problem
So you probably have dev, test and prod environments, or only two as modern recommendations suggest. But what about chaos testing, load testing, integrations with many external vendors? I got a life lesson recently that one setup doesn’t fill all cases.
As many solutions hosted via cloud services and Kubernetes, it is fairly easy to setup a new infrastructure on Azure, or add a new namespace, run it for a day and then delete.
But this is just a part of the problem, how would you deploy your code with a proper config and without manually touching any branches at all? Git commands and branch cleaning will help you :)
Initial setup
So there is a need for two protected branches for pipeline to use, and here is a two options, one that do not keep branch history and reset everything on the merge, and another that keeps history intact.
BT abbreaviation means “Beta test” — a special name of the environment for Azure Function application.
Version with history.
This is a more classic variant, but history of merges by pipeline will pollute you git history. This is why both this BT and BT_Config branches should be locked out from anyone besides specific pipeline.
Setup of the persistent deploy branch
git branch BT
git checkout BT
git rm -rf --ignore-unmatch .
git clean -fdx
git commit --allow-empty -m "Empty init commit"
git push origin BTSetup of configuration branch for persistent deploy branch
git branch BT_Config
git checkout BT_Config
git rm -r *
git checkout HEAD -- .gitignore .gitattributes "Promotion app/Function1.cs"
git add .gitignore .gitattributes "Promotion app/Function1.cs"
git commit -m "BT branch configuration init"
git push origin BT_ConfigVersion without keeping history(detached head)
This option is for maximum clean state and will result is separate git history for both branches, only the latest merge into BT branch will be displayed.
Setup of the persistent deploy branch.
git checkout --orphan ST
INITIAL_COMMIT=$(git rev-list --max-parents=0 HEAD)
git reset --hard $INITIAL_COMMIT
git clean -fdx
git push origin ST --forceSetup of configuration branch. Please take a note, that configuration branch uses unedited Azure function file and will keep only this file.
git checkout --orphan ST_Config
git rm -r *
git checkout HEAD -- .gitignore .gitattributes "Promotion app/Function1.cs"
git add .gitignore .gitattributes "Promotion app/Function1.cs"
git commit -m "BT branch configuration init"
git push origin ST_ConfigPipeline
While pipeline is the key part, you should also know that you can try out flow with command line and git commands.
Below are commands for solution version that keeps git history. As we are planning to deploy specific code version, we searching for the needed tag in the git work tree. Then overwriting changes needed from BT_Config branch so application will have config needed for specific environment.
# switching to branch and cleaning files locally => empty commit
git switch BT
git rm -rf --ignore-unmatch .
git clean -fdx
git push origin BT --force
# Step 2: Import code from master at specific tag
TAG="v1.2.3" # Replace with your actual tag name
git rev-parse "$TAG"
git restore --source=$TAG --worktree --staged -- .
git commit -m "Imported master at $TAG"
# Step 3: Overwrite Function1.cs with version from BT_config
rm -f "Promotion app/Function1.cs"
git restore --source=BT_config "Promotion app/Function1.cs"
git add "Promotion app/Function1.cs"
git commit -m "Updated Function1.cs from ST_config"
# Step 4: Force push to update the remote ST branch
git push origin BT --force And now is the time for pipeline that will auto merge changes to the particular branch. This pipeline reflects commands you can find above.
GitHub Action pipeline below will ask you to provide a git tag value to deploy specific version and also will validate if this tag exist.
name: Update BT Branch
on:
push:
branches:
- BT
workflow_dispatch:
inputs:
tag:
description: 'Tag to import from master'
required: true
default: 'v1.2.3'
jobs:
update-bt-branch:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
ref: BT # Checkout the ST branch
- name: Set up Git
run: |
git config user.name "Stas"
git config user.email "stas@stas.com"
- name: Validate the provided tag
run: |
TAG="${{ github.event.inputs.tag }}"
if git rev-parse "$TAG" >/dev/null 2>&1
then
echo "Tag '$TAG' exists."
else
echo "Error: Tag '$TAG' does not exist."
exit 1
fi
- name: Reset branch to empty state
run: |
git switch BT
git rm -rf --ignore-unmatch .
git clean -fdx
git add -A
git commit --allow-empty -m "Reset"
git push origin BT --force
- name: Import code from master at specific tag
run: |
TAG="${{ github.event.inputs.tag }}"
git restore --source=$TAG --worktree --staged -- .
git commit -m "Imported master at $TAG"
- name: Fetch BT_config branch
run: git fetch origin BT_config:BT_config
- name: Apply BT related config file Function1.cs from BT_config branch
run: |
FILE_PATHS=(
"Promotion app/Function1.cs"
"Jenkinsfile"
)
for FILE_PATH in "${FILE_PATHS[@]}"; do
rm -f "$FILE_PATH"
if git restore --source=BT_config "$FILE_PATH"; then
git add "$FILE_PATH"
else
echo "Warning: Could not restore $FILE_PATH from BT_config"
fi
done
git commit -m "Updated with files from BT_config"
- name: Force push to update the remote BT branch
run: git push origin BT --forceAnd now for the pipeline that will reset branch and history each time your code merged and keep history clean.
Code below is responsible for hard reset of the branch, ST branch should be created with -orphan command.
INITIAL_COMMIT=$(git rev-list --max-parents=0 HEAD)
git reset --hard $INITIAL_COMMITAnd pipeline itself, be aware that I named branches ST on purpose to differentiate version with history reset.
name: Update ST Branch
on:
push:
branches:
- ST
workflow_dispatch:
inputs:
tag:
description: 'Tag to import from master'
required: true
default: 'v1.2.3'
jobs:
update-st-branch:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
ref: ST # Checkout the ST branch
- name: Set up Git
run: |
git config user.name "Stas"
git config user.email "stas@stas.com"
- name: Validate the provided tag
run: |
TAG="${{ github.event.inputs.tag }}"
if git rev-parse "$TAG" >/dev/null 2>&1
then
echo "Tag '$TAG' exists."
else
echo "Error: Tag '$TAG' does not exist."
exit 1
fi
- name: Reset to initial commit
run: |
INITIAL_COMMIT=$(git rev-list --max-parents=0 HEAD)
git reset --hard $INITIAL_COMMIT
git clean -fdx
git push origin ST --force
- name: Import code from master at specific tag
run: |
TAG="${{ github.event.inputs.tag }}"
git rm -rf --ignore-unmatch .
git clean -fdx
git restore --source=$TAG --worktree --staged -- .
git commit -m "Imported master at $TAG"
- name: Fetch ST_config branch
run: git fetch origin ST_config:ST_config
- name: Apply ST related config file Function1.cs from ST_config branch
run: |
FILE_PATH_CONFIG="Promotion app/Function1.cs"
rm -f "$FILE_PATH_CONFIG"
git restore --source=ST_config "$FILE_PATH_CONFIG"
git add "$FILE_PATH_CONFIG"
git commit -m "Updated Function1.cs and Pipeline from ST_config 1"
- name: Force push to update the remote ST branch
run: git push origin ST --forceSummary
Life of software engineer is full of compromises, modern way of working with Platform engineering relies on Test and Prod, but life of a product app or integration with multiple versions of third-party apps can be a real challenge.
Hiding complexity behind automated pipeline is not a sin :), if you keep branches behind protected and inaccessible to anyone.
Cheers!
