docker #12
This file contains hidden or 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
| name: docker | |
| run-name: ${{ inputs.run-name }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| run-name: | |
| description: 'set run-name for workflow (multiple calls)' | |
| type: string | |
| required: false | |
| default: 'docker' | |
| runs-on: | |
| description: 'set runs-on for workflow (github or selfhosted)' | |
| type: string | |
| required: false | |
| default: 'ubuntu-22.04' | |
| build: | |
| description: 'set WORKFLOW_BUILD' | |
| required: false | |
| default: 'true' | |
| release: | |
| description: 'set WORKFLOW_GITHUB_RELEASE' | |
| required: false | |
| default: 'false' | |
| readme: | |
| description: 'set WORKFLOW_GITHUB_README' | |
| required: false | |
| default: 'false' | |
| etc: | |
| description: 'base64 encoded json string' | |
| required: false | |
| jobs: | |
| docker: | |
| runs-on: ${{ inputs.runs-on }} | |
| timeout-minutes: 1440 | |
| services: | |
| registry: | |
| image: registry:2 | |
| ports: | |
| - 5000:5000 | |
| permissions: | |
| actions: read | |
| contents: write | |
| packages: write | |
| steps: | |
| - name: init / checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | |
| with: | |
| ref: ${{ github.ref_name }} | |
| fetch-depth: 0 | |
| - name: init / setup environment | |
| uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 | |
| with: | |
| script: | | |
| const { existsSync, readFileSync } = require('node:fs'); | |
| const { resolve } = require('node:path'); | |
| const { inspect } = require('node:util'); | |
| const { Buffer } = require('node:buffer'); | |
| const inputs = `${{ toJSON(github.event.inputs) }}`; | |
| const opt = {input:{}, dot:{}}; | |
| try{ | |
| if(inputs.length > 0){ | |
| opt.input = JSON.parse(inputs); | |
| if(opt.input?.etc){ | |
| opt.input.etc = JSON.parse(Buffer.from(opt.input.etc, 'base64').toString('ascii')); | |
| } | |
| } | |
| }catch(e){ | |
| core.warning('could not parse github.event.inputs'); | |
| } | |
| try{ | |
| const path = resolve('.json'); | |
| if(existsSync(path)){ | |
| try{ | |
| opt.dot = JSON.parse(readFileSync(path).toString()); | |
| }catch(e){ | |
| throw new Error('could not parse .json'); | |
| } | |
| }else{ | |
| throw new Error('.json does not exist'); | |
| } | |
| }catch(e){ | |
| core.setFailed(e); | |
| } | |
| core.info(inspect(opt, {showHidden:false, depth:null, colors:true})); | |
| const docker = { | |
| image:{ | |
| name:opt.dot.image, | |
| arch:(opt.dot.arch || 'linux/amd64,linux/arm64'), | |
| prefix:((opt.input?.etc?.semverprefix) ? `${opt.input?.etc?.semverprefix}-` : ''), | |
| suffix:((opt.input?.etc?.semversuffix) ? `-${opt.input?.etc?.semversuffix}` : ''), | |
| description:(opt.dot?.readme?.description || ''), | |
| tags:[], | |
| }, | |
| app:{ | |
| image:opt.dot.image, | |
| name:opt.dot.name, | |
| version:(opt.input?.etc?.version || opt.dot?.semver?.version), | |
| root:opt.dot.root, | |
| UID:(opt.input?.etc?.uid || 1000), | |
| GID:(opt.input?.etc?.gid || 1000), | |
| no_cache:new Date().getTime(), | |
| }, | |
| cache:{ | |
| registry:'localhost:5000/', | |
| }, | |
| tags:[], | |
| }; | |
| docker.cache.name = `${docker.image.name}:${docker.image.prefix}buildcache${docker.image.suffix}`; | |
| docker.cache.grype = `${docker.cache.registry}${docker.image.name}:${docker.image.prefix}grype${docker.image.suffix}`; | |
| docker.app.prefix = docker.image.prefix; | |
| docker.app.suffix = docker.image.suffix; | |
| // setup tags | |
| if(!opt.dot?.semver?.disable?.rolling){ | |
| docker.image.tags.push('rolling'); | |
| } | |
| if(opt.input?.etc?.dockerfile !== 'arch.dockerfile' && opt.input?.etc?.tag){ | |
| docker.image.tags.push(`${context.sha.substring(0,7)}`); | |
| docker.image.tags.push(opt.input.etc.tag); | |
| docker.image.tags.push(`${opt.input.etc.tag}-${docker.app.version}`); | |
| docker.cache.name = `${docker.image.name}:buildcache-${opt.input.etc.tag}`; | |
| }else if(docker.app.version !== 'latest'){ | |
| const semver = docker.app.version.split('.'); | |
| docker.image.tags.push(`${context.sha.substring(0,7)}`); | |
| if(Array.isArray(semver)){ | |
| if(semver.length >= 1) docker.image.tags.push(`${semver[0]}`); | |
| if(semver.length >= 2) docker.image.tags.push(`${semver[0]}.${semver[1]}`); | |
| if(semver.length >= 3) docker.image.tags.push(`${semver[0]}.${semver[1]}.${semver[2]}`); | |
| } | |
| if(opt.dot?.semver?.stable && new RegExp(opt.dot?.semver.stable, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('stable'); | |
| if(opt.dot?.semver?.latest && new RegExp(opt.dot?.semver.latest, 'ig').test(docker.image.tags.join(','))) docker.image.tags.push('latest'); | |
| }else{ | |
| docker.image.tags.push('latest'); | |
| } | |
| for(const tag of docker.image.tags){ | |
| docker.tags.push(`${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); | |
| docker.tags.push(`ghcr.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); | |
| docker.tags.push(`quay.io/${docker.image.name}:${docker.image.prefix}${tag}${docker.image.suffix}`); | |
| } | |
| // setup build arguments | |
| if(opt.input?.etc?.build?.args){ | |
| for(const arg in opt.input.etc.build.args){ | |
| docker.app[arg] = opt.input.etc.build.args[arg]; | |
| } | |
| } | |
| if(opt.dot?.build?.args){ | |
| for(const arg in opt.dot.build.args){ | |
| docker.app[arg] = opt.dot.build.args[arg]; | |
| } | |
| } | |
| const arguments = []; | |
| for(const argument in docker.app){ | |
| arguments.push(`APP_${argument.toUpperCase()}=${docker.app[argument]}`); | |
| } | |
| // export to environment | |
| core.exportVariable('DOCKER_CACHE_REGISTRY', docker.cache.registry); | |
| core.exportVariable('DOCKER_CACHE_NAME', docker.cache.name); | |
| core.exportVariable('DOCKER_CACHE_GRYPE', docker.cache.grype); | |
| core.exportVariable('DOCKER_IMAGE_NAME', docker.image.name); | |
| core.exportVariable('DOCKER_IMAGE_ARCH', docker.image.arch); | |
| core.exportVariable('DOCKER_IMAGE_TAGS', docker.tags.join(',')); | |
| core.exportVariable('DOCKER_IMAGE_DESCRIPTION', docker.image.description); | |
| core.exportVariable('DOCKER_IMAGE_ARGUMENTS', arguments.join("\r\n")); | |
| core.exportVariable('DOCKER_IMAGE_DOCKERFILE', opt.input?.etc?.dockerfile || 'arch.dockerfile'); | |
| core.exportVariable('WORKFLOW_BUILD', (opt.input?.build === undefined) ? false : opt.input.build); | |
| core.exportVariable('WORKFLOW_CREATE_RELEASE', (opt.input?.release === undefined) ? false : opt.input.release); | |
| core.exportVariable('WORKFLOW_CREATE_README', (opt.input?.readme === undefined) ? false : opt.input.readme); | |
| core.exportVariable('WORKFLOW_GRYPE_FAIL_ON_SEVERITY', (opt.dot?.grype?.fail === undefined) ? true : opt.dot.grype.fail); | |
| core.exportVariable('WORKFLOW_GRYPE_SEVERITY_CUTOFF', (opt.dot?.grype?.severity || 'high')); | |
| if(opt.dot?.readme?.comparison){ | |
| core.exportVariable('WORKFLOW_CREATE_COMPARISON', true); | |
| core.exportVariable('WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE', opt.dot.readme.comparison.image); | |
| core.exportVariable('WORKFLOW_CREATE_COMPARISON_IMAGE', `${docker.image.name}:${docker.app.version}`); | |
| } | |
| # DOCKER | |
| - name: docker / login to hub | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 | |
| with: | |
| username: 11notes | |
| password: ${{ secrets.DOCKER_TOKEN }} | |
| - name: github / login to ghcr | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 | |
| with: | |
| registry: ghcr.io | |
| username: 11notes | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: quay / login to quay | |
| uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 | |
| with: | |
| registry: quay.io | |
| username: 11notes+github | |
| password: ${{ secrets.QUAY_TOKEN }} | |
| - name: docker / setup qemu | |
| if: env.WORKFLOW_BUILD == 'true' | |
| uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a | |
| - name: docker / setup buildx | |
| if: env.WORKFLOW_BUILD == 'true' | |
| uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 | |
| with: | |
| driver-opts: network=host | |
| - name: docker / build & push & tag grype | |
| if: env.WORKFLOW_BUILD == 'true' | |
| id: docker-build | |
| uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d | |
| with: | |
| context: . | |
| file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} | |
| push: true | |
| platforms: ${{ env.DOCKER_IMAGE_ARCH }} | |
| cache-from: type=registry,ref=${{ env.DOCKER_CACHE_NAME }} | |
| cache-to: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true | |
| build-args: | | |
| ${{ env.DOCKER_IMAGE_ARGUMENTS }} | |
| tags: | | |
| ${{ env.DOCKER_CACHE_GRYPE }} | |
| - name: grype / scan | |
| if: env.WORKFLOW_BUILD == 'true' | |
| id: grype | |
| uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e | |
| with: | |
| image: ${{ env.DOCKER_CACHE_GRYPE }} | |
| fail-build: ${{ env.WORKFLOW_GRYPE_FAIL_ON_SEVERITY }} | |
| severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} | |
| output-format: 'sarif' | |
| by-cve: true | |
| cache-db: true | |
| - name: grype / fail | |
| if: env.WORKFLOW_BUILD == 'true' && (failure() || steps.grype.outcome == 'failure') | |
| uses: anchore/scan-action@dc6246fcaf83ae86fcc6010b9824c30d7320729e | |
| with: | |
| image: ${{ env.DOCKER_CACHE_GRYPE }} | |
| fail-build: false | |
| severity-cutoff: ${{ env.WORKFLOW_GRYPE_SEVERITY_CUTOFF }} | |
| output-format: 'table' | |
| by-cve: true | |
| cache-db: true | |
| - name: docker / build & push | |
| if: env.WORKFLOW_BUILD == 'true' | |
| uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d | |
| with: | |
| context: . | |
| file: ${{ env.DOCKER_IMAGE_DOCKERFILE }} | |
| push: true | |
| sbom: true | |
| provenance: mode=max | |
| platforms: ${{ env.DOCKER_IMAGE_ARCH }} | |
| cache-from: type=registry,ref=${{ env.DOCKER_CACHE_REGISTRY }}${{ env.DOCKER_CACHE_NAME }} | |
| cache-to: type=registry,ref=${{ env.DOCKER_CACHE_NAME }},mode=max,compression=zstd,force-compression=true | |
| build-args: | | |
| ${{ env.DOCKER_IMAGE_ARGUMENTS }} | |
| tags: | | |
| ${{ env.DOCKER_IMAGE_TAGS }} | |
| # RELEASE | |
| - name: github / release / log | |
| continue-on-error: true | |
| id: git-log | |
| run: | | |
| LOCAL_LAST_TAG=$(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`) | |
| echo "using last tag: ${LOCAL_LAST_TAG}" | |
| LOCAL_COMMITS=$(git log ${LOCAL_LAST_TAG}..HEAD --oneline) | |
| EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) | |
| echo "commits<<${EOF}" >> ${GITHUB_OUTPUT} | |
| echo "${LOCAL_COMMITS}" >> ${GITHUB_OUTPUT} | |
| echo "${EOF}" >> ${GITHUB_OUTPUT} | |
| - name: github / release / markdown | |
| if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-log.outcome == 'success' | |
| id: git-release | |
| uses: 11notes/action-docker-release@v1 | |
| # WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?! | |
| # --------------------------------------------------------------------------------- | |
| # the next step "github / release / create" creates a new release based on the code | |
| # in the repo. This code is not modified and can't be modified by this action. | |
| # It does create the markdown for the release, which could be abused, but to what | |
| # extend? Adding a link to a malicious repo? | |
| with: | |
| git_log: ${{ steps.git-log.outputs.commits }} | |
| - name: github / release / create | |
| if: env.WORKFLOW_CREATE_RELEASE == 'true' && steps.git-release.outcome == 'success' | |
| uses: actions/create-release@4c11c9fe1dcd9636620a16455165783b20fc7ea0 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: ${{ github.ref }} | |
| release_name: ${{ github.ref }} | |
| body: ${{ steps.git-release.outputs.release }} | |
| draft: false | |
| prerelease: false | |
| # LICENSE | |
| - name: license / update year | |
| continue-on-error: true | |
| uses: actions/github-script@62c3794a3eb6788d9a2a72b219504732c0c9a298 | |
| with: | |
| script: | | |
| const { existsSync, readFileSync, writeFileSync } = require('node:fs'); | |
| const { resolve } = require('node:path'); | |
| const file = 'LICENSE'; | |
| const year = new Date().getFullYear(); | |
| try{ | |
| const path = resolve(file); | |
| if(existsSync(path)){ | |
| let license = readFileSync(file).toString(); | |
| if(!new RegExp(`Copyright \\(c\\) ${year} 11notes`, 'i').test(license)){ | |
| license = license.replace(/Copyright \(c\) \d{4} /i, `Copyright (c) ${new Date().getFullYear()} `); | |
| writeFileSync(path, license); | |
| } | |
| }else{ | |
| throw new Error(`file ${file} does not exist`); | |
| } | |
| }catch(e){ | |
| core.setFailed(e); | |
| } | |
| # README | |
| - name: github / checkout HEAD | |
| continue-on-error: true | |
| run: | | |
| git checkout HEAD | |
| - name: docker / setup comparison images | |
| if: env.WORKFLOW_CREATE_COMPARISON == 'true' | |
| continue-on-error: true | |
| run: | | |
| docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }} | |
| docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size0.log | |
| docker image pull ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} | |
| docker image ls --filter "reference=${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }}" --format json | jq --raw-output '.Size' &> ./comparison.size1.log | |
| docker run --entrypoint "/bin/sh" --rm ${{ env.WORKFLOW_CREATE_COMPARISON_FOREIGN_IMAGE }} -c id &> ./comparison.id.log | |
| - name: github / create README.md | |
| id: github-readme | |
| continue-on-error: true | |
| if: env.WORKFLOW_CREATE_README == 'true' | |
| uses: 11notes/action-docker-readme@v1 | |
| # WHY IS THIS ACTION NOT SHA256 PINNED? SECURITY MUCH?!?!?! | |
| # --------------------------------------------------------------------------------- | |
| # the next step "github / commit & push" only adds the README and LICENSE as well as | |
| # compose.yaml to the repository. This does not pose a security risk if this action | |
| # would be compromised. The code of the app can't be changed by this action. Since | |
| # only the files mentioned are commited to the repo. Sure, someone could make a bad | |
| # compose.yaml, but since this serves only as an example I see no harm in that. | |
| with: | |
| sarif_file: ${{ steps.grype.outputs.sarif }} | |
| build_output_metadata: ${{ steps.docker-build.outputs.metadata }} | |
| - name: docker / push README.md to docker hub | |
| continue-on-error: true | |
| if: steps.github-readme.outcome == 'success' && hashFiles('README_NONGITHUB.md') != '' | |
| uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 | |
| env: | |
| DOCKER_USER: 11notes | |
| DOCKER_PASS: ${{ secrets.DOCKER_TOKEN }} | |
| with: | |
| destination_container_repo: ${{ env.DOCKER_IMAGE_NAME }} | |
| provider: dockerhub | |
| short_description: ${{ env.DOCKER_IMAGE_DESCRIPTION }} | |
| readme_file: 'README_NONGITHUB.md' | |
| - name: github / commit & push | |
| continue-on-error: true | |
| if: steps.github-readme.outcome == 'success' && hashFiles('README.md') != '' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add README.md | |
| if [ -f compose.yaml ]; then | |
| git add compose.yaml | |
| fi | |
| if [ -f compose.yml ]; then | |
| git add compose.yml | |
| fi | |
| if [ -f LICENSE ]; then | |
| git add LICENSE | |
| fi | |
| git commit -m "github-actions[bot]: update README.md" | |
| git push origin HEAD:master | |
| # REPOSITORY SETTINGS | |
| - name: github / update description and set repo defaults | |
| run: | | |
| curl --request PATCH \ | |
| --url https://api.github.com/repos/${{ github.repository }} \ | |
| --header 'authorization: Bearer ${{ secrets.REPOSITORY_TOKEN }}' \ | |
| --header 'content-type: application/json' \ | |
| --data '{ | |
| "description":"${{ env.DOCKER_IMAGE_DESCRIPTION }}", | |
| "homepage":"", | |
| "has_issues":true, | |
| "has_discussions":true, | |
| "has_projects":false, | |
| "has_wiki":false | |
| }' \ | |
| --fail |