|
| 1 | +name: Build docker artifacts |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_call: |
| 5 | + inputs: |
| 6 | + branch: |
| 7 | + type: string |
| 8 | + required: false |
| 9 | + # When using github.ref || github.head_ref, it would contain the full path, including /, which breaks the postgres hostname |
| 10 | + default: ${{ github.sha }} |
| 11 | + runs_on: |
| 12 | + type: string |
| 13 | + required: false |
| 14 | + default: "ubuntu-22.04" |
| 15 | + secrets: |
| 16 | + DATAVISYN_BOT_REPO_TOKEN: |
| 17 | + required: false |
| 18 | + CHECKOUT_TOKEN: |
| 19 | + required: false |
| 20 | + description: "Token to use for the checkout actions to access private repositories" |
| 21 | + |
| 22 | +concurrency: |
| 23 | + group: "${{ github.workflow }}-${{ github.ref || github.head_ref }}" |
| 24 | + cancel-in-progress: true |
| 25 | + |
| 26 | +env: |
| 27 | + WORKFLOW_BRANCH: "mp/build_docker" |
| 28 | + PYTHON_BASE_IMAGE: "python:3.10.8-slim-bullseye" |
| 29 | + DATAVISYN_PYTHON_BASE_IMAGE: "188237246440.dkr.ecr.eu-central-1.amazonaws.com/datavisyn/base/python:main" |
| 30 | + NODE_BASE_IMAGE: "node:20.9-bullseye" |
| 31 | + DATAVISYN_NGINX_BASE_IMAGE: "188237246440.dkr.ecr.eu-central-1.amazonaws.com/datavisyn/base/nginx:main" |
| 32 | + |
| 33 | +permissions: |
| 34 | + contents: read |
| 35 | + id-token: write |
| 36 | + |
| 37 | +jobs: |
| 38 | + get-flavors: |
| 39 | + name: Get flavors from config.json |
| 40 | + outputs: |
| 41 | + result: ${{ steps.get-flavors.outputs.result }} |
| 42 | + runs-on: ${{ inputs.runs_on || 'ubuntu-22.04' }} |
| 43 | + steps: |
| 44 | + - name: Checkout repository |
| 45 | + uses: actions/checkout@v4 |
| 46 | + with: |
| 47 | + ref: ${{ inputs.branch }} |
| 48 | + token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }} |
| 49 | + |
| 50 | + - name: Checkout github-workflows repository |
| 51 | + uses: actions/checkout@v4 |
| 52 | + with: |
| 53 | + repository: datavisyn/github-workflows |
| 54 | + ref: ${{ env.WORKFLOW_BRANCH }} |
| 55 | + path: ./tmp/github-workflows |
| 56 | + |
| 57 | + - name: Validate ./deploy/build/config.json |
| 58 | + shell: bash |
| 59 | + run: | |
| 60 | + # Validate the config with the schema |
| 61 | + python -m venv .venv |
| 62 | + source .venv/bin/activate |
| 63 | + pip install jsonschema |
| 64 | + jsonschema -i ./deploy/build/config.json ./tmp/github-workflows/.github/workflows/build-docker-artifacts-config.schema.json |
| 65 | + deactivate |
| 66 | + rm -rf .venv |
| 67 | +
|
| 68 | + - name: Get all flavors and components from ./deploy/build/config.json |
| 69 | + uses: actions/github-script@v7 |
| 70 | + id: get-flavors |
| 71 | + with: |
| 72 | + script: | |
| 73 | + const fs = require('fs'); |
| 74 | + const path = require('path'); |
| 75 | + const config = require('./deploy/build/config.json'); |
| 76 | +
|
| 77 | + const buildTime = new Date().toISOString().replace(/:/g, '').replace(/\..+/, 'Z'); |
| 78 | + const imageTagBranchName = "${{ github.ref }}".replace('refs/heads/', '').replace(/[^a-zA-Z0-9._-]/g, '-'); |
| 79 | + const imageTag = `tagged-${imageTagBranchName}-${buildTime}`; |
| 80 | +
|
| 81 | + const flavors = config.flavors.filter(flavor => flavor.skip !== true).map(flavor => { |
| 82 | + return { |
| 83 | + ...flavor, |
| 84 | + // Add metadata to the flavor object (will be used as matrix input) |
| 85 | + build_time: buildTime, |
| 86 | + image_tag: imageTag, |
| 87 | + image_tag_branch_name: imageTagBranchName, |
| 88 | + ecr_respositories: flavor.components.map(component => component.ecr_repository), |
| 89 | + components: flavor.components.map(component => { |
| 90 | + return { |
| 91 | + ...component, |
| 92 | + // Add metadata to the component object (will be used as matrix input), |
| 93 | + flavor, |
| 94 | + flavor_directory: `./deploy/build/${flavor.directory}`, |
| 95 | + build_time: buildTime, |
| 96 | + image_tag: imageTag, |
| 97 | + image_tag_branch_name: imageTagBranchName, |
| 98 | + }; |
| 99 | + }), |
| 100 | + }; |
| 101 | + }); |
| 102 | +
|
| 103 | + const flattenedComponents = flavors.flatMap(flavor => flavor.components); |
| 104 | +
|
| 105 | + const result = { |
| 106 | + flavors, |
| 107 | + components: flattenedComponents, |
| 108 | + }; |
| 109 | + console.log(result); |
| 110 | + return result; |
| 111 | +
|
| 112 | + build-flavors: |
| 113 | + name: Build ${{ matrix.component.directory }} of ${{ matrix.component.flavor.directory }} (${{ matrix.component.ecr_repository }}:${{ matrix.component.image_tag }}) |
| 114 | + needs: get-flavors |
| 115 | + strategy: |
| 116 | + fail-fast: true |
| 117 | + matrix: |
| 118 | + component: ${{ fromJson(needs.get-flavors.outputs.result).components }} |
| 119 | + runs-on: ${{ inputs.runs_on || 'ubuntu-22.04' }} |
| 120 | + steps: |
| 121 | + - name: View flavor and component |
| 122 | + shell: bash |
| 123 | + run: | |
| 124 | + echo "Component ${{ toJson(matrix.component) }}" |
| 125 | + - name: Remove unnecessary files |
| 126 | + run: | |
| 127 | + sudo rm -rf /usr/share/dotnet |
| 128 | + sudo rm -rf /usr/local/lib/android |
| 129 | + sudo rm -rf /opt/ghc |
| 130 | + # TODO: Support arbitrary repositories, not just the current one? |
| 131 | + - name: Checkout repository |
| 132 | + uses: actions/checkout@v4 |
| 133 | + with: |
| 134 | + ref: ${{ inputs.branch }} |
| 135 | + token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }} |
| 136 | + # This is required such that yarn install can access private repositories, i.e. visyn_pro |
| 137 | + # https://github.com/yarnpkg/yarn/issues/2614#issuecomment-2148174789 |
| 138 | + persist-credentials: false |
| 139 | + - name: Checkout github-workflows repository |
| 140 | + uses: actions/checkout@v4 |
| 141 | + with: |
| 142 | + repository: datavisyn/github-workflows |
| 143 | + ref: ${{ env.WORKFLOW_BRANCH }} |
| 144 | + path: ./tmp/github-workflows |
| 145 | + # This is required such that yarn install can access private repositories, i.e. visyn_pro |
| 146 | + # https://github.com/yarnpkg/yarn/issues/2614#issuecomment-2148174789 |
| 147 | + persist-credentials: false |
| 148 | + - name: Copy _base folder and .env |
| 149 | + shell: bash |
| 150 | + run: | |
| 151 | + if [[ -d "./deploy/build/_base" ]]; then |
| 152 | + echo "copy _base directory into flavor" |
| 153 | + cp -r -n "./deploy/build/_base/." "${{ matrix.component.flavor_directory }}" |
| 154 | + tree "${{ matrix.component.flavor_directory }}" |
| 155 | + fi |
| 156 | + if [[ -f "${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}/.env" ]]; then |
| 157 | + echo "copy .env into repo root" |
| 158 | + cp "${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}/.env" "./" |
| 159 | + fi |
| 160 | +
|
| 161 | + # Required for build secrets to work: https://docs.docker.com/build/ci/github-actions/secrets/#secret-mounts |
| 162 | + - name: Set up QEMU |
| 163 | + uses: docker/setup-qemu-action@v3 |
| 164 | + - name: Set up Docker Buildx |
| 165 | + uses: docker/setup-buildx-action@v3 |
| 166 | + |
| 167 | + - name: Configure AWS Credentials |
| 168 | + uses: aws-actions/[email protected] |
| 169 | + with: |
| 170 | + role-to-assume: ${{ vars.DV_AWS_ECR_ROLE }} |
| 171 | + aws-region: ${{ vars.DV_AWS_REGION }} |
| 172 | + |
| 173 | + - name: Login to Amazon ECR |
| 174 | + id: login-ecr |
| 175 | + uses: aws-actions/[email protected] |
| 176 | + |
| 177 | + - name: Build image |
| 178 | + uses: docker/build-push-action@v6 |
| 179 | + with: |
| 180 | + context: . |
| 181 | + file: ${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }}/Dockerfile |
| 182 | + push: true |
| 183 | + # Disable provenance as it creates weird multi-arch images: https://github.com/docker/build-push-action/issues/755 |
| 184 | + provenance: false |
| 185 | + build-args: | |
| 186 | + DOCKERFILE_DIRECTORY=${{ matrix.component.flavor_directory }}/${{ matrix.component.directory }} |
| 187 | + PYTHON_BASE_IMAGE=${{ env.PYTHON_BASE_IMAGE }} |
| 188 | + DATAVISYN_PYTHON_BASE_IMAGE=${{ env.DATAVISYN_PYTHON_BASE_IMAGE }} |
| 189 | + NODE_BASE_IMAGE=${{ env.NODE_BASE_IMAGE }} |
| 190 | + DATAVISYN_NGINX_BASE_IMAGE=${{ env.DATAVISYN_NGINX_BASE_IMAGE }} |
| 191 | + secrets: |
| 192 | + # Mount the token as secret mount: https://docs.docker.com/build/ci/github-actions/secrets/#secret-mounts |
| 193 | + "github_token=${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }}" |
| 194 | + # 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/ |
| 195 | + # This will be useful for the images which don't change between flavors, e.g. the backend images |
| 196 | + tags: | |
| 197 | + ${{ vars.DV_AWS_ECR_REGISTRY }}/${{ matrix.component.ecr_repository }}:${{ matrix.component.image_tag }} |
| 198 | + labels: | |
| 199 | + name=${{ matrix.component.ecr_repository }} |
| 200 | + version=${{ matrix.component.image_tag_branch_name }} |
| 201 | + org.opencontainers.image.description=Image for ${{ matrix.component.ecr_repository }} |
| 202 | + org.opencontainers.image.source=${{ github.event.repository.html_url }} |
| 203 | + org.opencontainers.image.url=${{ github.event.repository.html_url }} |
| 204 | + org.opencontainers.image.title=${{ matrix.component.ecr_repository }} |
| 205 | + org.opencontainers.image.version=${{ matrix.component.image_tag_branch_name }} |
| 206 | + org.opencontainers.image.created=${{ matrix.component.build_time }} |
| 207 | + org.opencontainers.image.revision=${{ github.sha }} |
| 208 | + env: |
| 209 | + # Disable the build summary for now as it leads to "Failed to export build record: .../export/rec.dockerbuild not found" |
| 210 | + # https://github.com/docker/build-push-action/issues/1156#issuecomment-2437227730 |
| 211 | + DOCKER_BUILD_SUMMARY: false |
| 212 | + |
| 213 | + - name: Log out from Amazon ECR |
| 214 | + shell: bash |
| 215 | + run: docker logout ${{ steps.login-ecr.outputs.registry }} |
| 216 | + |
| 217 | + - name: Scan image |
| 218 | + if: ${{ matrix.component.skip_image_scan != true }} |
| 219 | + id: get-ecr-scan-result |
| 220 | + uses: ./tmp/github-workflows/.github/actions/get-ecr-scan-result |
| 221 | + with: |
| 222 | + aws_role: ${{ vars.DV_AWS_ECR_ROLE }} |
| 223 | + aws_region: ${{ vars.DV_AWS_REGION }} |
| 224 | + ecr_registry: ${{ vars.DV_AWS_ECR_REGISTRY }} |
| 225 | + ecr_repository: ${{ matrix.component.ecr_repository }} |
| 226 | + image_tag: ${{ matrix.component.image_tag }} |
| 227 | + - name: Check scan results |
| 228 | + if: ${{ matrix.component.skip_image_scan != true }} |
| 229 | + run: | |
| 230 | + if [ "${{ steps.get-ecr-scan-result.outputs.critical }}" != "null" ] || [ "${{ steps.get-ecr-scan-result.outputs.high }}" != "null" ]; then |
| 231 | + echo "Docker image contains vulnerabilities at critical or high level" |
| 232 | + exit 1 #exit execution due to docker image vulnerabilities |
| 233 | + fi |
| 234 | +
|
| 235 | + post-build: |
| 236 | + name: Retag images of flavor ${{ matrix.flavor || 'default' }} |
| 237 | + needs: [get-flavors, build-flavors] |
| 238 | + strategy: |
| 239 | + fail-fast: false |
| 240 | + matrix: |
| 241 | + flavor: ${{ fromJson(needs.get-flavors.outputs.result).flavors }} |
| 242 | + runs-on: ${{ inputs.runs_on || 'ubuntu-22.04' }} |
| 243 | + steps: |
| 244 | + - name: Checkout repository |
| 245 | + uses: actions/checkout@v4 |
| 246 | + with: |
| 247 | + ref: ${{ inputs.branch }} |
| 248 | + token: ${{ secrets.CHECKOUT_TOKEN || github.event.repository.private == true && secrets.DATAVISYN_BOT_REPO_TOKEN || github.token }} |
| 249 | + |
| 250 | + - name: Checkout github-workflows repository |
| 251 | + uses: actions/checkout@v4 |
| 252 | + with: |
| 253 | + repository: datavisyn/github-workflows |
| 254 | + ref: ${{ env.WORKFLOW_BRANCH }} |
| 255 | + path: ./tmp/github-workflows |
| 256 | + |
| 257 | + - name: Configure AWS Credentials |
| 258 | + uses: aws-actions/[email protected] |
| 259 | + with: |
| 260 | + role-to-assume: ${{ vars.DV_AWS_ECR_ROLE }} |
| 261 | + aws-region: ${{ vars.DV_AWS_REGION }} |
| 262 | + |
| 263 | + - name: Login to Amazon ECR |
| 264 | + id: login-ecr |
| 265 | + uses: aws-actions/[email protected] |
| 266 | + |
| 267 | + - name: Retag images |
| 268 | + shell: bash |
| 269 | + run: | |
| 270 | + image_tag="${{ matrix.flavor.image_tag }}" |
| 271 | + image_tag_branch_name="${{ matrix.flavor.image_tag_branch_name }}" |
| 272 | +
|
| 273 | + echo "image_tag=$image_tag" |
| 274 | + echo "image_tag_branch_name=$image_tag_branch_name" |
| 275 | +
|
| 276 | + for repository_name in $(jq -r '.ecr_respositories[]' <<< "$FLAVOR"); do |
| 277 | + 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 )') |
| 278 | + if [[ -z "${IMAGE_META}" || ${IMAGE_META} == "null" ]]; then |
| 279 | + 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') |
| 280 | + aws ecr put-image --repository-name "$repository_name" --image-tag "$image_tag_branch_name" --image-manifest "$MANIFEST" |
| 281 | + else |
| 282 | + echo "image already tagged!" |
| 283 | + fi |
| 284 | + done; |
| 285 | + env: |
| 286 | + FLAVOR: ${{ toJSON(matrix.flavor) }} |
| 287 | + |
| 288 | + - name: Log out from Amazon ECR |
| 289 | + shell: bash |
| 290 | + run: docker logout ${{ steps.login-ecr.outputs.registry }} |
0 commit comments