r/devops • u/Mukul-nst • 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 }}
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.
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.
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.