Skip to content

Publish Docker image #8

Publish Docker image

Publish Docker image #8

name: Publish Docker image
on:
push:
tags:
# v1.2.3 → prod (release estable)
# v1.2.3-rc.1 → test (release candidate)
# v1.2.3-beta.1 → test (beta)
- "v*.*.*"
workflow_dispatch:
inputs:
target_environment:
description: "Entorno destino — solo para re-deploys de emergencia"
required: true
default: test
type: choice
options:
- test
- prod
permissions:
contents: read
id-token: write
jobs:
resolve-env:
runs-on: ubuntu-latest
outputs:
target: ${{ steps.resolve.outputs.target }}
steps:
- id: resolve
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "target=${{ inputs.target_environment }}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.ref_name }}" == *-* ]]; then
echo "target=test" >> "$GITHUB_OUTPUT"
else
echo "target=prod" >> "$GITHUB_OUTPUT"
fi
push_to_registry:
needs: resolve-env
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
timeout-minutes: 20
environment: ${{ needs.resolve-env.outputs.target }}
env:
APP_ENV: ${{ needs.resolve-env.outputs.target }}
DOCKERHUB_IMAGE: ${{ secrets.DOCKER_USERNAME }}/apiflask-demo
DATABASE_URL: ${{ vars['DATABASE_URL'] }}
APP_SECRET_KEY: ${{ secrets['APP_SECRET_KEY'] }}
APP_SECURITY_PASSWORD_SALT: ${{ secrets['APP_SECURITY_PASSWORD_SALT'] }}
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Lint Dockerfile
# Falla temprano por malas practicas del Dockerfile.
run: docker run --rm -i hadolint/hadolint:v2.12.0 < Dockerfile
- name: Trivy config scan
# Escanea solo el Dockerfile del build para evitar falsos bloqueos por
# configuraciones no relacionadas con la imagen a publicar.
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: config
scan-ref: Dockerfile
hide-progress: true
severity: HIGH,CRITICAL
exit-code: '1'
- name: Log in to Docker Hub
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.DOCKERHUB_IMAGE }}
tags: |
# Tag inmutable por commit para trazabilidad.
type=sha
# Tag semver desde el git tag.
type=semver,pattern={{version}}
# latest solo en releases estables de la rama por defecto.
type=raw,value=latest,enable=${{ github.ref_type == 'tag' && !contains(github.ref_name, '-') }}
- name: Build and push Docker image
id: build_and_push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0.15.0
- name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0
- name: Build immutable image reference
id: image_ref
run: |
IMAGE_REF="${DOCKERHUB_IMAGE}@${{ steps.build_and_push.outputs.digest }}"
echo "IMAGE_REF=$IMAGE_REF" >> "$GITHUB_ENV"
echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT"
- name: Esperar propagación de la imagen en Docker Hub
# Docker Hub puede tardar unos segundos en exponer el digest recién publicado.
# Sin esta espera, Trivy puede fallar con "image not found" de forma intermitente.
run: |
for attempt in {1..10}; do
if docker buildx imagetools inspect "$IMAGE_REF" > /dev/null 2>&1; then
echo "Imagen disponible en Docker Hub: $IMAGE_REF"
exit 0
fi
echo "Intento $attempt/10: digest aún no visible, reintentando..."
sleep 6
done
echo "No se pudo resolver la imagen en Docker Hub: $IMAGE_REF"
exit 1
- name: Trivy image scan
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: ${{ steps.image_ref.outputs.image_ref }}
format: table
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: '1'
- name: Generate SBOM (SPDX)
run: syft "$IMAGE_REF" -o spdx-json=sbom.spdx.json
- name: Sign image (keyless with OIDC)
run: cosign sign --yes "$IMAGE_REF"
- name: Attach SBOM attestation
run: |
cosign attest --yes \
--predicate sbom.spdx.json \
--type spdxjson \
"$IMAGE_REF"
- name: Verify signature and attestation
run: |
if [[ "${{ github.ref_type }}" == "tag" ]]; then
CERT_REF_REGEX='refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?'
else
CERT_REF_REGEX='refs/heads/main'
fi
# El regexp restringe la verificación al workflow exacto que firmó la imagen:
# - repositorio: ${{ github.repository }} (no cualquier repo de GitHub)
# - archivo: envia-a-docker.yaml (no cualquier workflow del repo)
# - ref: tags semver (incluye prerelease) o rama main en re-deploy manual
# Un regexp más permisivo (ej. "https://github.com/.+") acepta firmas de
# cualquier repo o workflow, anulando la garantía de provenance.
cosign verify \
--certificate-identity-regexp "https://github.com/${{ github.repository }}/.github/workflows/envia-a-docker.yaml@${CERT_REF_REGEX}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE_REF"
cosign verify-attestation \
--type spdxjson \
--certificate-identity-regexp "https://github.com/${{ github.repository }}/.github/workflows/envia-a-docker.yaml@${CERT_REF_REGEX}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE_REF"
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-dockerhub-${{ github.sha }}
path: sbom.spdx.json
if-no-files-found: error