Publish Docker image #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |