GitHub Actions CI/CD Architecture

1. Overview

This guide documents the CI/CD pipeline architecture used across Event and Membership Management projects. The system uses a combination of reusable workflows (defined centrally) and project-specific workflows that consume them.

CI/CD is one element of the broader EMS service baseline — the parent context (Maven module shape, Helm chart, ArgoCD manifests, OTel, profiles) lives in Microservice Pattern. Read that page first when bootstrapping a new service; this page focuses on the GitHub Actions layer specifically.

1.1. Key Principles

  • DRY (Don’t Repeat Yourself) - Common build logic is centralized in reusable workflows

  • Consistency - All projects follow the same patterns and conventions

  • Conditional Execution - Only run tests for changed components (frontend/backend)

  • GitOps - Deployments are managed through ArgoCD and Git-based configuration

2. Workflow Architecture

The CI/CD system consists of two layers:

Reusable Workflows (in christhonie/event/.github/workflows/)

Centrally managed, versioned workflow definitions that encapsulate common operations like testing, building, and deploying.

Project Workflows (in each repository’s .github/workflows/)

Project-specific workflows that orchestrate the reusable workflows and define triggers.

Diagram

The five wrapper workflows above are the canonical inventory for a backend or MCP-adapter service. Portal services (those serving an Angular SPA) add a frontend filter to pr-non-main.yml and pass require-npm: true to maven-package.yml.

3. Path Filtering Strategy

3.1. Excluding Non-Build Directories

Use paths-ignore at the workflow trigger level to prevent unnecessary builds when only documentation or configuration files change:

on:
  push:
    branches:
      - develop
    # NOTE: Keep paths-ignore in sync with push-main.yml
    paths-ignore:
      - '*.md'
      - '*.adoc'
      - '.devcontainer/**'
      - '.husky/**'
      - '.jhipster/**'
      - 'docs/**'
When modifying the paths-ignore list, update it in all workflow files that share this pattern. The sync note comment serves as a reminder.

