diff --git a/.github/actions/build-release-candidate/action.yml b/.github/actions/build-release-candidate/action.yml new file mode 100644 index 000000000..4c888d25b --- /dev/null +++ b/.github/actions/build-release-candidate/action.yml @@ -0,0 +1,52 @@ +name: 'Build release candidates' +description: 'Builds, caches, and loads the docker image' +inputs: + app_name: + description: 'Name of application' +outputs: + image_cache_key: + description: "Cache key for the .tar of the docker image" + value: ${{ steps.create-image-identifier.outputs.image }} + +runs: + using: "composite" + steps: + + - name: Create image identifier + id: create-image-identifier + run: | + IMAGE_NAME=$(make APP_NAME=${{ inputs.app_name }} release-image-name) + IMAGE_TAG=$(make release-image-tag) + echo "image=$IMAGE_NAME:$IMAGE_TAG" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Retrieve the image from cache + id: retrieve-image-from-cache + uses: actions/cache@v4 + with: + path: /tmp/docker-image.tar + key: ${{ steps.create-image-identifier.outputs.image }} + + - name: Build and tag Docker image + if: steps.retrieve-image-from-cache.outputs.cache-hit != 'true' + run: | + make APP_NAME=${{ inputs.app_name }} release-build + shell: bash + + - name: Save Docker image + if: steps.retrieve-image-from-cache.outputs.cache-hit != 'true' + run: | + docker save ${{ steps.create-image-identifier.outputs.image }} > /tmp/docker-image.tar + shell: bash + + - name: Cache Docker image + if: steps.retrieve-image-from-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: /tmp/docker-image.tar + key: ${{ steps.create-image-identifier.outputs.image }} + + - name: Load the Docker image + run: | + docker load < /tmp/docker-image.tar + shell: bash diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 9ba64ff6b..631975c80 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -15,7 +15,7 @@ on: outputs: commit_hash: description: The SHA that was built - value: ${{ jobs.get-commit-hash.outputs.commit_hash }} + value: ${{ jobs.check-image-already-published.outputs.commit_hash }} workflow_dispatch: inputs: app_name: @@ -28,41 +28,69 @@ on: type: string jobs: - get-commit-hash: - name: Get commit hash + check-image-already-published: + name: Check whether the image is already published runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + outputs: commit_hash: ${{ steps.get-commit-hash.outputs.commit_hash }} + is_image_published: ${{ steps.check-image-published.outputs.is_image_published }} steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} - - name: Get commit hash - id: get-commit-hash + - id: get-commit-hash run: | # HEAD should be the same as inputs.ref since we checked out inputs.ref COMMIT_HASH=$(git rev-parse HEAD) echo "Commit hash: $COMMIT_HASH" echo "commit_hash=$COMMIT_HASH" >> "$GITHUB_OUTPUT" + - name: Set up Terraform + uses: ./.github/actions/setup-terraform + + - name: Configure AWS credentials + uses: ./.github/actions/configure-aws-credentials + with: + app_name: ${{ inputs.app_name }} + environment: shared + + - name: Check if image is already published + id: check-image-published + run: | + is_image_published=$(./bin/is-image-published "${{ inputs.app_name }}" "${{ steps.get-commit-hash.outputs.commit_hash }}") + echo "Is image published: $is_image_published" + echo "is_image_published=$is_image_published" >> "$GITHUB_OUTPUT" + build-and-publish: - name: Build and publish runs-on: ubuntu-latest - needs: get-commit-hash - concurrency: build-and-publish-${{ inputs.app_name }}-${{ needs.get-commit-hash.outputs.commit_hash }} + needs: check-image-already-published + if: ${{ needs.check-image-already-published.outputs.is_image_published != 'true' }} permissions: contents: read id-token: write + + concurrency: + group: build-${{ inputs.app_name }}-${{ needs.check-image-already-published.outputs.commit_hash }} + cancel-in-progress: false steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} + - id: build-release-candidate + uses: ./.github/actions/build-release-candidate + with: + app_name: ${{ inputs.app_name }} + - name: Set up Terraform uses: ./.github/actions/setup-terraform @@ -72,17 +100,5 @@ jobs: app_name: ${{ inputs.app_name }} environment: shared - - name: Check if image is already published - id: check-image-published - run: | - is_image_published=$(./bin/is-image-published "${{ inputs.app_name }}" "${{ needs.get-commit-hash.outputs.commit_hash }}") - echo "Is image published: $is_image_published" - echo "is_image_published=$is_image_published" >> "$GITHUB_OUTPUT" - - - name: Build release - if: steps.check-image-published.outputs.is_image_published == 'false' - run: make APP_NAME=${{ inputs.app_name }} release-build - - name: Publish release - if: steps.check-image-published.outputs.is_image_published == 'false' run: make APP_NAME=${{ inputs.app_name }} release-publish diff --git a/.github/workflows/vulnerability-scans.yml b/.github/workflows/vulnerability-scans.yml index fb4a838b0..6090539c7 100644 --- a/.github/workflows/vulnerability-scans.yml +++ b/.github/workflows/vulnerability-scans.yml @@ -1,8 +1,9 @@ -# GitHub Actions CI workflow that runs vulnerability scans on the application's Docker image -# to ensure images built are secure before they are deployed. +# GitHub Actions CI workflow that runs vulnerability scans on the application's +# Dockerfile or Docker image to ensure images built are secure before they are deployed. + +# The docker image is built once and cached, with that image used by the jobs that +# require access to the image. -# NOTE: The workflow isn't able to pass the docker image between jobs, so each builds the image. -# A future PR will pass the image between the scans to reduce overhead and increase speed name: Vulnerability Scans on: @@ -41,12 +42,56 @@ jobs: if: always() # Runs even if there is a failure run: cat hadolint-results.txt >> "$GITHUB_STEP_SUMMARY" + get-commit-hash: + runs-on: ubuntu-latest + outputs: + commit_hash: ${{ steps.get-commit-hash.outputs.image_tag }} + + steps: + - uses: actions/checkout@v4 + + - id: get-commit-hash + run: | + IMAGE_TAG=$(make release-image-tag) + echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT" + + build: + runs-on: ubuntu-latest + needs: get-commit-hash + outputs: + image: ${{ steps.build-release-candidate.outputs.image_cache_key }} + + # to support the build happening only once for a given version, we disable concurrent execution + concurrency: + group: build-${{ inputs.app_name }}-${{ needs.get-commit-hash.outputs.commit_hash }} + cancel-in-progress: false + + steps: + - uses: actions/checkout@v4 + + - id: build-release-candidate + uses: ./.github/actions/build-release-candidate + with: + app_name: ${{ inputs.app_name }} + trivy-scan: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v4 + - name: Restore cached Docker image + uses: actions/cache/restore@v4 + with: + path: /tmp/docker-image.tar + key: ${{ needs.build.outputs.image }} + fail-on-cache-miss: true + + - name: Load cached Docker image + run: | + docker load < /tmp/docker-image.tar + - uses: ./.github/actions/first-file id: trivy-ignore with: @@ -59,19 +104,11 @@ jobs: with: files: ${{ inputs.app_name }}/trivy-secret.yaml trivy-secret.yaml - - name: Build and tag Docker image for scanning - id: build-image - run: | - make APP_NAME=${{ inputs.app_name }} release-build - IMAGE_NAME=$(make APP_NAME=${{ inputs.app_name }} release-image-name) - IMAGE_TAG=$(make release-image-tag) - echo "image=$IMAGE_NAME:$IMAGE_TAG" >> "$GITHUB_OUTPUT" - - name: Run Trivy vulnerability scan uses: aquasecurity/trivy-action@master with: scan-type: image - image-ref: ${{ steps.build-image.outputs.image }} + image-ref: ${{ needs.build.outputs.image }} format: table exit-code: 1 ignore-unfixed: true @@ -88,10 +125,22 @@ jobs: anchore-scan: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v4 + - name: Restore cached Docker image + uses: actions/cache/restore@v4 + with: + path: /tmp/docker-image.tar + key: ${{ needs.build.outputs.image }} + fail-on-cache-miss: true + + - name: Load cached Docker image + run: | + docker load < /tmp/docker-image.tar + - uses: ./.github/actions/first-file id: grype-config with: @@ -99,18 +148,10 @@ jobs: ${{ inputs.app_name }}/.grype.yml .grype.yml - - name: Build and tag Docker image for scanning - id: build-image - run: | - make APP_NAME=${{ inputs.app_name }} release-build - IMAGE_NAME=$(make APP_NAME=${{ inputs.app_name }} release-image-name) - IMAGE_TAG=$(make release-image-tag) - echo "image=$IMAGE_NAME:$IMAGE_TAG" >> "$GITHUB_OUTPUT" - - name: Run Anchore vulnerability scan uses: anchore/scan-action@v3 with: - image: ${{ steps.build-image.outputs.image }} + image: ${{ needs.build.outputs.image }} output-format: table env: GRYPE_CONFIG: ${{ steps.grype-config.outputs.found_file }} @@ -121,10 +162,22 @@ jobs: dockle-scan: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v4 + - name: Restore cached Docker image + uses: actions/cache/restore@v4 + with: + path: /tmp/docker-image.tar + key: ${{ needs.build.outputs.image }} + fail-on-cache-miss: true + + - name: Load cached Docker image + run: | + docker load < /tmp/docker-image.tar + - uses: ./.github/actions/first-file id: dockle-config with: @@ -132,14 +185,6 @@ jobs: ${{ inputs.app_name }}/.dockleconfig .dockleconfig - - name: Build and tag Docker image for scanning - id: build-image - run: | - make APP_NAME=${{ inputs.app_name }} release-build - IMAGE_NAME=$(make APP_NAME=${{ inputs.app_name }} release-image-name) - IMAGE_TAG=$(make release-image-tag) - echo "image=$IMAGE_NAME:$IMAGE_TAG" >> "$GITHUB_OUTPUT" - # Dockle doesn't allow you to have an ignore file for the DOCKLE_ACCEPT_FILES # variable, this will save the variable in this file to env for Dockle - name: Set any acceptable Dockle files @@ -151,7 +196,7 @@ jobs: - name: Run Dockle container linter uses: erzz/dockle-action@v1.3.1 with: - image: ${{ steps.build-image.outputs.image }} + image: ${{ needs.build.outputs.image }} exit-code: "1" failure-threshold: WARN accept-filenames: ${{ env.DOCKLE_ACCEPT_FILES }}