GAP Documentation
GitHub Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

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:

  1. Your GitHub Actions workflow builds and pushes the container image to Artifact Registry
  2. The update-image-tag job commits the new image tag to gap.yaml
  3. ArgoCD detects the change and syncs your application

1. Build and push your container image

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_NAME to europe-west3-docker.pkg.dev/sap-ems-base-infra-package-p/gap-images/<your-application-name>
  • Set IMAGE_TAG to ${{ github.sha }} (or another unique identifier)
  • Authenticate to the registry with gcloud auth configure-docker europe-west3-docker.pkg.dev
  • Do not use a latest tag — the image repository has tag immutability enabled
  • Authentication uses Workload Identity Federation (keyless) via organizational secrets (GAP_DEPLOY_WI_PROVIDER, GAP_DEPLOY_SERVICE_ACCOUNT)
The GAP_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.

2. Install the GAP-Workflow GitHub App

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.

3. Add the update-image-tag workflow step

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 multiple gap folders, see the monorepo section below for a retry mechanism to handle race conditions.

4. Register your application in gap-registry

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 in gap.yaml
  • app.repo — (required) URL to your app repository
  • app.labels.group — (optional) used to group applications in the ArgoCD UI
  • app.path — (optional, defaults to "gap") path to the gap folder if not at the default location
  • app.autoSyncToStaging — (optional, defaults to true) set to false if using preDeploy (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 uses preDeploy, set autoSyncToStaging: false in app.yaml for the first sync. After the app appears in ArgoCD, manually sync only the ServiceAccount first, then sync the remaining resources. After the first successful sync, set autoSyncToStaging back to true. See the preDeploy section for full details.

5. Verify deployment

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 to production

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 the GAP_NO_PRUNE environment variable to true to disable pruning during the sync.

Monorepo setup

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.yaml file for each application, and make sure each entry uses the correct app.path to point to its respective gap folder (e.g. app.path: gap-workers).