build(deps): bump python from 2c941e8 to e5c9fa2
#1430
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: Build Multi-Platform Docker Images | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| tags: [ 'v*' ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| inputs: | |
| platforms: | |
| description: 'Target platforms (comma-separated)' | |
| required: false | |
| default: 'linux/amd64,linux/arm64' | |
| type: string | |
| push_to_registry: | |
| description: 'Push to Docker Hub' | |
| required: false | |
| default: true | |
| type: boolean | |
| env: | |
| REGISTRY: docker.io | |
| IMAGE_NAME: certmate | |
| # Default to read-only. The build job authenticates to Docker Hub via the | |
| # DOCKERHUB_TOKEN secret, not the workflow GITHUB_TOKEN, so no write | |
| # permission on the repo is needed for the push. If we ever add SLSA | |
| # attestation here (Phase 2), `id-token: write` lands at the job level. | |
| permissions: read-all | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Isolate docker config dir (per-run) | |
| # Install the buildx CLI plugin into a per-run docker config dir instead | |
| # of the host-shared ~/.docker/cli-plugins. Every runner instance on this | |
| # host runs as the same user and shares ~/.docker, so two concurrent runs | |
| # (the main-branch push and the tag push from one `git push origin main | |
| # vX.Y.Z`) raced: one executed docker-buildx while the other overwrote it | |
| # -> ETXTBSY "text file is busy". A unique DOCKER_CONFIG removes the shared | |
| # target so builds never collide. (runner.* is only available inside steps, | |
| # not in job-level env, so this is a step rather than `env:`.) | |
| run: | | |
| cfg="${RUNNER_TEMP}/.docker-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| mkdir -p "$cfg" | |
| echo "DOCKER_CONFIG=$cfg" >> "$GITHUB_ENV" | |
| - name: Set up QEMU | |
| # GitHub-hosted runners have no binfmt for arm64; QEMU emulation lets | |
| # buildx cross-build linux/arm64 on push/tag (PRs build amd64 only). | |
| uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 | |
| with: | |
| version: latest | |
| - name: Log in to Docker Hub | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ secrets.DOCKERHUB_USER }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Compute image namespace | |
| id: ns | |
| # GitHub Actions doesn't pass secrets.* to workflows triggered by | |
| # fork-originated PRs. With the previous template the build job was | |
| # constructing a malformed tag like `docker.io//certmate:pr-122` | |
| # (note the double slash) and exiting before any test-correctness | |
| # signal could land. Fall back to the repo owner so the tag is | |
| # always well-formed; push is still disabled for PRs separately. | |
| run: | | |
| OWNER="${{ secrets.DOCKERHUB_USER }}" | |
| if [ -z "$OWNER" ]; then OWNER="${{ github.repository_owner }}"; fi | |
| echo "namespace=$OWNER" >> "$GITHUB_OUTPUT" | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ steps.ns.outputs.namespace }}/${{ env.IMAGE_NAME }} | |
| # latest = released: only v* tag builds (cut after the release.sh | |
| # real-cert e2e gate) may publish :latest; pushes to main get branch | |
| # tags only (e.g. :main), because CI performs no real-ACME validation. | |
| tags: | | |
| type=ref,event=branch | |
| type=ref,event=pr | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=semver,pattern=v{{version}} | |
| type=semver,pattern=v{{major}}.{{minor}} | |
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| flavor: | | |
| latest=auto | |
| - name: Determine platforms | |
| id: platforms | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "platforms=${{ github.event.inputs.platforms }}" >> $GITHUB_OUTPUT | |
| elif [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # PR builds validate the Dockerfile only; amd64 is enough and avoids | |
| # slow arm64 QEMU emulation on GitHub-hosted runners. | |
| echo "platforms=linux/amd64" >> $GITHUB_OUTPUT | |
| else | |
| echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Determine push setting | |
| id: should_push | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "push=false" >> $GITHUB_OUTPUT | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "push=${{ github.event.inputs.push_to_registry }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "push=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build and push Docker image | |
| id: build | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| platforms: ${{ steps.platforms.outputs.platforms }} | |
| push: ${{ steps.should_push.outputs.push }} | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| REQUIREMENTS_FILE=requirements.txt | |
| - name: Update Docker Hub description | |
| if: github.ref == 'refs/heads/main' && steps.should_push.outputs.push == 'true' | |
| continue-on-error: true # Don't fail build if description update fails | |
| uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v4 (was v3) | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USER }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| repository: ${{ secrets.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }} | |
| readme-filepath: ./README.dockerhub.md | |
| short-description: 'SSL Certificate Management System - 22 DNS providers, Multi-CA support, Enterprise-ready' | |
| - name: Image digest | |
| if: steps.should_push.outputs.push == 'true' | |
| run: echo ${{ steps.build.outputs.digest }} | |
| security-scan: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| # Runs on pull requests too (#412). It used to be skipped there, and on | |
| # pushes it scanned the floating `:latest` tag — which, on a push to main, | |
| # is still the PREVIOUS release's image (see the tag policy above). So a | |
| # CRITICAL introduced by the current change was never scanned by anyone. | |
| # The image under test is now rebuilt here from the same buildx cache and | |
| # loaded locally, so what is scanned is what was just built. | |
| # Job-level permission override: the workflow default is read-all | |
| # (set at the top of this file in v2.6.2), but the Trivy SARIF | |
| # upload step needs to write into the Security > Code scanning | |
| # tab. Granting it here keeps every other step read-only. | |
| permissions: | |
| security-events: write | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 | |
| - name: Build the image under test (amd64, loaded locally) | |
| # Reuses the buildx cache the build job just populated, so this is a | |
| # cache hit rather than a second full build. | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| load: true | |
| push: false | |
| tags: certmate:scan | |
| cache-from: type=gha | |
| build-args: | | |
| REQUIREMENTS_FILE=requirements.txt | |
| - name: Run Trivy vulnerability scanner (report) | |
| # Pinned to v0.36.0 (was @master — never pin to a mutable branch | |
| # in CI; upstream HEAD changes silently bring new behaviour or | |
| # become an attack surface if compromised). | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: 'certmate:scan' | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| severity: 'HIGH,CRITICAL' | |
| - name: Fail on a fixable CRITICAL | |
| # The reporting step above deliberately does not gate: the image | |
| # carries a long tail of unfixable base-image findings tracked in #403, | |
| # and a gate that is always red is a gate nobody reads. This one has | |
| # teeth precisely where action is possible — a CRITICAL with a fix | |
| # available means a base-image or dependency bump is due. | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: 'certmate:scan' | |
| format: 'table' | |
| # Vulnerabilities only. The secret scanner flags a .pyc inside the | |
| # third-party certbot_dns_google package as a GCP service account — | |
| # a false positive that would make this gate permanently red. Secret | |
| # findings still reach the Security tab via the SARIF above. | |
| scanners: 'vuln' | |
| severity: 'CRITICAL' | |
| ignore-unfixed: true | |
| exit-code: '1' | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 | |
| # Push/tag only. On a pull request the SARIF lands on the PR ref, so the | |
| # image's long tail of unfixable base-image CVEs (tracked in #403) shows | |
| # up as new code-scanning alerts and blocks the merge — the | |
| # always-red-gate failure mode this scan was fixed to avoid (#412). On a | |
| # PR the *gate* is the "Fail on a fixable CRITICAL" step above, which is | |
| # actionable; the Security-tab inventory belongs to main. | |
| if: always() && github.event_name != 'pull_request' | |
| with: | |
| sarif_file: 'trivy-results.sarif' |