77 branches :
88 - main
99 workflow_dispatch :
10+ inputs :
11+ promote :
12+ description : ' Promote to production tags after build'
13+ required : false
14+ type : boolean
15+ default : false
1016
1117jobs :
1218 build :
13- name : Build & Deploy
19+ name : Build Images
1420 runs-on : ubuntu-latest
1521 permissions :
1622 contents : read
@@ -49,38 +55,125 @@ jobs:
4955 username : ${{ github.actor }}
5056 password : ${{ secrets.GITHUB_TOKEN }}
5157
58+ - name : Check if image already exists
59+ id : check
60+ run : |
61+ IMAGE="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }}"
62+ echo "Checking if ${IMAGE} already exists..."
63+
64+ if docker manifest inspect "${IMAGE}" >/dev/null 2>&1; then
65+ echo "✅ Image already exists, skipping build"
66+ echo "exists=true" >> $GITHUB_OUTPUT
67+ else
68+ echo "❌ Image does not exist, will build"
69+ echo "exists=false" >> $GITHUB_OUTPUT
70+ fi
71+
72+ - name : Extract metadata
73+ if : steps.check.outputs.exists == 'false'
74+ id : meta
75+ uses : docker/metadata-action@v5
76+ with :
77+ images : ghcr.io/${{ github.repository_owner }}/python-container-builder
78+ tags : |
79+ type=sha,prefix=${{ matrix.python_version }}-,format=long
80+ type=ref,event=pr,prefix=pr-,suffix=-${{ matrix.python_version }}
81+
5282 - name : Build and push Docker images
83+ if : steps.check.outputs.exists == 'false'
5384 uses : docker/build-push-action@v6
5485 with :
5586 context : .
5687 file : Dockerfile
5788 platforms : linux/amd64,linux/arm64
58- push : ${{ github.event_name != 'pull_request' }}
89+ push : true
5990 build-args : |
6091 DEBIAN_VERSION=${{ matrix.debian_version }}
6192 PYTHON_VERSION=${{ matrix.python_version }}
62- tags : |
63- ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}
64- ${{ matrix.python_version == '3.14' && format('ghcr.io/{0}/python-container-builder:latest', github.repository_owner) || '' }}
65- provenance : false
66- outputs : type=image,name=python-container-builder,annotation-index.org.opencontainers.image.description=build your Python distroless containers with this
93+ tags : ${{ steps.meta.outputs.tags }}
94+ labels : ${{ steps.meta.outputs.labels }}
95+ cache-from : type=gha,scope=python-${{ matrix.python_version }}
96+ cache-to : type=gha,mode=max,scope=python-${{ matrix.python_version }}
97+ provenance : true
98+ sbom : true
99+ outputs : type=image,name=python-container-builder,annotation-index.org.opencontainers.image.description=Build your Python distroless containers with this
100+
101+ promote :
102+ name : Promote to Production
103+ runs-on : ubuntu-latest
104+ needs : [build, test, security-scan]
105+ # Promote on:
106+ # 1. Normal merge to main (not force push)
107+ # 2. Manual workflow dispatch with promote flag enabled
108+ # CRITICAL: Only runs if build, test, AND security-scan all succeed
109+ if : |
110+ (github.event_name == 'push' && github.ref == 'refs/heads/main' && !github.event.forced) ||
111+ (github.event_name == 'workflow_dispatch' && inputs.promote == true)
112+ permissions :
113+ contents : read
114+ packages : write
115+ strategy :
116+ matrix :
117+ python_version : ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
118+ steps :
119+ - name : Log in to GitHub Container Registry
120+ uses : docker/login-action@v3
121+ with :
122+ registry : ghcr.io
123+ username : ${{ github.actor }}
124+ password : ${{ secrets.GITHUB_TOKEN }}
125+
126+ - name : Promote commit SHA to version tag
127+ run : |
128+ # Get the full commit SHA
129+ COMMIT_SHA="${{ github.sha }}"
130+
131+ # Source image with commit SHA
132+ SOURCE_IMAGE="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${COMMIT_SHA}"
133+
134+ # Destination tags
135+ VERSION_TAG="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}"
136+
137+ echo "Promoting ${SOURCE_IMAGE} to ${VERSION_TAG}"
138+
139+ # Re-tag the existing image (no rebuild)
140+ docker buildx imagetools create \
141+ "${SOURCE_IMAGE}" \
142+ --tag "${VERSION_TAG}"
143+
144+ echo "✅ Successfully promoted ${{ matrix.python_version }} to production"
145+
146+ - name : Promote latest tag
147+ if : matrix.python_version == '3.14'
148+ run : |
149+ COMMIT_SHA="${{ github.sha }}"
150+ SOURCE_IMAGE="ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${COMMIT_SHA}"
151+ LATEST_TAG="ghcr.io/${{ github.repository_owner }}/python-container-builder:latest"
152+
153+ echo "Promoting ${SOURCE_IMAGE} to ${LATEST_TAG}"
154+
155+ docker buildx imagetools create \
156+ "${SOURCE_IMAGE}" \
157+ --tag "${LATEST_TAG}"
158+
159+ echo "✅ Successfully promoted latest tag"
67160
68161 security-scan :
69162 name : Security Scan
70163 runs-on : ubuntu-latest
71164 needs : build
72- if : github.event_name != 'pull_request'
73165 permissions :
74166 contents : read
75167 security-events : write
76168 steps :
77169 - name : Run Trivy vulnerability scanner
78170 uses : aquasecurity/trivy-action@master
79171 with :
80- image-ref : ghcr.io/${{ github.repository_owner }}/python-container-builder:latest
172+ image-ref : ghcr.io/${{ github.repository_owner }}/python-container-builder:3.14-${{ github.sha }}
81173 format : ' sarif'
82174 output : ' trivy-results.sarif'
83175 severity : ' CRITICAL,HIGH'
176+ exit-code : ' 1'
84177
85178 - name : Upload Trivy results to GitHub Security
86179 uses : github/codeql-action/upload-sarif@v4
@@ -92,15 +185,15 @@ jobs:
92185 uses : aquasecurity/trivy-action@master
93186 if : always()
94187 with :
95- image-ref : ghcr.io/${{ github.repository_owner }}/python-container-builder:latest
188+ image-ref : ghcr.io/${{ github.repository_owner }}/python-container-builder:3.14-${{ github.sha }}
96189 format : ' table'
97190 severity : ' CRITICAL,HIGH'
191+ exit-code : ' 1'
98192
99193 test :
100194 name : Test Images
101195 runs-on : ubuntu-latest
102196 needs : build
103- if : github.event_name != 'pull_request'
104197 permissions :
105198 contents : read
106199 strategy :
@@ -109,32 +202,32 @@ jobs:
109202 steps :
110203 - name : Test Python version
111204 run : |
112- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} python --version
205+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} python --version
113206
114207 - name : Test uv is installed
115208 run : |
116- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} uv --version
209+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} uv --version
117210
118211 - name : Test poetry is installed
119212 run : |
120- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} poetry --version
213+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} poetry --version
121214
122215 - name : Test pipenv is installed
123216 run : |
124- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} pipenv --version
217+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} pipenv --version
125218
126219 - name : Test pdm is installed
127220 run : |
128- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} pdm --version
221+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} pdm --version
129222
130223 - name : Test venv is created
131224 run : |
132- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} sh -c 'test -d /.venv && echo "venv exists"'
225+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} sh -c 'test -d /.venv && echo "venv exists"'
133226
134227 - name : Test package installation with uv
135228 run : |
136- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} sh -c 'uv pip install requests && python -c "import requests; print(f\"requests {requests.__version__} imported successfully\")"'
229+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} sh -c 'uv pip install requests && python -c "import requests; print(f\"requests {requests.__version__} imported successfully\")"'
137230
138231 - name : Test package installation with pip
139232 run : |
140- docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }} sh -c 'pip install click && python -c "import click; print(f\"click {click.__version__} imported successfully\")"'
233+ docker run --rm ghcr.io/${{ github.repository_owner }}/python-container-builder:${{ matrix.python_version }}-${{ github.sha }} sh -c 'pip install click && python -c "import click; print(f\"click {click.__version__} imported successfully\")"'
0 commit comments