The two push workflows must not ignore src/main/helm/** — they own the ArgoCD bump for their environment and would otherwise leave the manifest stale. See Helm changes (no separate pipeline).

Do not use trigger-level paths-ignore in pr-non-main.yml. That workflow carries the PR checks required status check (see The pr-checks aggregator), and a required check that is never created leaves a pull request pending forever rather than mergeable. With paths-ignore present, a docs-only or Helm-only PR triggers nothing at all and so can never be merged.

Filtering still happens for PRs — one level down, in the paths-filter job, which decides whether the expensive test jobs run. They skip when irrelevant and pr-checks reports success. The cost is one short ubuntu-latest job on PRs that would previously have run nothing.

This applies only to workflows whose checks are required. push-dev.yml and push-main.yml keep their paths-ignore.

paths-ignore skips a run only when every changed file matches an entry. One unmatched file is enough to trigger the workflow, so it cannot be used to route a commit to a different pipeline — only to suppress a run entirely. Under GitFlow this makes it useless for isolating chart changes on main, because a release merge always carries pom.xml.

3.2. Conditional Job Execution

Use the dorny/paths-filter action to conditionally run frontend or backend tests based on what files changed:

jobs:
  paths-filter:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            frontend:
              - '.github/workflows/**'
              - 'package*.json'
              - 'angular.json'
              - 'tsconfig*.json'
              - 'webpack/**'
              - 'src/main/webapp/**'
            backend:
              - '.github/workflows/**'
              - 'pom.xml'
              - 'src/main/java/**'
              - 'src/main/resources/**'
              - 'src/test/java/**'
              - 'src/test/resources/**'

  test-fe:
    needs: paths-filter
    if: needs.paths-filter.outputs.frontend == 'true'
    uses: christhonie/event/.github/workflows/test-fe.yml@main
    # ...

  test-be:
    needs: paths-filter
    if: needs.paths-filter.outputs.backend == 'true'
    uses: christhonie/event/.github/workflows/test-be.yml@main
    # ...

3.3. The pr-checks aggregator

Conditional job execution creates a problem for branch protection: required status checks are matched by name, and Back-end / ✅ Test is legitimately skipped on a front-end-only PR. Requiring it directly would block those PRs.

The fix is one job that always runs, inspects its dependencies itself, and reports a single verdict. Require only that job.

  pr-checks:
    name: 'PR checks'
    needs: [paths-filter, test-fe, test-be]
    if: always()          (1)
    runs-on: ubuntu-latest
    steps:
      - name: Verify every gate passed or was legitimately skipped
        run: |
          set -euo pipefail
          failed=0
          for entry in "paths-filter=${{ needs.paths-filter.result }}" \
                       "test-fe=${{ needs.test-fe.result }}" \
                       "test-be=${{ needs.test-be.result }}"; do
            job="${entry%%=*}"; result="${entry#*=}"
            case "$result" in
              success|skipped) ;;   (2)
              *) echo "::error::$job reported '$result'"; failed=1 ;;
            esac
          done
          [ "$failed" -eq 0 ] || { echo "::error::This PR must not be merged."; exit 1; }
1 Load-bearing. Without always(), a failed dependency skips this job too, the required check reports skipped, and branch protection can treat that as satisfied — a gate that opens precisely when a job fails, which is worse than no gate because it looks like protection.
2 skipped is paths-filter working as designed. failure and cancelled are fatal.

In the Angular repositories needs also lists check-format, which has no if: and so always reports. See Formatting gate.

Behaviour:

Scenario test-fe test-be PR checks

Back-end PR, a gate fails

skipped

failure

failure

Back-end PR, all good

skipped

success

success

Front-end-only PR

success

skipped

success

Run cancelled

cancelled

failure

This job reports; it does not block. Only branch protection blocks — see Branch protection: require the PR checks context. A PR with a red pr-checks and no protection still shows mergeable=MERGEABLE.

The pattern exists because admin-portal PR #85 merged with a red Back-end / ✅ Test — a javadoc doclint error — which broke develop and stayed broken through the next merge. CI had caught it correctly; nothing stopped the merge.

3.4. Formatting gate

The Angular repositories' pre-commit hook formats staged files but deliberately skips wherever it cannot run — see Formatting and the Pre-commit Hook. check-format.yml is what enforces formatting:

  check-format:
    name: 'Format'
    uses: christhonie/event/.github/workflows/check-format.yml@main   (1)
    secrets:
      NPM_PASSWORD: ${{ secrets.EVENT_PACKAGE_REPO_TOKEN }}
1 No needs: paths-filter and no if:. The hook formats Java and Markdown too, so the check must run on every pull request.

It checks out the pull request’s merge commit, soft-resets it to the merge base so every change is staged as one commit, and runs npx lint-staged --no-stash. The job fails if that changes the index or working tree, or if lint-staged itself fails.

Running lint-staged, rather than Prettier directly, is the point: the check applies the repository’s own glob, .prettierignore and commands, whatever lint-staged and Prettier versions the repository pins. Only changed files are checked, so a repository with existing drift adopts it without a mass reformat — at the cost that touching a drifted file requires formatting all of it.

pull_request events only. event-admin-ui references it at @main while its other jobs are pinned to a tag that predates it.

4. Reusable Workflows Reference

The following reusable workflows are available in christhonie/event/.github/workflows/:

Workflow Purpose Inputs Secrets Outputs

test-be.yml

Run backend tests using Maven

maven-profiles, jdk-version

MAVEN_PASSWORD

-

test-fe.yml

Run frontend tests using npm

node-version, run-tests (default false — installs and builds but skips the tests, with a warning)

NPM_PASSWORD

-

check-format.yml

Fail a pull request whose changes the pre-commit formatting would rewrite — see Formatting gate

node-version

NPM_PASSWORD

-

maven-package.yml

Build and cache Maven artifacts

maven-profiles, jdk-version, require-npm, node-version

MAVEN_PASSWORD

cached target

maven-deploy.yml

Deploy JAR to GitHub Packages

maven-profiles, jdk-version

MAVEN_PASSWORD

-

docker-build.yml

Build and push Docker image via JIB

maven-profiles, jdk-version

DOCKER_PAT

image-tag

helm-build.yml

Package and push Helm charts

maven-profiles, jdk-version

DOCKER_PAT

-

argocd-update.yml

Update ArgoCD deployment manifests in christhonie/idl-xnl-jhb-rc01

environment (req), service-name (req), chart-version (bumps targetRevisionthis is the one to pass), build-sha (opt — sets podAnnotations.app.kubernetes.io/build-sha to force pod rollover on SNAPSHOT rebuilds), image-tag (opt — do not use, see warning below), argocd-repo (opt, default christhonie/idl-xnl-jhb-rc01), spa-url (opt — public base URL of a SPA-serving service; enables the post-deploy cache check, see Post-deploy SPA cache verification)

ARGOCD_REPO_TOKEN

-

dependencygraph.yml

Submit dependency graph to GitHub

maven-profiles, jdk-version

-

-

publish.yml

Upload build artifacts

jdk-version

-

-

gitflow-release-finish.yml

Complete GitFlow release process

jdk-version

-

-

copilot-review.yml

Request a Copilot code review on a new pull request

-

RELEASE_PAT

-

Pass chart-version to argocd-update.yml, never image-tag.

image-tag writes spec.source.helm.valuesObject.image.tag, which the chart treats as an override of its own appVersion rather than a companion to it. Nothing ever clears that key, so a single call freezes the deployed image while later releases keep bumping targetRevision.

All wrapper workflows across event-admin-service, membership-ui, ems-admin-portal and event-registration-ui pass only chart-version (plus build-sha on dev). See ArgoCD Deployment for the failure this caused in registration-portal-stage.yml.

4.1. Workflow Naming Convention

Workflow names use emoji prefixes for quick visual identification:

🚀

Deployment and push workflows

Testing workflows

📦

Packaging workflows

🐋

Docker workflows

Helm/Kubernetes workflows

GitFlow release workflows

📰

Publishing workflows

🔄

Synchronization workflows

🤖

Code review automation

4.2. Post-deploy SPA cache verification

argocd-update.yml takes an optional spa-url. When set, the deploy is followed by a verify-spa-cache job that waits for the new build.version to appear at that URL, then asserts index.html does not revalidate to a stale copy:

  update-argocd-stage:
    uses: christhonie/event/.github/workflows/argocd-update.yml@main
    with:
      environment: stage
      service-name: admin-portal
      chart-version: ${{ needs.helm-build.outputs.chart-version }}
      spa-url: https://admin-portal-stage.idealogic.co.za

The check is one conditional request:

curl -s -o /dev/null -w '%{http_code}' \
  -H 'If-Modified-Since: Thu, 01 Jan 1970 00:00:01 GMT' https://HOST/index.html
# 200 = correct
# 304 = the deployed image serves stale bundles to every returning browser

That timestamp is Jib’s fixed reproducible-build time, so it is exactly the validator a browser cached from a broken build holds. A 304 means returning users keep running the previous release until they hard-reload.

Why this is a deployment check and not a unit test. Bugs #975 and #976 were omissions, not regressions — two services never received the index.html validator fix that a third had, and one carried the defect for about five months. A repo-local test can only protect code that already exists in that repo, so no test anywhere would have flagged either. This tests the deployed artefact, so it also covers chart edits, ingress rewrites and a CDN appearing in front of a host.

Requires the service to expose build.version, i.e. the build-info goal on spring-boot-maven-plugin. The job says so when the wait times out.

Two limits worth knowing. On dev the version is a -SNAPSHOT that does not change between builds, so the readiness gate can be satisfied by the pod already running and the check may evaluate the previous image; it still catches a service that has never had the fix. And prod is a manual promotion that does not run this workflow, so it is unguarded.

4.3. Copilot code review requests

copilot-review.yml requests a Copilot code review on every new pull request. Each repository calls it from a thin wrapper at .github/workflows/copilot-review.yml:

on:
  pull_request:
    types:
      - opened
      - ready_for_review

jobs:
  copilot-review:
    name: Request Copilot Review
    uses: christhonie/event/.github/workflows/copilot-review.yml@main
    secrets:
      RELEASE_PAT: ${{ secrets.RELEASE_PAT }}

4.3.1. Why the request is made under a PAT

A Copilot review is billed to the entitlement of whoever requests it, never the author’s. A contributor who holds no Copilot subscription cannot obtain a review by asking for one, and no review appears on their pull requests at all.

Requesting under RELEASE_PAT — a token belonging to an account that does hold Copilot — is what closes that gap. The token owner’s entitlement is spent and the review lands on the contributor’s pull request as normal.

The GITHUB_TOKEN fallback exists only so that a repository without the secret still parses. It runs as github-actions[bot], which holds no Copilot entitlement: the request is accepted and no review is ever produced. It is a diagnostic path, not a working one.

Requesting the review needs Pull requests: write on the token, which is a different permission from the Contents: read+write the release back-merge needs. A classic PAT carrying the repo scope already covers both.

4.3.2. Why not the GitHub ruleset

GitHub offers a branch ruleset rule, Automatically request Copilot code review, which appears to do the same job in one setting with no YAML at all. It does not solve this problem, and it fails silently.

The rule requests the review on the author’s behalf. The GitHub UI states that it applies "if the author has access to Copilot code review and their premium requests quota has not reached the limit". Where the author holds no subscription that condition is never met, so nothing happens: no review, no error, no failed check, and no entry in the pull request timeline.

The ruleset is therefore useful only where every contributor already holds a Copilot seat — which is precisely the case in which the problem does not arise.

4.3.3. Pull requests that are skipped

The shared workflow declines to spend a premium request on three classes of pull request:

dependabot[bot]

Version bumps carry no diff that a review can improve.

github-actions[bot]

The GitFlow release and back-merge pull requests, whose content was built, tested and merged before they existed.

Drafts

Collected later by the ready_for_review trigger instead.

A ruleset cannot express the first two. It has no author condition, and its bypass_actors escape hatch accepts an app only on a repository where that app is installed — adding Dependabot as a bypass actor fails with 422 Validation Failed everywhere Dependabot is not configured.

5. Pipeline Flows

5.1. Pull Request Pipeline

When a PR is created or updated against non-main branches:

PR created/updated
       │
       ├──► paths-filter
       │         │
       │         ├──► test-fe (if frontend changed)
       │         │
       │         └──► test-be (if backend changed)
       │
       └──► check-format (Angular repositories; always)
       │
       ▼
  pr-checks — the single required status check

5.2. Develop Branch Pipeline

When code is pushed to the develop branch:

Push to develop
       │
       ▼
  paths-filter
       │
       ├──► test-fe ──┐
       │              │
       └──► test-be ──┤
                      │
                      ▼
                  package
                      │
       ┌──────────────┼──────────────┬──────────────┐
       │              │              │              │
       ▼              ▼              ▼              ▼
   publish     dependencygraph  docker-build    helm-build
                                                    │
                                                    ▼
                                            argocd-update
                                               (env=dev,
                                          chart-version + build-sha)

build-sha is passed only on develop. It writes podAnnotations."app.kubernetes.io/build-sha", forcing a pod rollover when the SNAPSHOT chart version has not changed but the image behind it has.

5.3. Main Branch Pipeline (Release)

When code is pushed to the main branch (typically after a release merge):

Push to main
       │
       ▼
    package (prod profile)
       │
       ├──────────────┬──────────────┐
       │              │              │
       ▼              ▼              ▼
   publish     docker-build     helm-build
                                     │
                          ┌──────────┼──────────────┐
                          ▼                         ▼
                  argocd-update             gitflow-release-finish
                  (env=stage)              (merges main → develop;
                                            bumps SNAPSHOT version)

5.4. Release Branch Pipeline

When code is pushed to a release/** branch (created by manual-release-start.yml):

Push to release/**
       │
       ▼
   test-be
   (verifies the RC before main is updated)

This is intentionally minimal — packaging, image build and chart push are deferred until the release branch is merged into main. The release branch is a candidate, not a deliverable.

Often nothing runs at all, and a release pull request reporting no checks is normal:

  • The push that creates the branch is made by manual-release-start.yml with GITHUB_TOKEN, and events from GITHUB_TOKEN trigger no workflows.

  • push-release.yml ignores *.md, so a release-notes commit runs nothing. See CI and Release Pipeline Traps.

  • The SPA and plugin repositories have no push-release.yml.

  • The release pull request targets main, which pr-non-main.yml ignores.

5.5. Manual Release Start

A workflow_dispatch trigger on manual-release-start.yml lets a human cut a release branch:

workflow_dispatch
   inputs: { version: "X.Y.Z" }
       │
       ▼
   mvn gitflow:release-start
   (creates release/X.Y.Z branch off develop,
    rewrites <revision> from X.Y.Z-SNAPSHOT to X.Y.Z)
       │
       ▼
   gh pr create -B main -H release/X.Y.Z
   (opens a PR back to main for review;
    merging the PR triggers push-main.yml)

This is the only way new versions enter main. There is no direct push from develop to main.

5.6. Helm changes (no separate pipeline)

Chart changes under src/main/helm/** travel with the branch’s normal pipeline: push-dev.yml on develop, push-main.yml on main. Both already call helm-build.yml and then argocd-update.yml, so the chart is packaged, pushed and deployed by the workflow that owns that environment.

Do not add src/main/helm/** to paths-ignore in push-dev.yml or push-main.yml, and do not reintroduce a separate Helm-only workflow.

ems-admin-portal and event-registration-ui previously carried a helm-release.yml triggered on paths: ['src/main/helm/**'] for main and develop, intended to ship a chart change without rebuilding the Java/Angular layers. It could not do that, for two reasons:

  • On main it cannot replace push-main.yml. GitHub skips a run only when every changed file matches paths-ignore, and a GitFlow release merge always carries pom.xml. So push-main.yml ran regardless and both workflows fired — observed on the ems-admin-portal 0.1.0 release merge, where both started in the same second despite push-main.yml listing src/main/helm/** in its paths-ignore.

  • On develop it deployed nothing. Its ArgoCD job was gated to main, so a chart-only commit published a chart and left the dev manifest pointing at the previous targetRevision.

It was also the sole source of two defects: the image.tag pin described in ArgoCD Deployment, and a parent-POM 401 in its version-extraction step that had to be fixed independently in both repos.

Retired in ems-admin-portal ea0b140 and event-registration-ui 732be0a1. All four service repos — event-admin-service, membership-ui, ems-admin-portal, event-registration-ui — now use the same two push workflows with no chart exception.

The legacy event-admin-ui still carries a helm-release.yml. It is dev-only and superseded by ems-admin-portal; treat it as out of pattern rather than as an example.

pr-non-main.yml is the one place src/main/helm/* *should stay in paths-ignore — a chart edit needs no Java or Angular tests, and that workflow performs no deployment.

6. Consuming Workflows

6.1. Referencing Reusable Workflows

Project workflows reference reusable workflows using the uses keyword:

jobs:
  test-be:
    uses: christhonie/event/.github/workflows/test-be.yml@main
    with:
      maven-profiles: dev
      jdk-version: '17'
    secrets:
      MAVEN_PASSWORD: ${{ secrets.EVENT_PACKAGE_REPO_TOKEN }}

6.2. Versioning Strategy

Recommended: Use @main to stay current with the latest reusable workflow improvements.

uses: christhonie/event/.github/workflows/test-be.yml@main

Alternative: Pin to a specific tag for stability when needed:

uses: christhonie/event/.github/workflows/[email protected]

7. Job Dependencies and Caching

7.1. Sequential Execution with needs

Use needs to define job dependencies:

jobs:
  package:
    uses: christhonie/event/.github/workflows/maven-package.yml@main
    # ...

  docker-build:
    needs: [package]  # Wait for package to complete
    uses: christhonie/event/.github/workflows/docker-build.yml@main
    # ...

7.2. Caching Strategy

The maven-package workflow caches the target/ directory, which is then reused by:

  • docker-build - For building Docker images

  • helm-build - For packaging Helm charts

  • publish - For uploading artifacts

  • dependencygraph - For dependency analysis

7.3. Running Jobs After Skipped Dependencies

Use if: always() to run jobs even when dependencies were skipped:

docker-build:
  needs: [package]
  if: always() && needs.package.result == 'success'  (1)
  uses: christhonie/event/.github/workflows/docker-build.yml@main
1 always() alone is rarely what you want. It runs the job when a dependency failed as well as when it skipped, so pair it with an explicit result check unless you intend to proceed after a failure.

always() changes meaning depending on what the job is for.

For a build job, it lets the pipeline continue past a skipped test — pair it with a result == 'success' guard so a genuine failure still stops the line.

For an aggregator whose check is required (see The pr-checks aggregator), always() is mandatory and the job must inspect the results itself. Omitting it there produces a required check that silently reports skipped whenever a dependency fails, which branch protection can treat as a pass.

8. Environment Variables Pattern

Centralize fallback logic at the job level to avoid repetition:

jobs:
  helm-build:
    runs-on: ubuntu-latest
    env:
      JDK_VERSION: ${{ inputs.jdk-version || '17' }}
      MAVEN_PROFILES: ${{ inputs.maven-profiles || 'dev' }}

    steps:
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: ${{ env.JDK_VERSION }}
          # ...

      - name: Package Helm
        run: mvn helm:package -P${{ env.MAVEN_PROFILES }}

9. Required Repository Settings

When bootstrapping a new service repo, two GitHub settings must be configured before the first push to develop, and a third once the repo has its first PR workflow:

9.1. Default workflow permissions: write

Settings → Actions → General → Workflow permissions → "Read and write permissions".

The reusable workflows declare permissions: contents: write (e.g. dependencygraph.yml, gitflow-release-finish.yml). GitHub blocks reusable workflows from elevating beyond the calling workflow’s default GITHUB_TOKEN permissions. New repos default to read, which causes the calling workflow to fail with startup_failure and no jobs created — an opaque error with no log output.

This is the single most common bring-up mistake. Mirror what event-admin-service already has set.

CLI shortcut to flip it:

gh api -X PUT repos/<owner>/<repo>/actions/permissions/workflow \
  -f default_workflow_permissions=write \
  -F can_approve_pull_request_reviews=true

9.2. Reusable workflow access on christhonie/event

On the christhonie/event repo: Settings → Actions → General → Access → "Accessible from repositories owned by the user 'christhonie'".

Without this, calls to christhonie/event/.github/workflows/@main from a service repo fail with "workflow not found" or a permissions error. The default for new repos is "Not accessible". Once set to "user-owned", every existing and future repo under christhonie/ can consume the reusable workflows.

This setting is configured once on christhonie/event, not per service repo.

9.3. Branch protection: require the PR checks context

Settings → Branches → Add rule → develop → "Require status checks to pass before merging" → select PR checks.

Without this, the pipeline is advisory. admin-portal PR #85 merged with a red Back-end / ✅ Test, broke develop, and stayed broken through the next merge — the check was correct, nothing enforced it.

Require exactly one context: PR checks. Never require Back-end / ✅ Test or Front-end / ✅ Test directly; paths-filter skips them by design and a skipped required check is not a pass. See The pr-checks aggregator.

CLI equivalent:

gh api -X PUT repos/<owner>/<repo>/branches/develop/protection \
  --input - <<'JSON'
{
  "required_status_checks": { "strict": false, "contexts": ["PR checks"] },
  "enforce_admins": false,
  "required_pull_request_reviews": null,
  "restrictions": null
}
JSON

Enabling this requires that pr-non-main.yml has no trigger-level paths-ignore, or a docs-only PR will never create the check and can never be merged. Make that change first. See the warning under Excluding Non-Build Directories.

strict: false is deliberate — strict: true forces every branch to be up to date with the base before merging, which serialises merges and adds little on a small team.

Do not apply this rule to main. pr-non-main.yml triggers on branches-ignore: [main], so a PR targeting main never emits a PR checks context at all — requiring it there leaves every release PR permanently pending. Protecting main needs a separate workflow that emits its own aggregator context; that does not exist today.

9.3.1. Sweep open PRs before you enable it

For pull_request events GitHub runs the workflow definition from the head branch, not from the base. Every branch created before the aggregator merged therefore produces no PR checks context, and its PR sits at mergeStateStatus=BLOCKED indefinitely — note that mergeable still reads MERGEABLE, so mergeable alone will not reveal this.

Before enabling protection, list what is open and account for it:

gh pr list --repo <owner>/<repo> --base develop --state open \
  --json number,headRefName -q '.[] | "#\(.number) \(.headRefName)"'

Each affected PR is unblocked by merging (or rebasing onto) the base branch so the head picks up the workflow. For Dependabot PRs, comment @dependabot rebase on each.

This is a real adoption cost, not a formality: when the aggregator landed in membership-ui there were 29 open Dependabot PRs, every one of which would have been blocked. Protection was deliberately deferred there for that reason, while registration-portal — with no open PRs — was enabled immediately.

9.3.2. Auditing existing protection

gh api repos/<owner>/<repo>/branches/<branch>/protection returns HTTP 404 with a JSON error body on an unprotected branch, and gh writes that body to stdout. A script that only tests whether output is non-empty will therefore report an unprotected branch as "protected with no required contexts" — the opposite of the truth. Test the status code:

code=$(gh api "repos/$repo/branches/$br/protection" --silent -i 2>/dev/null | head -1 | awk '{print $2}')
[ "$code" = "200" ] || echo "NOT PROTECTED"

9.4. service-name ↔ chart name ↔ ArgoCD manifest filename

The service-name input passed to argocd-update.yml must match three things:

  • The Helm chart’s name: in Chart.yaml

  • The Docker image name on Docker Hub (christhonie/<service-name>)

  • The ArgoCD manifest filename in idl-xnl-jhb-rc01: argocd/<service-name>-<env>.yml

It does not need to match the GitHub repo name — for example, christhonie/ems-mcp-server ships chart name ems-mcp-server, image christhonie/ems-mcp-server, and manifest ems-mcp-server-prod.yml. The repo name happens to match in this case, but the convention links chart/image/manifest, not repo.

10. Secrets Configuration

10.1. Required Secrets

Secret Purpose Used By

GITHUB_TOKEN

Built-in token for repository operations

Git operations, GitHub API

EVENT_PACKAGE_REPO_TOKEN

Standard secret name for GitHub Packages access (Maven/NPM)

maven-package, test-be, test-fe, maven-deploy

DOCKER_PAT

Docker Hub Personal Access Token

docker-build, helm-build

ARGOCD_REPO_TOKEN

Access to ArgoCD configuration repository

argocd-update

RELEASE_PAT

Administrator PAT used for the release back-merge push and for Copilot review requests — see Repository Rules

gitflow-release-finish, copilot-review

10.2. Secret Variable Mapping

The EVENT_PACKAGE_REPO_TOKEN secret is internally mapped to workflow-specific variables:

# In workflow steps
secrets:
  MAVEN_PASSWORD: ${{ secrets.EVENT_PACKAGE_REPO_TOKEN }}
  NPM_PASSWORD: ${{ secrets.EVENT_PACKAGE_REPO_TOKEN }}

10.3. Maven Repository Configuration

The setup-java action creates a settings.xml file with credentials:

- name: Set up JDK
  uses: actions/setup-java@v3
  with:
    java-version: '17'
    distribution: 'temurin'
    cache: maven
    server-id: github-christhonie
    server-username: MAVEN_USERNAME
    server-password: MAVEN_PASSWORD

- name: Build with Maven
  run: mvn --batch-mode package
  env:
    MAVEN_USERNAME: christhonie
    MAVEN_PASSWORD: ${{ secrets.EVENT_PACKAGE_REPO_TOKEN }}

The generated settings.xml will contain:

<servers>
  <server>
    <id>github-christhonie</id>
    <username>${env.MAVEN_USERNAME}</username>
    <password>${env.MAVEN_PASSWORD}</password>
  </server>
</servers>

11. Artifact Management

11.1. Build Artifacts

Build artifacts (JAR files) are uploaded to GitHub Actions artifacts for investigation:

- name: Upload Artifacts
  uses: actions/upload-artifact@v4
  with:
    name: build-artifacts
    path: target/*.jar
    retention-days: 14
Artifacts are automatically purged after the retention period.

11.2. Dependency Graph (SBOM)

The dependencygraph workflow submits the Maven dependency graph to GitHub’s dependency graph feature, enabling:

  • Security vulnerability scanning

  • Software Bill of Materials (SBOM) generation

  • Dependency alerts

12. Docker Compose Version Synchronization

Retired. The sync-docker-versions autotask no longer runs in any active repository. src/main/docker/dev.yml is a manually-maintained developer convenience file that is expected to drift from <revision>.

It was removed because it broke releases in two distinct ways — a GH006 protected-branch rejection on main, and a recurring dev.yml three-way merge conflict during gitflow:release-finish. See Docker Compose Version Sync for the full account and for how to update the compose files by hand.

Removed in event-registration-ui PR #56 (41659b93) and membership-ui PR #116. event-admin-service and ems-admin-portal never had it. The legacy event-admin-ui still carries sync-docker-versions.yml; treat it as out of pattern, not as an example.

13. Version Strategy for Docker/Helm Artifacts

TODO: Implement build number conversion for SNAPSHOT versions:

  • Docker containers and Helm charts ending in -SNAPSHOT should include a build number

  • Pattern: 1.2.3-SNAPSHOT1.2.3-SNAPSHOT-{buildNumber} or 1.2.3-{buildNumber}

  • Ensures each build has a unique, retrievable version identifier

  • Build number options: GitHub run number, timestamp, or short commit SHA