Deployment Pipeline
The deployment pipeline uses GitHub Actions to build your container image, and ArgoCD (via the gap-registry) to deploy it to the clusters.
The flow:
- Your GitHub Actions workflow builds and pushes the container image to Artifact Registry
- The
update-image-tagjob commits the new image tag togap.yaml - ArgoCD detects the change and syncs your application
Add a GitHub Actions workflow that builds and pushes your image to Artifact Registry.
env:
IMAGE_NAME: europe-west3-docker.pkg.dev/sap-ems-base-infra-package-p/gap-images/<your-application-name>
IMAGE_TAG: "${{ github.sha }}"
jobs:
build-container:
timeout-minutes: 10
runs-on: ubuntu-latest
# enable permissions for workload identity federation
# you may need other permissions, check here: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: actions/checkout@v3
- name: Build container image
# optionally add build-args like: --build-arg NPM_TOKEN=${{ secrets.NPM_DEPLOYER_TOKEN }}
run: docker build . --file Dockerfile --tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
# authenticate to google cloud via workload identity federation
- uses: 'google-github-actions/auth@v3'
with:
workload_identity_provider: "${{ secrets.GAP_DEPLOY_WI_PROVIDER }}"
service_account: "${{ secrets.GAP_DEPLOY_SERVICE_ACCOUNT }}"
if: ${{github.ref_name == 'main' }}
# authenticate docker to the registry and push image
- run: |
gcloud auth configure-docker europe-west3-docker.pkg.dev
docker push --all-tags ${{ env.IMAGE_NAME }}
if: ${{github.ref_name == 'main' }}
Key points:
- Set
IMAGE_NAMEtoeurope-west3-docker.pkg.dev/sap-ems-base-infra-package-p/gap-images/<your-application-name> - Set
IMAGE_TAGto${{ github.sha }}(or another unique identifier) - Authenticate to the registry with
gcloud auth configure-docker europe-west3-docker.pkg.dev - Do not use a
latesttag — the image repository has tag immutability enabled - Authentication uses Workload Identity Federation (keyless) via organizational secrets (
GAP_DEPLOY_WI_PROVIDER,GAP_DEPLOY_SERVICE_ACCOUNT)
TheGAP_DEPLOY_*variables are GitHub organizational secrets — you do not need to add them manually.
If your image is already built during CI steps, you can skip this step and use the prebuilt image feature instead.
Add the GAP-Workflow application to your repository.
If you have branch protection policies that require PRs or block pushes to the main branch, add the GAP-Workflow application to the Bypass list on GitHub.
Add a job that updates the image tag in gap.yaml after a successful build. This is what triggers ArgoCD to deploy your application.
Make sure you update <name-of-container-building-step> and <name-of-the-main-branch> in the example below:
on:
push:
paths-ignore:
- 'gap/**'
env:
IMAGE_TAG: "${{ github.sha }}"
jobs:
update-image-tag:
timeout-minutes: 10
permissions:
contents: "write"
needs:
- <name-of-container-building-step>
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/<name-of-the-main-branch>' && github.actor != 'gap-workflows[bot]' }}
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ secrets.GAP_WORKFLOW_APP_ID }}
private-key: ${{ secrets.GAP_WORKFLOW_APP_PEM }}
- uses: "actions/checkout@v6"
with:
token: ${{ steps.app-token.outputs.token }}
- name: Get GitHub App User ID
id: get-user-id
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Update image tag
run: |
yq -i '.image.tag = env(IMAGE_TAG)' gap/gap.yaml
- name: Commit and push changes
id: commit_gap_yaml
run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
git add gap/gap.yaml
git commit -m "Update image tag to ${{ env.IMAGE_TAG }}"
git push origin HEAD:${{ github.ref_name }}
The paths-ignore on gap/** prevents the update-image-tag commit from re-triggering the build workflow.
If you have a monorepo with multiplegapfolders, see the monorepo section below for a retry mechanism to handle race conditions.
Add an entry to the gap-registry. The GAP team will approve and merge your PR. If you cannot create a PR, reach out on the #infra-support Slack channel or open a ticket in the team’s Jira project.
In case of a monorepo structure, register each of the gap folders as a separate app in gap-registry.
Create the file <your-namespace>/<your-app-name>/app.yaml with the following content:
app:
name: <my-application>
repo: https://github.com/emartech/<my-app-repo-name>.git
Available fields in app.yaml:
app.name— (required) name of your app, same as ingap.yamlapp.repo— (required) URL to your app repositoryapp.labels.group— (optional) used to group applications in the ArgoCD UIapp.path— (optional, defaults to"gap") path to the gap folder if not at the default locationapp.autoSyncToStaging— (optional, defaults totrue) set tofalseif usingpreDeploy(see note below). Only applies to staging environments.app.targetRepoRevision— (optional, defaults to"HEAD") can be set to a tag/branch/commit
Make sure your per-environment override directories and secrets are committed/created before adding your app to gap-registry, otherwise the first manifest generation could lead to dangling resources.
If your application usespreDeploy, setautoSyncToStaging: falseinapp.yamlfor the first sync. After the app appears in ArgoCD, manually sync only theServiceAccountfirst, then sync the remaining resources. After the first successful sync, setautoSyncToStagingback totrue. See the preDeploy section for full details.
After your PR to gap-registry is merged, verify that the application appears in ArgoCD (it can take a few minutes). Staging environments will be auto-synced unless autoSyncToStaging was set to false.
For production, check the diff in ArgoCD and sync the application manually.
Promoting your application to production can be done with two clicks via ArgoCD.
To automatically sync to production from your CI workflow, set the GAP_PROD_DEPLOY environment variable to true in your workflow’s deployment step (it is false by default).
You may also set theGAP_NO_PRUNEenvironment variable totrueto disable pruning during the sync.
If you have a monorepo with multiple gap.yaml files and multiple workflows building each application, you can end up with a race condition in the update-image-tag step.
A typical monorepo folder structure looks like this:
your-monorepo/
├── gap/ # first application's gap folder (default path)
│ ├── gap.yaml
│ ├── staging-defaults/
│ │ └── gap.yaml
│ ├── (...)
│ ├── p-eu1-01/
│ │ └── gap.yaml
└── gap-workers/ # second application's gap folder
├── gap.yaml
├── staging-defaults/
│ └── gap.yaml
├── (...)
└── p-eu1-01/
└── gap.yaml
Each gap folder has its own gap.yaml and environment override directories. The folder name (e.g. gap-workers) is what you set as app.path in the gap-registry entry.
To rectify this you can use a retry mechanism:
- uses: nick-fields/retry@v3
name: Commit and push changes
id: commit_gap_yaml
with:
timeout_minutes: 10
max_attempts: 10
retry_on_exit_code: 1
on_retry_command: echo "Retrying commit and push due to parallel GAP deployment changes..."
command: |
git pull --rebase origin ${{ github.ref_name }}
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
git add gap-workers/gap.yaml
git commit -m "Update image tag to ${{ env.IMAGE_TAG }}"
git push origin HEAD:${{ github.ref_name }}
Each application in a monorepo needs its own entry in the [gap-registry][gap-registry]. Create a separate<your-namespace>/<your-app-name>/app.yamlfile for each application, and make sure each entry uses the correctapp.pathto point to its respective gap folder (e.g.app.path: gap-workers).