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

3

u/Raja-Karuppasamy 1d ago

DEBUG: 'True' and ALLOWED_HOST: '*' in the test env is fine for CI, but worth a comment noting these should never leak into what gets built into the actual image, easy mistake to make later if someone copies this job as a starting point for a “real” deploy workflow.

bigger one: you’re tagging the image with ${{ github.sha }} only, no latest or version tag. that’s actually good practice for rollback purposes (don’t let past-me tell you otherwise, i learned this one the hard way), but make sure whatever deploys this image knows how to find the right sha, or you’ll end up manually digging through registry tags at 2am.

also packages: write permission scoped at the job level is the right call, a lot of people just set it at workflow level for everything, which is broader than it needs to be.

one thing missing: no caching for pip installs. actions/setup-python supports cache: 'pip' , small change, speeds up every run once your dependency list grows.

2

u/Raja-Karuppasamy 1d ago

DEBUG: 'True' and ALLOWED_HOST: '*' in the test env is fine for CI, but worth a comment noting these should never leak into what gets built into the actual image, easy mistake to make later if someone copies this job as a starting point for a “real” deploy workflow.

bigger one: you’re tagging the image with ${{ github.sha }} only, no latest or version tag. that’s actually good practice for rollback purposes (don’t let past-me tell you otherwise, i learned this one the hard way), but make sure whatever deploys this image knows how to find the right sha, or you’ll end up manually digging through registry tags at 2am.

also packages: write permission scoped at the job level is the right call, a lot of people just set it at workflow level for everything, which is broader than it needs to be.

one thing missing: no caching for pip installs. actions/setup-python supports cache: 'pip' , small change, speeds up every run once your dependency list grows.

2

u/Little-Squad-X 1d ago

I don't know what triggered this CI. Is it manual, push, or PR?

If it is triggered by a PR or even a push (which not many people usually do), it’s best to have "concurrency" set up. This way, you don't spam the runner and overload it.

https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency

2

u/radiales 1d ago

Because nobody wrote it yet, try to use git hashes instead of version numbers as those could be compromised when a repo gets hacked

2

u/Low-Opening25 1d ago

This is a pipeline that anyone can whip out in 10 mins. Where is validation, code quality scan, security scan, and where is actual release pipeline with version tagging? also it looks like generated with AI, Claude likes to use the same job name schema.

1

u/Mukul-nst 1d ago

Brother I am still learning about it. That's why I asked for suggestions. Thank you for your suggestions. Will surely implement what you suggested

4

u/DevOps_Lady 2d ago

I'm not 100% sure about this but you are testing the code but not the docker image. I don't remember the syntax and on phone but maybe search testing docker images with github actions.

3

u/Sure_Stranger_6466 For Hire - US Remote 2d ago

POSTGRES_PASSWORD

This should be encrypted via ${{ secrets.POSTGRES_PASSWORD }}.

2

u/razzledazzled 2d ago

I would read about github secrets and the difference with variables. You can look into environments and rulesets to protect main branch. Other than the glaringly obvious security concerns with your testing step, you're just using shared actions so I don't know what there is to critique. GHA is just a collection of text files, it's not really complicated.

1

u/codemochi 8h 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.

0

u/trainurdoggos 2d ago

Beyond what’s already been mentioned:

For edification purposes and personal development, what you are doing here might technically work but, in an enterprise or corporate situation this would never fly.

You shouldn’t be spinning up a Postgres instance with every build and test. In my opinion and my experience, the app should be able to test and build without a database connection.

But IF you do NEED it, it should be connecting out to a dev instance you have set up just for testing against. Pulling a whole database image in for a test and build introduces a crazy amount of security risks into the CI.

My corpo security team would be on us hard if we did this.

2

u/Sinless27 2d ago

Assuming your database is defined in code, I don’t see a problem with it. My org pretty heavily uses dacpacs to run integration tests with https://testcontainers.com responsible for creating the sql containers and then deploying the dacpac to them.

1

u/trainurdoggos 2d ago

Fair. I just thought it was worth mentioning. Haven't seen this done anywhere I work, but if your place is not concerned about it, then shrug. I don't thinks it an especially big deal, but like I said, at the place I work now, our security and architecture teams would be on us about this. We use pools of self managed GHA runners, so that may be why. I don't know, as just my two cents.

1

u/ThatSituation9908 2d ago

I disagree. If you can do shift-left testing, where you run integration testing during PR, you should. It's much easier to do with smaller apps.

In some practices, integration test can replace unit tests. I would find this uncomfortable, but behavioral-driven testing does this.