Skip to content

feat: optimize skip_push and skip_retag #3

feat: optimize skip_push and skip_retag

feat: optimize skip_push and skip_retag #3

name: Build docker artifacts

Check failure on line 1 in .github/workflows/build-docker-artifacts.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/build-docker-artifacts.yml

Invalid workflow file

(Line: 314, Col: 9): Unrecognized named-value: 'matrix'. Located at position 1 within expression: matrix.flavor.test_images.enabled == true, (Line: 385, Col: 9): Unrecognized named-value: 'matrix'. Located at position 43 within expression: always() && !cancelled() && !failure() && matrix.flavor.skip_retag != true
on:
workflow_call:
inputs:
builds:
type: string
required: false
default: ""
description: "Comma separated list of build keys. Leave empty to run for all."
push_to:
type: string
required: false
default: ""
description: "Comma separated list of push keys. Leave empty to trigger for all."
branch:
type: string
required: false
skip_push:
description: "Skip retagging and pushing the built images to customer registries"
type: boolean
required: false
default: false
fail_fast:
type: boolean
required: false
default: true
scan_high_severity:
description: "Include high severity"
type: boolean
required: false
default: true
runs_on:
type: string
required: false
default: "ubuntu-22.04"
secrets:
DATAVISYN_BOT_REPO_TOKEN:
required: false
CHECKOUT_TOKEN:
required: false
description: "Token to use for the checkout actions to access private repositories"
concurrency:
group: "${{ github.workflow }}-${{ github.ref || github.head_ref }}-${{ inputs.branch }}-${{ inputs.builds }}-${{ inputs.push_to }}"
cancel-in-progress: true
env:
WORKFLOW_BRANCH: "main"
DATAVISYN_PYTHON_BASE_IMAGE: "188237246440.dkr.ecr.eu-central-1.amazonaws.com/datavisyn/base/python:main"
DATAVISYN_NGINX_BASE_IMAGE: "188237246440.dkr.ecr.eu-central-1.amazonaws.com/datavisyn/base/nginx:main"
permissions:
contents: read
id-token: write
jobs:
get-flavors:
name: Get flavors from config.json
outputs:
result: ${{ steps.get-flavors.outputs.result }}
# Do not run this on self-hosted, as it is faster and shouldn't be blocking anything
# runs-on: ${{ inputs.runs_on || 'ubuntu-22.04' }}
runs-on: "ubuntu-22.04"
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || github.sha }}
token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }}
- name: Checkout github-workflows repository
uses: actions/checkout@v5
with:
repository: datavisyn/github-workflows
ref: ${{ env.WORKFLOW_BRANCH }}
path: ./tmp/github-workflows
- name: Validate ./deploy/build/config.json
shell: bash
run: |
# Validate the config with the schema
python -m venv .venv
source .venv/bin/activate
pip install jsonschema
jsonschema -i ./deploy/build/config.json ./tmp/github-workflows/.github/workflows/build-docker-artifacts-config.schema.json
deactivate
rm -rf .venv
- name: Get all flavors and components from ./deploy/build/config.json
uses: actions/github-script@v7
id: get-flavors
with:
script: |
const fs = require('fs');
const path = require('path');
const config = require('./deploy/build/config.json');
const buildTime = new Date().toISOString().replace(/:/g, '').replace(/\..+/, 'Z');
const imageTagBranchName = "${{ github.ref }}".replace('refs/heads/', '').replace('refs/tags/', '').replace(/[^a-zA-Z0-9._-]/g, '-');
const imageTag = `tagged-${imageTagBranchName}-${buildTime}`;
const builds = process.env.BUILDS ? process.env.BUILDS.split(',') : Object.keys(config.build);
const push_to = process.env.PUSH_TO ? process.env.PUSH_TO.split(',') : Object.keys(config.push || {});
const skip_push = process.env.SKIP_PUSH === 'true';
const flavors = builds.map(id => [id, config.build[id]]).filter(([id, flavor]) => flavor.skip !== true).map(([id, flavor]) => {
const hooksDirectory = path.join('./deploy/build', flavor.directory, 'hooks');
const testImagesHookScript = path.join(hooksDirectory, 'test-images.sh');
const testImagesHookReport = path.join(hooksDirectory, 'test-images-report');
const testImageEnabled = fs.existsSync(testImagesHookScript);
return {
...flavor,
id,
// We can skip the push only if we don't want to test the image below
skip_push: skip_push && !testImageEnabled,
skip_retag: skip_push,
// Add metadata to the flavor object (will be used as matrix input)
build_time: buildTime,
image_tag: imageTag,
image_tag_branch_name: imageTagBranchName,
ecr_respositories: flavor.components.map(component => component.ecr_repository),
test_images: {
enabled: testImageEnabled,
script_path: testImagesHookScript,
report_path: testImagesHookReport,
},
components: flavor.components.map(component => {
// Format build arguments as build-args string
const formattedBuildArgs = component.build_args ?
Object.entries(component.build_args).map(([key, value]) => `${key}=${value}`).join('\n') : '';
return {
...component,
// We can skip the push only if we don't want to test the image below
skip_push: skip_push && !testImageEnabled,
skip_retag: skip_push,
// Add metadata to the component object (will be used as matrix input),
flavor,
flavor_id: id,
flavor_directory: `./deploy/build/${flavor.directory}`,
build_time: buildTime,
image_tag: imageTag,
image_ref: `${{ vars.DV_AWS_ECR_REGISTRY }}/${component.ecr_repository}:${imageTag}`,
image_tag_branch_name: imageTagBranchName,
formatted_build_args: formattedBuildArgs,
};
}),
};
});
const flattenedComponents = flavors.flatMap(flavor => flavor.components);
const result = {
flavors,
components: flattenedComponents,
push_to: push_to.join(','),
};
console.log(result);
return result;
env:
SKIP_PUSH: ${{ inputs.skip_push }}
BUILDS: ${{ inputs.builds }}
PUSH_TO: ${{ inputs.push_to }}
build-flavors:
name: Build ${{ matrix.component.directory }} of ${{ matrix.component.flavor.directory }} (${{ matrix.component.ecr_repository }}:${{ matrix.component.image_tag }})
needs: get-flavors
strategy:
fail-fast: ${{ inputs.fail_fast }}
matrix:
component: ${{ fromJson(needs.get-flavors.outputs.result).components }}
runs-on: ${{ vars.BUILD_DOCKER_RUNS_ON_OVERRIDE || vars.RUNS_ON_OVERRIDE || inputs.runs_on || 'ubuntu-22.04' }}
steps:
- name: View flavor and component
shell: bash
run: |
echo "Component ${{ toJson(matrix.component) }}"
- name: Remove unnecessary files
if: ${{ vars.BUILD_DOCKER_NEEDS_MORE_SPACE == 'true' }}
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
# TODO: Support arbitrary repositories, not just the current one?
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || github.sha }}
token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }}
# This is required such that yarn install can access private repositories, i.e. visyn_pro
# https://github.com/yarnpkg/yarn/issues/2614#issuecomment-2148174789
persist-credentials: false
- name: Checkout github-workflows repository
uses: actions/checkout@v5
with:
repository: datavisyn/github-workflows
ref: ${{ env.WORKFLOW_BRANCH }}
path: ./tmp/github-workflows
# This is required such that yarn install can access private repositories, i.e. visyn_pro
# https://github.com/yarnpkg/yarn/issues/2614#issuecomment-2148174789
persist-credentials: false
- name: Copy _base folder and .env
shell: bash
run: |
if [[ -d "./deploy/build/_base" ]]; then
echo "copy _base directory into flavor"
cp -r -n "./deploy/build/_base/." "${{ matrix.component.flavor_directory }}"
tree "${{ matrix.component.flavor_directory }}"
fi
if [[ -f "${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}/.env" ]]; then
echo "copy .env into repo root"
cp "${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}/.env" "./"
fi
# Required for build secrets to work: https://docs.docker.com/build/ci/github-actions/secrets/#secret-mounts
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure AWS Credentials
uses: aws-actions/[email protected]
with:
role-to-assume: ${{ vars.DV_AWS_ECR_ROLE }}
aws-region: ${{ vars.DV_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/[email protected]
- uses: ./tmp/github-workflows/.github/actions/get-branch
id: get-branch
with:
branch: ${{ inputs.branch }}
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}/Dockerfile
push: false
load: true
# Disable provenance as it creates weird multi-arch images: https://github.com/docker/build-push-action/issues/755
provenance: false
# Disable the cache to avoid outdated (base) images
no-cache: true
build-args: |
GIT_BRANCH=${{ steps.get-branch.outputs.branch }}
GIT_COMMIT_HASH=${{ steps.get-branch.outputs.commit_hash }}
DOCKERFILE_DIRECTORY=${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}
DATAVISYN_PYTHON_BASE_IMAGE=${{ env.DATAVISYN_PYTHON_BASE_IMAGE }}
DATAVISYN_NGINX_BASE_IMAGE=${{ env.DATAVISYN_NGINX_BASE_IMAGE }}
UV_HTTP_TIMEOUT=300
${{ matrix.component.formatted_build_args }}
secrets:
# Mount the token as secret mount: https://docs.docker.com/build/ci/github-actions/secrets/#secret-mounts
"github_token=${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }}"
# TODO: As soon as we only have a single tag, we can push the same image to multiple repositories: https://docs.docker.com/build/ci/github-actions/push-multi-registries/
# This will be useful for the images which don't change between flavors, e.g. the backend images
tags: |
${{ matrix.component.image_ref }}
labels: |
name=${{ matrix.component.ecr_repository }}
version=${{ matrix.component.image_tag_branch_name }}
org.opencontainers.image.description=Image for ${{ matrix.component.ecr_repository }}
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.title=${{ matrix.component.ecr_repository }}
org.opencontainers.image.version=${{ matrix.component.image_tag_branch_name }}
org.opencontainers.image.created=${{ matrix.component.build_time }}
org.opencontainers.image.revision=${{ github.sha }}
env:
# Disable the build summary for now as it leads to "Failed to export build record: .../export/rec.dockerbuild not found"
# https://github.com/docker/build-push-action/issues/1156#issuecomment-2437227730
DOCKER_BUILD_SUMMARY: false
- name: Determine trivy scan severity levels
id: set_severity
run: |
if [[ "${{ github.event.inputs.scan_high_severity }}" == "false" ]] || \
[[ "${{ vars.SCAN_HIGH_SEVERITY }}" == "false" ]] || \
[[ "${{ matrix.component.scan_high_severity }}" == "false" ]]; then
echo "severity=CRITICAL" >> "$GITHUB_OUTPUT"
else
echo "severity=HIGH,CRITICAL" >> "$GITHUB_OUTPUT"
fi
- name: Run Trivy vulnerability scanner
uses: aquasecurity/[email protected]
with:
image-ref: ${{ matrix.component.image_ref }}
# Disable scanning the current directory (defaults to .)
scan-ref: "/dev/null"
format: "table"
exit-code: "1"
ignore-unfixed: false
vuln-type: "os,library"
severity: ${{ steps.set_severity.outputs.severity }}
continue-on-error: false
- name: Push image
if: ${{ matrix.component.skip_push != true }}
# Instead of the docker/build-push-action@v6 which will rebuild the image, just push it directly
run: docker push ${{ matrix.component.image_ref }}
- name: Log out from Amazon ECR
shell: bash
run: docker logout ${{ steps.login-ecr.outputs.registry }}
test-images:
name: Test images of flavor ${{ matrix.flavor.id || 'default' }}
needs: [get-flavors, build-flavors]
if: ${{ matrix.flavor.test_images.enabled == true }}
strategy:
fail-fast: false
matrix:
flavor: ${{ fromJson(needs.get-flavors.outputs.result).flavors }}
runs-on: ${{ inputs.runs_on || 'ubuntu-22.04' }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || github.sha }}
token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }}
- name: Configure AWS Credentials
uses: aws-actions/[email protected]
with:
role-to-assume: ${{ vars.DV_AWS_ECR_ROLE }}
aws-region: ${{ vars.DV_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/[email protected]
- name: Run test-images.sh hook
shell: bash
id: test-images
run: |
test_images_hook=$(jq -r '.test_images.script_path' <<< "$FLAVOR")
test_images_report=$(jq -r '.test_images.report_path' <<< "$FLAVOR")
if [[ -f "$test_images_hook" ]]; then
# Iterate through all components and store their image ref in an environment variable
for component in $(jq -c '.components[]' <<< "$FLAVOR"); do
name=$(jq -r '.ecr_repository' <<< "$component")
image_ref=$(jq -r '.image_ref' <<< "$component")
# Replace all non-alphanumeric characters with underscores and convert to uppercase
name_upper=$(echo "${name//[^[:alnum:]]/_}" | tr '[:lower:]' '[:upper:]')
echo "Setting environment variable IMAGE_${name_upper}=${image_ref}"
export "IMAGE_${name_upper}=${image_ref}"
done;
# Create report folder to avoid any downstream Docker volume issues
# TODO: For some reason this doesn't work yet, i.e. if a docker-compose script mounts a volume here, nothing shows up...
mkdir -p "$test_images_report"
chmod 777 "$test_images_report"
echo "test_images_report=${test_images_report}" >> "$GITHUB_OUTPUT"
echo "Run $test_images_hook"
chmod +x "$test_images_hook"
bash "$test_images_hook"
else
echo "No $test_images_hook found, skipping tests."
fi
env:
FLAVOR: ${{ toJSON(matrix.flavor) }}
- name: Upload test-images-report
uses: actions/upload-artifact@v4
if: ${{ steps.test-images.outputs.test_images_report }}
with:
name: "test-images-report-${{ matrix.flavor.id || 'default' }}"
path: ${{ steps.test-images.outputs.test_images_report }}
- name: Log out from Amazon ECR
shell: bash
run: docker logout ${{ steps.login-ecr.outputs.registry }}
retag-images:
name: Retag images of flavor ${{ matrix.flavor.id || 'default' }}
needs: [get-flavors, test-images]
# As test-images may be skipped, we need this if to ensure this job runs: https://github.com/actions/runner/issues/491#issuecomment-1507495166
if: ${{ always() && !cancelled() && !failure() && matrix.flavor.skip_retag != true }}
strategy:
fail-fast: false
matrix:
flavor: ${{ fromJson(needs.get-flavors.outputs.result).flavors }}
# Do not run this on self-hosted, as it is faster and shouldn't be blocking anything
# runs-on: ${{ inputs.runs_on || 'ubuntu-22.04' }}
runs-on: "ubuntu-22.04"
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ inputs.branch || github.sha }}
token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }}
- name: Checkout github-workflows repository
uses: actions/checkout@v5
with:
repository: datavisyn/github-workflows
ref: ${{ env.WORKFLOW_BRANCH }}
path: ./tmp/github-workflows
- name: Configure AWS Credentials
uses: aws-actions/[email protected]
with:
role-to-assume: ${{ vars.DV_AWS_ECR_ROLE }}
aws-region: ${{ vars.DV_AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/[email protected]
- name: Retag images
shell: bash
run: |
image_tag="${{ matrix.flavor.image_tag }}"
image_tag_branch_name="${{ matrix.flavor.image_tag_branch_name }}"
echo "image_tag=$image_tag"
echo "image_tag_branch_name=$image_tag_branch_name"
for repository_name in $(jq -r '.ecr_respositories[]' <<< "$FLAVOR"); do
IMAGE_META=$(aws ecr describe-images --repository-name "$repository_name" --image-ids imageTag="$image_tag" --output json | jq --arg var "${image_tag_branch_name}" '.imageDetails[0].imageTags | index( $var )')
if [[ -z "${IMAGE_META}" || ${IMAGE_META} == "null" ]]; then
MANIFEST=$(aws ecr batch-get-image --repository-name "$repository_name" --image-ids imageTag="$image_tag" --output json | jq --raw-output --join-output '.images[0].imageManifest')
aws ecr put-image --repository-name "$repository_name" --image-tag "$image_tag_branch_name" --image-manifest "$MANIFEST"
else
echo "image already tagged!"
fi
done;
env:
FLAVOR: ${{ toJSON(matrix.flavor) }}
- name: Log out from Amazon ECR
shell: bash
run: docker logout ${{ steps.login-ecr.outputs.registry }}
push-to-repositories:
name: Push images to push targets
# if? When should we do this? Always? Only for certain branches? If so, how should we define that, in the config.json?
if: ${{ fromJson(needs.get-flavors.outputs.result).push_to != '' }}
needs: [retag-images, get-flavors]
uses: datavisyn/github-workflows/.github/workflows/build-docker-artifacts-trigger-push.yml@main
secrets: inherit
with:
push_to: ${{ fromJson(needs.get-flavors.outputs.result).push_to }}
branch: ${{ inputs.branch || github.sha }}
# Do not run this on self-hosted, as it is faster and shouldn't be blocking anything
# runs_on: ${{ inputs.runs_on || 'ubuntu-22.04' }}