GitHub Actions per Service

Workflow for order-service

GitHub Actions

In a monorepo, the paths filter makes this workflow run only when order-service changes:

.github/workflows/order-service.yml

name: order-service

on:
  push:
    branches: [main]
    paths:
      - "order-service/**"
      - ".github/workflows/order-service.yml"
  pull_request:
    paths:
      - "order-service/**"

env:
  IMAGE: ghcr.io/shopease/order-service

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    defaults:
      run:
        working-directory: order-service
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
          cache: maven

      - name: Test (unit + Testcontainers integration + contract)
        run: ./mvnw -B verify

      - name: Log in to GitHub Container Registry
        if: github.ref == 'refs/heads/main'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push image
        if: github.ref == 'refs/heads/main'
        uses: docker/build-push-action@v6
        with:
          context: order-service
          push: true
          tags: ${{ env.IMAGE }}:${{ github.sha }}

  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: azure/setup-kubectl@v4
      - name: Roll out the new image
        env:
          KUBECONFIG_DATA: ${{ secrets.STAGING_KUBECONFIG }}
        run: |
          echo "$KUBECONFIG_DATA" > kubeconfig && export KUBECONFIG=kubeconfig
          kubectl -n shopease set image deployment/order-service order-service=${{ env.IMAGE }}:${{ github.sha }}
          kubectl -n shopease rollout status deployment/order-service --timeout=5m

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production          # GitHub "required reviewers" = manual approval gate
    steps:
      - run: echo "same steps as staging, with the production kubeconfig"

Example output

order-service  #482  main  a9c41e7
  build             passed  3m 12s   (Tests run: 148, Failures: 0)
  deploy-staging    passed  1m 05s   deployment "order-service" successfully rolled out
  deploy-production waiting for approval from @shopease/payments-leads

Keep 15 Pipelines DRY with a Reusable Workflow

Calling a reusable workflow

Put the common steps in one reusable workflow and call it from each service's small workflow:

.github/workflows/payment-service.yml

# .github/workflows/payment-service.yml
name: payment-service
on:
  push:
    branches: [main]
    paths: ["payment-service/**"]
jobs:
  pipeline:
    uses: ./.github/workflows/java-service.yml      # has: on: workflow_call
    with:
      service: payment-service
    secrets: inherit
  • Add an image scan step (Trivy, Grype) and fail on critical vulnerabilities.
  • Store only immutable tags in production: the Git SHA or a release version, never latest.
  • Many teams use GitOps: CI only updates the image tag in a deployment repo, and Argo CD or Flux syncs the cluster to that repo.

Database Migrations in the Pipeline

Each service owns its schema, so each service runs its own Flyway or Liquibase migrations at startup (see the Hibernate & JPA material). During a rolling update old and new Pods run at the same time, so every migration must work with both versions. Use expand and contract:

  1. Expand (release 1): add the new column delivery_slot as nullable; new code writes both old and new columns.
  2. Migrate: backfill old rows with a batch job.
  3. Contract (release 2, after all Pods run the new code): stop using the old column, then drop it.

Note: Never rename or drop a column in the same release that stops using it. A rollback to the previous version would crash.