r/devops 2d ago

Discussion CI pipeline using Github actions

I started learning CI/CD using github actions after containerising my application and I have created CI pipeline for django app that runs test, builds and pushes image to github container registry.
I am sharing my yaml file for CI pipeline. Please do share your thoughts and where can i improve.

name: Test Pipeline 
on: 
  push:
jobs:
  test-backend:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:14
        ports:
          - 5432:5432
        env: 
          POSTGRES_USER: test_user
          POSTGRES_DB: erp
          POSTGRES_PASSWORD: 123456

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: setup python
        uses: actions/setup-python@v5
        with: 
          python-version: "3.13.5"

      - name: install dependencies
        run: pip install -r Backend/requirement.txt

      - name: run tests
        env: 
          DATABASE_URL: postgresql://test_user:123456@localhost:5432/erp
          DEBUG: 'True'
          ALLOWED_HOST: '*'
        run: |
          cd Backend 
          python manage.py test

  build-and-push-image:
    needs: test-backend
    permissions:
      contents: read
      packages: write
    runs-on: ubuntu-latest
    steps:
      - name: login to ghcr
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}


      - name: checkout repo
        uses: actions/checkout@v4


      - name: build image
        run: docker build -t ghcr.io/namespace/erp:${{ github.sha }} ./Backend


      - name: push image
        run: docker push ghcr.io/namespace/erp:${{ github.sha }}
0 Upvotes

14 comments sorted by

View all comments

1

u/codemochi 13h ago

Since you asked where to improve, here's one way you could restructure it. The two jobs get combined into one, tests get run in the image and gating narrows the image push to the main branch only:

    jobs:
      build-test-push:
        runs-on: ubuntu-latest
        timeout-minutes: 15
        permissions:
          contents: read
          packages: write
        services:
          postgres:
            image: postgres:14
            env:
              POSTGRES_USER: test_user
              POSTGRES_DB: erp
              POSTGRES_PASSWORD: 123456
            ports:
              - 5432:5432
        steps:
          - uses: actions/checkout@v4

          - uses: docker/setup-buildx-action@v3

          - name: build image, load into local docker
            uses: docker/build-push-action@v6
            with:
              context: ./Backend
              load: true
              tags: ghcr.io/namespace/erp:${{ github.sha }}
              cache-from: type=gha
              cache-to: type=gha,mode=max

          - name: run tests inside that image
            run: |
              docker run --rm --network host \
                -e DATABASE_URL=postgresql://test_user:123456@localhost:5432/erp \
                -e DEBUG=True -e ALLOWED_HOST='*' \
                ghcr.io/namespace/erp:${{ github.sha }} \
                python manage.py test

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

          - name: push
            if: github.ref == 'refs/heads/main'
            run: docker push ghcr.io/namespace/erp:${{ github.sha }}

Three things worth knowing about why it's shaped this way:

* `--network host` is there because the postgres service publishes 5432 on the runner host, and a plain `docker run` container can't see it via its own localhost. Host networking keeps your DATABASE_URL unchanged.

* `load: true` plus a separate `docker push` means the image you tested and the image you push are the same bytes, not a rebuild that's probably identical.

* The gha cache replaces pip caching entirely, but only pays off if your Dockerfile copies requirement.txt and installs it before copying the rest of the source. Otherwise every commit invalidates the install layer.

The tests-inside-the-image step assumes manage.py sits in the image's WORKDIR, adjust the command if your Dockerfile lays it out differently.