Skip to content
Latchkey

Build and Publish the titus-scanner Docker Image workflow (SpecterOps/Nemesis)

The Build and Publish the titus-scanner Docker Image workflow from SpecterOps/Nemesis, explained and optimized by Latchkey.

C

CI health: C - fair

Point runs-on at Latchkey and get run de-duplication, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: SpecterOps/Nemesis.github/workflows/docker-build-titus.ymlLicense BSD-3-ClauseView source

What it does

This is the Build and Publish the titus-scanner Docker Image workflow from the SpecterOps/Nemesis repository, a real project running GitHub Actions. It is shown here with attribution under its BSD-3-Clause license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Build and Publish the titus-scanner Docker Image

on:
  workflow_dispatch: # For manual triggering
  push:
    branches: [ "main" ]
    # Trigger on changes to titus-scanner files
    paths:
      - 'projects/titus_scanner/**'
      - '.github/workflows/docker-build-titus.yml'

env:
  REGISTRY: ghcr.io
  IMAGE_PREFIX: "specterops/nemesis"

jobs:
  build-titus-scanner:
    strategy:
      matrix:
        include:
          - name: titus-scanner
            context: .
            dockerfile: ./projects/titus_scanner/Dockerfile
            runner: ubuntu-22.04
            platform: linux/amd64
            arch: amd64
          - name: titus-scanner
            context: .
            dockerfile: ./projects/titus_scanner/Dockerfile
            runner: ubuntu-22.04-arm
            platform: linux/arm64
            arch: arm64

    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      packages: write
      actions: write  # Added for cache cleanup

    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
        with:
          driver-opts: |
            image=moby/buildkit:latest
            network=host

      - name: Log in to the Container registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for ${{ matrix.name }} image
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.name }}
          tags: |
            type=sha,format=short,suffix=-${{ matrix.arch }}
            type=ref,event=branch,suffix=-${{ matrix.arch }}
            type=ref,event=pr,suffix=-${{ matrix.arch }}
            type=raw,value=latest-${{ matrix.arch }},enable={{is_default_branch}}
            type=raw,value=cache-${{ matrix.arch }}

      - name: Build and push ${{ matrix.name }} image
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
        timeout-minutes: 30
        with:
          context: ${{ matrix.context }}
          file: ${{ matrix.dockerfile }}
          push: true
          platforms: ${{ matrix.platform }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: |
            type=gha,scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}
            ${{ matrix.arch == 'amd64' && 'type=gha,scope=titus-scanner-amd64-main' || 'type=gha,scope=titus-scanner-arm64-main' }}
          cache-to: |
            type=gha,mode=${{ matrix.arch == 'arm64' && 'min' || 'max' }},scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}

      - name: Test built image
        run: |
          docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner:${{ steps.meta.outputs.version }} --help || true

      # Simple cache cleanup - keep only the 2 most recent caches
      - name: Simple cache cleanup
        if: success()
        run: |
          gh extension install actions/gh-actions-cache || true
          gh actions-cache list --key "titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}" --sort created-at --order desc | tail -n +3 | cut -f1 | xargs -I {} gh actions-cache delete {} --confirm || true
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        continue-on-error: true

  # Create multi-arch manifest
  create-manifest:
    needs: build-titus-scanner
    runs-on: ubuntu-22.04
    permissions:
      contents: read
      packages: write

    steps:
      - name: Log in to the Container registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata for manifest
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner
          tags: |
            type=sha,format=short
            type=ref,event=branch
            type=ref,event=pr
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Create and push multi-arch manifest
        run: |
          # Create multi-arch manifests for each tag
          echo '${{ steps.meta.outputs.tags }}' | while IFS= read -r tag; do
            if [ -n "$tag" ]; then
              echo "Creating manifest for: $tag"
              docker buildx imagetools create \
                --tag "$tag" \
                "${tag}-amd64" \
                "${tag}-arm64"
            fi
          done

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: Build and Publish the titus-scanner Docker Image
 
on:
  workflow_dispatch: # For manual triggering
  push:
    branches: [ "main" ]
    # Trigger on changes to titus-scanner files
    paths:
      - 'projects/titus_scanner/**'
      - '.github/workflows/docker-build-titus.yml'
 
env:
  REGISTRY: ghcr.io
  IMAGE_PREFIX: "specterops/nemesis"
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  build-titus-scanner:
    timeout-minutes: 30
    strategy:
      matrix:
        include:
          - name: titus-scanner
            context: .
            dockerfile: ./projects/titus_scanner/Dockerfile
            runner: ubuntu-22.04
            platform: linux/amd64
            arch: amd64
          - name: titus-scanner
            context: .
            dockerfile: ./projects/titus_scanner/Dockerfile
            runner: ubuntu-22.04-arm
            platform: linux/arm64
            arch: arm64
 
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      packages: write
      actions: write  # Added for cache cleanup
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
        with:
          driver-opts: |
            image=moby/buildkit:latest
            network=host
 
      - name: Log in to the Container registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Extract metadata for ${{ matrix.name }} image
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.name }}
          tags: |
            type=sha,format=short,suffix=-${{ matrix.arch }}
            type=ref,event=branch,suffix=-${{ matrix.arch }}
            type=ref,event=pr,suffix=-${{ matrix.arch }}
            type=raw,value=latest-${{ matrix.arch }},enable={{is_default_branch}}
            type=raw,value=cache-${{ matrix.arch }}
 
      - name: Build and push ${{ matrix.name }} image
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
        timeout-minutes: 30
        with:
          context: ${{ matrix.context }}
          file: ${{ matrix.dockerfile }}
          push: true
          platforms: ${{ matrix.platform }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: |
            type=gha,scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}
            ${{ matrix.arch == 'amd64' && 'type=gha,scope=titus-scanner-amd64-main' || 'type=gha,scope=titus-scanner-arm64-main' }}
          cache-to: |
            type=gha,mode=${{ matrix.arch == 'arm64' && 'min' || 'max' }},scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}
 
      - name: Test built image
        run: |
          docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner:${{ steps.meta.outputs.version }} --help || true
 
      # Simple cache cleanup - keep only the 2 most recent caches
      - name: Simple cache cleanup
        if: success()
        run: |
          gh extension install actions/gh-actions-cache || true
          gh actions-cache list --key "titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}" --sort created-at --order desc | tail -n +3 | cut -f1 | xargs -I {} gh actions-cache delete {} --confirm || true
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        continue-on-error: true
 
  # Create multi-arch manifest
  create-manifest:
    timeout-minutes: 30
    needs: build-titus-scanner
    runs-on: latchkey-small
    permissions:
      contents: read
      packages: write
 
    steps:
      - name: Log in to the Container registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Extract metadata for manifest
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner
          tags: |
            type=sha,format=short
            type=ref,event=branch
            type=ref,event=pr
            type=raw,value=latest,enable={{is_default_branch}}
 
      - name: Create and push multi-arch manifest
        run: |
          # Create multi-arch manifests for each tag
          echo '${{ steps.meta.outputs.tags }}' | while IFS= read -r tag; do
            if [ -n "$tag" ]; then
              echo "Creating manifest for: $tag"
              docker buildx imagetools create \
                --tag "$tag" \
                "${tag}-amd64" \
                "${tag}-arm64"
            fi
          done
 

What changed

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

This workflow runs 2 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow