feat(github): add github integration (#368) #777
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
# This action is centrally managed in https://github.com/<organization>/.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] | |
types: [opened, synchronize, reopened] | |
push: | |
branches: [master] | |
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 | |
- name: Find dotnet solution file | |
id: find_dotnet | |
run: | | |
solution=$(find . -maxdepth 1 -type f -iname "*.sln") | |
echo "found solution: ${solution}" | |
# do not quote to keep this as a single line | |
echo solution=${solution} >> $GITHUB_OUTPUT | |
if [[ $solution != "" ]]; then | |
echo "dotnet=true" >> $GITHUB_OUTPUT | |
else | |
echo "dotnet=false" >> $GITHUB_OUTPUT | |
fi | |
outputs: | |
dockerfiles: ${{ steps.find.outputs.dockerfiles }} | |
matrix: ${{ steps.find.outputs.matrix }} | |
dotnet: ${{ steps.find_dotnet.outputs.dotnet }} | |
solution: ${{ steps.find_dotnet.outputs.solution }} | |
setup_release: | |
if: ${{ needs.check_dockerfiles.outputs.dockerfiles }} | |
name: Setup Release | |
needs: | |
- check_dockerfiles | |
outputs: | |
publish_release: ${{ steps.setup_release.outputs.publish_release }} | |
release_body: ${{ steps.setup_release.outputs.release_body }} | |
release_commit: ${{ steps.setup_release.outputs.release_commit }} | |
release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }} | |
release_tag: ${{ steps.setup_release.outputs.release_tag }} | |
release_version: ${{ steps.setup_release.outputs.release_version }} | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Setup Release | |
id: setup_release | |
uses: LizardByte/[email protected] | |
with: | |
dotnet: ${{ needs.check_dockerfiles.outputs.dotnet }} | |
github_token: ${{ secrets.GITHUB_TOKEN }} | |
docker: | |
needs: [check_dockerfiles, 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@v10 | |
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.setup_release.outputs.release_tag }} | |
run: | | |
# get branch name | |
BRANCH=${GITHUB_HEAD_REF} | |
RELEASE=${{ needs.setup_release.outputs.publish_release }} | |
COMMIT=${{ needs.setup_release.outputs.release_commit }} | |
if [ -z "$BRANCH" ]; then | |
echo "This is a PUSH event" | |
BRANCH=${{ github.ref_name }} | |
CLONE_URL=${{ github.event.repository.clone_url }} | |
else | |
echo "This is a PULL REQUEST event" | |
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 }}" | |
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 [[ ${RELEASE} == "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 "clone_url=${CLONE_URL}" >> $GITHUB_OUTPUT | |
echo "artifacts=${ARTIFACTS}" >> $GITHUB_OUTPUT | |
echo "no_cache_filters=${NO_CACHE_FILTERS}" >> $GITHUB_OUTPUT | |
echo "platforms=${PLATFORMS}" >> $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@v4 | |
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: ${{ needs.setup_release.outputs.publish_release == '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: ${{ needs.setup_release.outputs.publish_release == '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@v6 | |
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.setup_release.outputs.release_tag }} | |
COMMIT=${{ needs.setup_release.outputs.release_commit }} | |
CLONE_URL=${{ steps.prepare.outputs.clone_url }} | |
RELEASE=${{ needs.setup_release.outputs.publish_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@v6 | |
with: | |
context: ./ | |
file: ${{ matrix.dockerfile }} | |
push: ${{ needs.setup_release.outputs.publish_release }} | |
platforms: ${{ steps.prepare.outputs.platforms }} | |
build-args: | | |
BRANCH=${{ steps.prepare.outputs.branch }} | |
BUILD_DATE=${{ steps.prepare.outputs.build_date }} | |
BUILD_VERSION=${{ needs.setup_release.outputs.release_tag }} | |
COMMIT=${{ needs.setup_release.outputs.release_commit }} | |
CLONE_URL=${{ steps.prepare.outputs.clone_url }} | |
RELEASE=${{ needs.setup_release.outputs.publish_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.publish_release == 'true' && steps.prepare.outputs.artifacts == 'true' }} | |
uses: LizardByte/[email protected] | |
with: | |
allowUpdates: true | |
artifacts: "*artifacts/*" | |
body: ${{ needs.setup_release.outputs.release_body }} | |
generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }} | |
name: ${{ needs.setup_release.outputs.release_tag }} | |
prerelease: true | |
tag: ${{ needs.setup_release.outputs.release_tag }} | |
token: ${{ secrets.GH_BOT_TOKEN }} | |
- name: Update Docker Hub Description | |
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} | |
uses: peter-evans/dockerhub-description@v4 | |
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 |