--- # This action is centrally managed in https://github.com//.github/ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. # This workflow is intended to work with all our organization Docker projects. A readme named `DOCKER_README.md` # will be used to update the description on Docker hub. # custom comments in dockerfiles: # `# platforms: ` # Comma separated list of platforms, i.e. `# platforms: linux/386,linux/amd64`. Docker platforms can alternatively # be listed in a file named `.docker_platforms`. # `# platforms_pr: ` # Comma separated list of platforms to run for PR events, i.e. `# platforms_pr: linux/amd64`. This will take # precedence over the `# platforms: ` directive. # `# artifacts: ` # `true` to build in two steps, stopping at `artifacts` build stage and extracting the image from there to the # GitHub runner. name: CI Docker on: pull_request: branches: [master, nightly] types: [opened, synchronize, reopened] push: branches: [master, nightly] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check_dockerfiles: name: Check Dockerfiles runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Find dockerfiles id: find run: | dockerfiles=$(find . -type f -iname "Dockerfile" -o -iname "*.dockerfile") echo "found dockerfiles: ${dockerfiles}" # do not quote to keep this as a single line echo dockerfiles=${dockerfiles} >> $GITHUB_OUTPUT MATRIX_COMBINATIONS="" for FILE in ${dockerfiles}; do # extract tag from file name tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(Dockerfile)/None/gm') if [[ $tag == "None" ]]; then MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\"}," else tag=$(echo $FILE | sed -r -z -e 's/(\.\/)*.*\/(.+)(\.dockerfile)/-\2/gm') MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS {\"dockerfile\": \"$FILE\", \"tag\": \"$tag\"}," fi done # removes the last character (i.e. comma) MATRIX_COMBINATIONS=${MATRIX_COMBINATIONS::-1} # setup matrix for later jobs matrix=$(( echo "{ \"include\": [$MATRIX_COMBINATIONS] }" ) | jq -c .) echo $matrix echo $matrix | jq . echo "matrix=$matrix" >> $GITHUB_OUTPUT outputs: dockerfiles: ${{ steps.find.outputs.dockerfiles }} matrix: ${{ steps.find.outputs.matrix }} check_changelog: name: Check Changelog needs: [check_dockerfiles] if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} runs-on: ubuntu-latest steps: - name: Checkout if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} uses: actions/checkout@v4 - name: Verify Changelog id: verify_changelog if: ${{ github.ref == 'refs/heads/master' || github.base_ref == 'master' }} # base_ref for pull request check, ref for push uses: LizardByte/.github/actions/verify_changelog@master with: token: ${{ secrets.GITHUB_TOKEN }} outputs: next_version: ${{ steps.verify_changelog.outputs.changelog_parser_version }} next_version_bare: ${{ steps.verify_changelog.outputs.changelog_parser_version_bare }} last_version: ${{ steps.verify_changelog.outputs.latest_release_tag_name }} release_body: ${{ steps.verify_changelog.outputs.changelog_parser_description }} setup_release: name: Setup Release needs: check_changelog runs-on: ubuntu-latest steps: - name: Set release details id: release_details env: RELEASE_BODY: ${{ needs.check_changelog.outputs.release_body }} run: | # determine to create a release or not if [[ $GITHUB_EVENT_NAME == "push" ]]; then RELEASE=true else RELEASE=false fi # set the release tag COMMIT=${{ github.sha }} if [[ $GITHUB_REF == refs/heads/master ]]; then TAG="${{ needs.check_changelog.outputs.next_version }}" RELEASE_NAME="${{ needs.check_changelog.outputs.next_version }}" RELEASE_BODY="$RELEASE_BODY" PRE_RELEASE="false" elif [[ $GITHUB_REF == refs/heads/nightly ]]; then TAG="nightly-dev" RELEASE_NAME="nightly" RELEASE_BODY="automated nightly release - $(date -u +'%Y-%m-%dT%H:%M:%SZ') - ${COMMIT}" PRE_RELEASE="true" fi echo "create_release=${RELEASE}" >> $GITHUB_OUTPUT echo "release_tag=${TAG}" >> $GITHUB_OUTPUT echo "release_commit=${COMMIT}" >> $GITHUB_OUTPUT echo "release_name=${RELEASE_NAME}" >> $GITHUB_OUTPUT echo "pre_release=${PRE_RELEASE}" >> $GITHUB_OUTPUT # this is stupid but works for multiline strings echo "RELEASE_BODY<> $GITHUB_ENV echo "$RELEASE_BODY" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV outputs: create_release: ${{ steps.release_details.outputs.create_release }} release_tag: ${{ steps.release_details.outputs.release_tag }} release_commit: ${{ steps.release_details.outputs.release_commit }} release_name: ${{ steps.release_details.outputs.release_name }} release_body: ${{ env.RELEASE_BODY }} pre_release: ${{ steps.release_details.outputs.pre_release }} lint_dockerfile: needs: [check_dockerfiles] if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} name: Lint Dockerfile${{ matrix.tag }} steps: - name: Checkout uses: actions/checkout@v4 - name: Hadolint id: hadolint uses: hadolint/hadolint-action@v3.1.0 with: dockerfile: ${{ matrix.dockerfile }} ignore: DL3008,DL3013,DL3016,DL3018,DL3028,DL3059 output-file: ./hadolint.log verbose: true - name: Log if: failure() run: | echo "Hadolint outcome: ${{ steps.hadolint.outcome }}" >> $GITHUB_STEP_SUMMARY cat "./hadolint.log" >> $GITHUB_STEP_SUMMARY docker: needs: [check_dockerfiles, check_changelog, setup_release] if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} runs-on: ubuntu-latest permissions: packages: write contents: write strategy: fail-fast: false matrix: ${{ fromJson(needs.check_dockerfiles.outputs.matrix) }} name: Docker${{ matrix.tag }} steps: - name: Maximize build space uses: easimon/maximize-build-space@v8 with: root-reserve-mb: 30720 # https://github.com/easimon/maximize-build-space#caveats remove-dotnet: 'true' remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' remove-docker-images: 'true' - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Prepare id: prepare env: NV: ${{ needs.check_changelog.outputs.next_version }} run: | # get branch name BRANCH=${GITHUB_HEAD_REF} RELEASE=false if [ -z "$BRANCH" ]; then echo "This is a PUSH event" BRANCH=${{ github.ref_name }} COMMIT=${{ github.sha }} CLONE_URL=${{ github.event.repository.clone_url }} if [[ $BRANCH == "master" ]]; then RELEASE=true fi else echo "This is a PULL REQUEST event" COMMIT=${{ github.event.pull_request.head.sha }} CLONE_URL=${{ github.event.pull_request.head.repo.clone_url }} fi # determine to push image to dockerhub and ghcr or not if [[ $GITHUB_EVENT_NAME == "push" ]]; then PUSH=true else PUSH=false fi # setup the tags REPOSITORY=${{ github.repository }} BASE_TAG=$(echo $REPOSITORY | tr '[:upper:]' '[:lower:]') TAGS="${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${COMMIT:0:7}${{ matrix.tag }}" if [[ $GITHUB_REF == refs/heads/master ]]; then TAGS="${TAGS},${BASE_TAG}:latest${{ matrix.tag }},ghcr.io/${BASE_TAG}:latest${{ matrix.tag }}" TAGS="${TAGS},${BASE_TAG}:master${{ matrix.tag }},ghcr.io/${BASE_TAG}:master${{ matrix.tag }}" elif [[ $GITHUB_REF == refs/heads/nightly ]]; then TAGS="${TAGS},${BASE_TAG}:nightly${{ matrix.tag }},ghcr.io/${BASE_TAG}:nightly${{ matrix.tag }}" else TAGS="${TAGS},${BASE_TAG}:test${{ matrix.tag }},ghcr.io/${BASE_TAG}:test${{ matrix.tag }}" fi if [[ ${NV} != "" ]]; then TAGS="${TAGS},${BASE_TAG}:${NV}${{ matrix.tag }},ghcr.io/${BASE_TAG}:${NV}${{ matrix.tag }}" fi # parse custom directives out of dockerfile # try to get the platforms from the dockerfile custom directive, i.e. `# platforms: xxx,yyy` # directives for PR event, i.e. not push event if [[ ${PUSH} == "false" ]]; then while read -r line; do if [[ $line == "# platforms_pr: "* && $PLATFORMS == "" ]]; then # echo the line and use `sed` to remove the custom directive PLATFORMS=$(echo -e "$line" | sed 's/# platforms_pr: //') elif [[ $PLATFORMS != "" ]]; then # break while loop once all custom "PR" event directives are found break fi done <"${{ matrix.dockerfile }}" fi # directives for all events... above directives will not be parsed if they were already found while read -r line; do if [[ $line == "# platforms: "* && $PLATFORMS == "" ]]; then # echo the line and use `sed` to remove the custom directive PLATFORMS=$(echo -e "$line" | sed 's/# platforms: //') elif [[ $line == "# artifacts: "* && $ARTIFACTS == "" ]]; then # echo the line and use `sed` to remove the custom directive ARTIFACTS=$(echo -e "$line" | sed 's/# artifacts: //') elif [[ $line == "# no-cache-filters: "* && $NO_CACHE_FILTERS == "" ]]; then # echo the line and use `sed` to remove the custom directive NO_CACHE_FILTERS=$(echo -e "$line" | sed 's/# no-cache-filters: //') elif [[ $PLATFORMS != "" && $ARTIFACTS != "" && $NO_CACHE_FILTERS != "" ]]; then # break while loop once all custom directives are found break fi done <"${{ matrix.dockerfile }}" # if PLATFORMS is blank, fall back to the legacy method of reading from the `.docker_platforms` file if [[ $PLATFORMS == "" ]]; then # read the platforms from `.docker_platforms` PLATFORMS=$(<.docker_platforms) fi # if PLATFORMS is still blank, fall back to `linux/amd64` if [[ $PLATFORMS == "" ]]; then PLATFORMS="linux/amd64" fi echo "branch=${BRANCH}" >> $GITHUB_OUTPUT echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT echo "commit=${COMMIT}" >> $GITHUB_OUTPUT echo "clone_url=${CLONE_URL}" >> $GITHUB_OUTPUT echo "release=${RELEASE}" >> $GITHUB_OUTPUT echo "artifacts=${ARTIFACTS}" >> $GITHUB_OUTPUT echo "no_cache_filters=${NO_CACHE_FILTERS}" >> $GITHUB_OUTPUT echo "platforms=${PLATFORMS}" >> $GITHUB_OUTPUT echo "push=${PUSH}" >> $GITHUB_OUTPUT echo "tags=${TAGS}" >> $GITHUB_OUTPUT - name: Set Up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 id: buildx - name: Cache Docker Layers uses: actions/cache@v3 with: path: /tmp/.buildx-cache key: Docker-buildx${{ matrix.tag }}-${{ github.sha }} restore-keys: | Docker-buildx${{ matrix.tag }}- - name: Log in to Docker Hub if: ${{ steps.prepare.outputs.push == 'true' }} # PRs do not have access to secrets uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Log in to the Container registry if: ${{ steps.prepare.outputs.push == 'true' }} # PRs do not have access to secrets uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.GH_BOT_NAME }} password: ${{ secrets.GH_BOT_TOKEN }} - name: Build artifacts if: ${{ steps.prepare.outputs.artifacts == 'true' }} id: build_artifacts uses: docker/build-push-action@v5 with: context: ./ file: ${{ matrix.dockerfile }} target: artifacts outputs: type=local,dest=artifacts push: false platforms: ${{ steps.prepare.outputs.platforms }} build-args: | BRANCH=${{ steps.prepare.outputs.branch }} BUILD_DATE=${{ steps.prepare.outputs.build_date }} BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }} COMMIT=${{ steps.prepare.outputs.commit }} CLONE_URL=${{ steps.prepare.outputs.clone_url }} RELEASE=${{ steps.prepare.outputs.release }} tags: ${{ steps.prepare.outputs.tags }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }} - name: Build and push id: build uses: docker/build-push-action@v5 with: context: ./ file: ${{ matrix.dockerfile }} push: ${{ steps.prepare.outputs.push }} platforms: ${{ steps.prepare.outputs.platforms }} build-args: | BRANCH=${{ steps.prepare.outputs.branch }} BUILD_DATE=${{ steps.prepare.outputs.build_date }} BUILD_VERSION=${{ needs.check_changelog.outputs.next_version }} COMMIT=${{ steps.prepare.outputs.commit }} CLONE_URL=${{ steps.prepare.outputs.clone_url }} RELEASE=${{ steps.prepare.outputs.release }} tags: ${{ steps.prepare.outputs.tags }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache no-cache-filters: ${{ steps.prepare.outputs.no_cache_filters }} - name: Arrange Artifacts if: ${{ steps.prepare.outputs.artifacts == 'true' }} working-directory: artifacts run: | # artifacts will be in sub directories named after the docker target platform, e.g. `linux_amd64` # so move files to the artifacts directory # https://unix.stackexchange.com/a/52816 find ./ -type f -exec mv -t ./ -n '{}' + # remove provenance file rm -f ./provenance.json - name: Upload Artifacts if: ${{ steps.prepare.outputs.artifacts == 'true' }} uses: actions/upload-artifact@v4 with: name: Docker${{ matrix.tag }} path: artifacts/ - name: Create/Update GitHub Release if: ${{ needs.setup_release.outputs.create_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} uses: ncipollo/release-action@v1 with: name: ${{ needs.setup_release.outputs.release_name }} tag: ${{ needs.setup_release.outputs.release_tag }} commit: ${{ needs.setup_release.outputs.release_commit }} artifacts: "*artifacts/*" token: ${{ secrets.GH_BOT_TOKEN }} allowUpdates: true body: ${{ needs.setup_release.outputs.release_body }} discussionCategory: announcements prerelease: ${{ needs.setup_release.outputs.pre_release }} - name: Update Docker Hub Description if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} uses: peter-evans/dockerhub-description@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_PASSWORD }} # token is not currently supported repository: ${{ env.BASE_TAG }} short-description: ${{ github.event.repository.description }} readme-filepath: ./DOCKER_README.md