├── .dockerignore ├── testdata └── Multi-Arch-Dockerfile ├── .github ├── FUNDING.yml ├── dependabot.yml ├── stale.yml ├── workflows │ ├── assign.yml │ ├── automerge.yml │ └── release.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── makefile ├── mock.sh ├── Dockerfile ├── LICENSE ├── SECURITY.md ├── action.yml ├── entrypoint.sh ├── README.md └── test.bats /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /testdata/Multi-Arch-Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: elgohr 2 | custom: ["https://www.paypal.me/elgohr"] 3 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .EXPORT_ALL_VARIABLES: 2 | DOCKER_BUILDKIT=0# to prevent caching of test results 3 | 4 | test: 5 | docker build . -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "docker" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 180 2 | daysUntilClose: 365 3 | exemptLabels: 4 | - pinned 5 | - security 6 | staleLabel: wontfix 7 | markComment: > 8 | This issue has been automatically marked as stale because it has not had 9 | recent activity. It will be closed if no further activity occurs. Thank you 10 | for your contributions. 11 | closeComment: false 12 | -------------------------------------------------------------------------------- /.github/workflows/assign.yml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | on: 3 | issues: 4 | types: [ opened ] 5 | jobs: 6 | auto-assign: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: pozil/auto-assign-issue@v2 10 | if: github.actor != 'dependabot[bot]' 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | assignees: elgohr -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | 3 | on: 4 | pull_request_target: 5 | branches: [ main ] 6 | types: [ opened ] 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | enableAutoMerge: 14 | runs-on: ubuntu-latest 15 | if: github.event.pull_request.user.login == 'dependabot[bot]' 16 | steps: 17 | - name: Enable auto-merge for Dependabot PRs 18 | run: gh pr merge --auto --merge "$PR_URL" 19 | env: 20 | PR_URL: ${{github.event.pull_request.html_url}} 21 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 22 | -------------------------------------------------------------------------------- /mock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | binary="$0" 3 | parameters="$@" 4 | echo "${binary} ${parameters}" >> mockArgs 5 | stdin=$(cat -) 6 | echo "${binary} ${stdin}" >> mockStdin 7 | 8 | function mockShouldFail() { 9 | [ "${MOCK_RETURNS[${binary}]}" = "_${parameters}" ] 10 | } 11 | 12 | source mockReturns 13 | if [ ! -z "${MOCK_RETURNS[${binary}]}" ] || [ ! -z "${MOCK_RETURNS[${binary} $1]}" ]; then 14 | if mockShouldFail ; then 15 | exit 1 16 | fi 17 | if [ ! -z "${MOCK_RETURNS[${binary} $1]}" ]; then 18 | echo ${MOCK_RETURNS[${binary} $1]} 19 | exit 0 20 | fi 21 | echo ${MOCK_RETURNS[${binary}]} 22 | fi 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: elgohr 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04@sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54 as runtime 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update \ 4 | && apt-get install -y ca-certificates curl gnupg lsb-release jq \ 5 | && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ 6 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ 7 | $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ 8 | && apt-get update \ 9 | && apt-get install -y docker-ce docker-ce-cli containerd.io 10 | ADD entrypoint.sh /entrypoint.sh 11 | ENTRYPOINT ["/entrypoint.sh"] 12 | 13 | FROM runtime as testEnv 14 | RUN apt-get install -y coreutils bats 15 | ADD test.bats /test.bats 16 | ADD mock.sh /usr/local/mock/docker 17 | ADD mock.sh /usr/local/mock/date 18 | RUN /test.bats 19 | 20 | FROM runtime 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lars Gohr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: elgohr 7 | 8 | --- 9 | 10 | ----- ------ 11 | First of all thank you, that you would like to help improving this action. 12 | 13 | Opening a bug report, you already tried to solve this issue yourself and can give detailed information in the underlying template. In this way everybody can benefit from this improvement. 14 | 15 | If you just came here because you want to have a one-on-one configuration session, please consider contacting me directly, as my PayPal is wide open. I don't run this as a business. So all the money gained by this will go directly to https://www.welthungerhilfe.org . In this way I will add the improvement for everybody for you. 16 | ----- ------ 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **To Reproduce** 22 | The Configuration to reproduce, including the output of the action. 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ---------------------------------------- | 7 | | v5 | :white_check_mark: | 8 | | v4 | :white_check_mark: (until 31st May 2023) | 9 | | main | :white_check_mark: | 10 | | master | :x: | 11 | 12 | Master has been deprecated (https://github.com/elgohr/Publish-Docker-Github-Action/commit/8217e91c0369a5342a4ef2d612de87492410a666) but will stay, so that it will not break peoples pipelines. 13 | 14 | ## Reporting a Vulnerability 15 | 16 | Please contact me directly via lars@gohr.digital 17 | 18 | ``` 19 | -----BEGIN PGP PUBLIC KEY BLOCK----- 20 | Version: GopenPGP 2.4.1 21 | Comment: https://gopenpgp.org 22 | 23 | xjMEXzQl4hYJKwYBBAHaRw8BAQdAgJPTL+JWwcqxDaWQT9OzmDUxS/neivTMCXKw 24 | sV4juSrNJWxhcnNAZ29oci5kaWdpdGFsIDxsYXJzQGdvaHIuZGlnaXRhbD7CjwQQ 25 | FgoAIAUCXzQl4gYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BACEJELP8jWbbD0eQ 26 | FiEEK87E+9+2plfaLBkGs/yNZtsPR5C9/AEAxy2Io+rMUDL4AZ8kpEPiuyciAx4T 27 | UBpNtRMXHroHJDAA/2I9WUbA3NkzOCT/BHHv9bZAgkHofjAVKMa3/iyPGP8KzjgE 28 | XzQl4hIKKwYBBAGXVQEFAQEHQEU2ICcRxHX/0IGIpnkTwkKbFjpe3CJcrWPr508j 29 | VKJwAwEIB8J4BBgWCAAJBQJfNCXiAhsMACEJELP8jWbbD0eQFiEEK87E+9+2plfa 30 | LBkGs/yNZtsPR5C2aAD/e7b6szYYyV8uHSmJRKwn92zqxm/maPbsLYxUAV5cEvEA 31 | /j/b0zjxt7G/i/GmvfYFwGd9dX2DSpa1DnzyXsGXIVAN 32 | =oIvB 33 | -----END PGP PUBLIC KEY BLOCK----- 34 | ``` 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [ main ] 5 | tags-ignore: 6 | - 'v*' 7 | pull_request: 8 | branches: [ main ] 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@v6 16 | - name: Build the Docker image 17 | run: docker build . 18 | 19 | integration-test-github: 20 | runs-on: ubuntu-latest 21 | if: github.event_name != 'pull_request' 22 | permissions: 23 | contents: read 24 | packages: write 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Publish to Registry 28 | if: ${{ github.actor != 'dependabot[bot]' }} 29 | uses: elgohr/Publish-Docker-Github-Action@main 30 | with: 31 | name: ghcr.io/elgohr/publish-docker-github-action/publish-docker-github-action 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | registry: ghcr.io 35 | 36 | integration-test-dockerhub: 37 | runs-on: ubuntu-latest 38 | if: github.event_name != 'pull_request' 39 | permissions: 40 | contents: read 41 | steps: 42 | - uses: actions/checkout@v6 43 | - name: Publish to Registry 44 | if: ${{ github.actor != 'dependabot[bot]' }} 45 | uses: elgohr/Publish-Docker-Github-Action@main 46 | with: 47 | name: lgohr/publish-docker-github-action 48 | username: ${{ secrets.DOCKER_USERNAME }} 49 | password: ${{ secrets.DOCKER_PASSWORD }} 50 | snapshot: true 51 | tag_names: true 52 | 53 | integration-test-multi-arch: 54 | runs-on: ubuntu-latest 55 | if: github.event_name != 'pull_request' 56 | permissions: 57 | contents: read 58 | packages: write 59 | steps: 60 | - uses: actions/checkout@v6 61 | - name: Set up Docker Buildx for Multi-Arch Build 62 | uses: docker/setup-buildx-action@v3 63 | - name: Publish to Registry 64 | if: ${{ github.actor != 'dependabot[bot]' }} 65 | uses: elgohr/Publish-Docker-Github-Action@main 66 | with: 67 | name: ghcr.io/elgohr/publish-docker-github-action/multi-arch-publish-docker-github-action 68 | username: ${{ github.actor }} 69 | password: ${{ secrets.GITHUB_TOKEN }} 70 | registry: ghcr.io 71 | platforms: linux/amd64,linux/arm64 72 | dockerfile: testdata/Multi-Arch-Dockerfile 73 | 74 | release: 75 | runs-on: ubuntu-latest 76 | if: github.event_name != 'pull_request' 77 | needs: 78 | - test 79 | - integration-test-github 80 | - integration-test-dockerhub 81 | - integration-test-multi-arch 82 | steps: 83 | - uses: actions/checkout@v6 84 | with: 85 | token: ${{ secrets.PUBLISH_TOKEN }} # for pushing to protected branch 86 | - name: Publish new version 87 | run: | 88 | git config --global user.email "no_reply@gohr.digital" 89 | git config --global user.name "Release Bot" 90 | git tag -fa v5 -m "Update v5 tag" 91 | git push origin v5 --force 92 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish Docker' 2 | author: 'Lars Gohr' 3 | branding: 4 | icon: 'anchor' 5 | color: 'blue' 6 | description: 'Uses the git branch as the docker tag and pushes the container' 7 | inputs: 8 | name: 9 | description: 'The name of the image you would like to push' 10 | required: true 11 | username: 12 | description: 'The login username for the registry' 13 | required: true 14 | password: 15 | description: 'The login password for the registry' 16 | required: true 17 | registry: 18 | description: 'Use registry for pushing to a custom registry' 19 | required: false 20 | snapshot: 21 | description: 'Use snapshot to push an additional image' 22 | required: false 23 | default_branch: 24 | description: 'Set the default branch of your repository (default: master)' 25 | required: false 26 | dockerfile: 27 | description: 'Use dockerfile when you would like to explicitly build a Dockerfile' 28 | required: false 29 | workdir: 30 | description: 'Use workdir when you would like to change the directory for building' 31 | required: false 32 | context: 33 | description: 'Use context when you would like to change the Docker build context.' 34 | required: false 35 | buildargs: 36 | description: 'Use buildargs when you want to pass a list of environment variables as build-args' 37 | required: false 38 | buildoptions: 39 | description: 'Use buildoptions when you want to configure options for building' 40 | required: false 41 | cache: 42 | description: 'Use cache when you have big images, that you would only like to build partially' 43 | required: false 44 | tags: 45 | description: 'Use tags when you want to bring your own tags (separated by comma)' 46 | required: false 47 | tag_names: 48 | description: 'Use tag_names when you want to push tags/release by their git name' 49 | required: false 50 | tag_semver: 51 | description: 'Push semver docker tags. e.g. image:1.2.3, image:1.2, image:1' 52 | required: false 53 | no_push: 54 | description: 'Set no_push to true if you want to prevent the action from pushing to a registry (default: false)' 55 | required: false 56 | platforms: 57 | description: 'Use platforms for building multi-arch images' 58 | required: false 59 | outputs: 60 | tag: 61 | description: 'Is the tag, which was pushed' 62 | value: ${{ steps.docker-publish.outputs.tag }} 63 | snapshot-tag: 64 | description: 'Is the tag that is generated by the snapshot-option and pushed' 65 | value: ${{ steps.docker-publish.outputs.snapshot-tag }} 66 | digest: 67 | description: 'Is the digest of the image, which was pushed' 68 | value: ${{ steps.docker-publish.outputs.digest }} 69 | runs: 70 | using: 'composite' 71 | steps: 72 | - id: docker-publish 73 | run: $GITHUB_ACTION_PATH/entrypoint.sh 74 | shell: bash 75 | env: 76 | INPUT_NAME: ${{ inputs.name }} 77 | INPUT_USERNAME: ${{ inputs.username }} 78 | INPUT_PASSWORD: ${{ inputs.password }} 79 | INPUT_REGISTRY: ${{ inputs.registry }} 80 | INPUT_SNAPSHOT: ${{ inputs.snapshot }} 81 | INPUT_DEFAULT_BRANCH: ${{ inputs.default_branch }} 82 | INPUT_DOCKERFILE: ${{ inputs.dockerfile }} 83 | INPUT_WORKDIR: ${{ inputs.workdir }} 84 | INPUT_CONTEXT: ${{ inputs.context }} 85 | INPUT_BUILDARGS: ${{ inputs.buildargs }} 86 | INPUT_BUILDOPTIONS: ${{ inputs.buildoptions }} 87 | INPUT_CACHE: ${{ inputs.cache }} 88 | INPUT_TAGS: ${{ inputs.tags }} 89 | INPUT_TAG_NAMES: ${{ inputs.tag_names }} 90 | INPUT_TAG_SEMVER: ${{ inputs.tag_semver }} 91 | INPUT_NO_PUSH: ${{ inputs.no_push }} 92 | INPUT_PLATFORMS: ${{ inputs.platforms }} -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | main() { 5 | echo "" # see https://github.com/actions/toolkit/issues/168 6 | 7 | sanitize "${INPUT_NAME}" "name" 8 | if ! usesBoolean "${INPUT_NO_PUSH}"; then 9 | sanitize "${INPUT_USERNAME}" "username" 10 | sanitize "${INPUT_PASSWORD}" "password" 11 | fi 12 | 13 | registryToLower 14 | nameToLower 15 | 16 | REGISTRY_NO_PROTOCOL=$(echo "${INPUT_REGISTRY}" | sed -e 's/^https:\/\///g') 17 | if uses "${INPUT_REGISTRY}" && ! isPartOfTheName "${REGISTRY_NO_PROTOCOL}"; then 18 | INPUT_NAME="${REGISTRY_NO_PROTOCOL}/${INPUT_NAME}" 19 | fi 20 | 21 | if uses "${INPUT_TAGS}"; then 22 | TAGS=$(echo "${INPUT_TAGS}" | sed "s/,/ /g") 23 | else 24 | translateDockerTag 25 | fi 26 | 27 | if uses "${INPUT_WORKDIR}"; then 28 | changeWorkingDirectory 29 | fi 30 | 31 | if uses "${INPUT_USERNAME}" && uses "${INPUT_PASSWORD}"; then 32 | echo "${INPUT_PASSWORD}" | docker login -u ${INPUT_USERNAME} --password-stdin ${INPUT_REGISTRY} 33 | fi 34 | 35 | FIRST_TAG=$(echo "${TAGS}" | cut -d ' ' -f1) 36 | DOCKERNAME="${INPUT_NAME}:${FIRST_TAG}" 37 | BUILDPARAMS="" 38 | CONTEXT="." 39 | 40 | if uses "${INPUT_DOCKERFILE}"; then 41 | useCustomDockerfile 42 | fi 43 | if uses "${INPUT_BUILDARGS}"; then 44 | addBuildArgs 45 | fi 46 | if uses "${INPUT_CONTEXT}"; then 47 | CONTEXT="${INPUT_CONTEXT}" 48 | fi 49 | if usesBoolean "${INPUT_CACHE}"; then 50 | useBuildCache 51 | fi 52 | if usesBoolean "${INPUT_SNAPSHOT}"; then 53 | useSnapshot 54 | fi 55 | 56 | build 57 | 58 | if usesBoolean "${INPUT_NO_PUSH}"; then 59 | if uses "${INPUT_USERNAME}" && uses "${INPUT_PASSWORD}"; then 60 | docker logout 61 | fi 62 | exit 0 63 | fi 64 | 65 | if ! uses "${INPUT_PLATFORMS}"; then 66 | push 67 | fi 68 | 69 | echo "tag=${FIRST_TAG}" >> "$GITHUB_OUTPUT" 70 | if uses "${INPUT_PLATFORMS}"; then 71 | DIGEST=$(jq -r '."containerimage.digest"' metadata.json) 72 | else 73 | DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${DOCKERNAME}) 74 | fi 75 | echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" 76 | 77 | docker logout 78 | } 79 | 80 | sanitize() { 81 | if [ -z "${1}" ]; then 82 | >&2 echo "Unable to find the ${2}. Did you set with.${2}?" 83 | exit 1 84 | fi 85 | } 86 | 87 | registryToLower(){ 88 | INPUT_REGISTRY=$(echo "${INPUT_REGISTRY}" | tr '[A-Z]' '[a-z]') 89 | } 90 | 91 | nameToLower(){ 92 | INPUT_NAME=$(echo "${INPUT_NAME}" | tr '[A-Z]' '[a-z]') 93 | } 94 | 95 | isPartOfTheName() { 96 | [ $(echo "${INPUT_NAME}" | sed -e "s/${1}//g") != "${INPUT_NAME}" ] 97 | } 98 | 99 | translateDockerTag() { 100 | local BRANCH=$(echo "${GITHUB_REF}" | sed -e "s/refs\/heads\///g" | sed -e "s/\//-/g") 101 | if hasCustomTag; then 102 | TAGS=$(echo "${INPUT_NAME}" | cut -d':' -f2) 103 | INPUT_NAME=$(echo "${INPUT_NAME}" | cut -d':' -f1) 104 | elif isOnDefaultBranch; then 105 | TAGS="latest" 106 | elif isGitTag && usesBoolean "${INPUT_TAG_SEMVER}" && isSemver "${GITHUB_REF}"; then 107 | if isPreRelease "${GITHUB_REF}"; then 108 | TAGS=$(echo "${GITHUB_REF}" | sed -e "s/refs\/tags\///g" | sed -E "s/v?([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z]+(\.[0-9]+)?)?/\1.\2.\3\4/g") 109 | else 110 | TAGS=$(echo "${GITHUB_REF}" | sed -e "s/refs\/tags\///g" | sed -E "s/v?([0-9]+)\.([0-9]+)\.([0-9]+)/\1.\2.\3 \1.\2 \1/g") 111 | fi 112 | elif isGitTag && usesBoolean "${INPUT_TAG_NAMES}"; then 113 | TAGS=$(echo "${GITHUB_REF}" | sed -e "s/refs\/tags\///g") 114 | elif isGitTag; then 115 | TAGS="latest" 116 | elif isPullRequest; then 117 | TAGS="${GITHUB_SHA}" 118 | else 119 | TAGS="${BRANCH}" 120 | fi; 121 | } 122 | 123 | hasCustomTag() { 124 | [ $(echo "${INPUT_NAME}" | sed -e "s/://g") != "${INPUT_NAME}" ] 125 | } 126 | 127 | isOnDefaultBranch() { 128 | if uses "${INPUT_DEFAULT_BRANCH}"; then 129 | [ "${BRANCH}" = "${INPUT_DEFAULT_BRANCH}" ] 130 | else 131 | [ "${BRANCH}" = "master" ] || [ "${BRANCH}" = "main" ] 132 | fi 133 | } 134 | 135 | isGitTag() { 136 | [ $(echo "${GITHUB_REF}" | sed -e "s/refs\/tags\///g") != "${GITHUB_REF}" ] 137 | } 138 | 139 | isPullRequest() { 140 | [ $(echo "${GITHUB_REF}" | sed -e "s/refs\/pull\///g") != "${GITHUB_REF}" ] 141 | } 142 | 143 | changeWorkingDirectory() { 144 | cd "${INPUT_WORKDIR}" 145 | } 146 | 147 | useCustomDockerfile() { 148 | BUILDPARAMS="${BUILDPARAMS} -f ${INPUT_DOCKERFILE}" 149 | } 150 | 151 | addBuildArgs() { 152 | for ARG in $(echo "${INPUT_BUILDARGS}" | tr ',' '\n'); do 153 | BUILDPARAMS="${BUILDPARAMS} --build-arg ${ARG}" 154 | echo "::add-mask::${ARG}" 155 | done 156 | } 157 | 158 | useBuildCache() { 159 | if docker pull "${DOCKERNAME}" 2>/dev/null; then 160 | BUILDPARAMS="${BUILDPARAMS} --cache-from ${DOCKERNAME}" 161 | fi 162 | } 163 | 164 | uses() { 165 | [ ! -z "${1}" ] 166 | } 167 | 168 | usesBoolean() { 169 | [ ! -z "${1}" ] && [ "${1}" = "true" ] 170 | } 171 | 172 | isSemver() { 173 | echo "${1}" | grep -Eq '^refs/tags/v?([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z]+(\.[0-9]+)?)?$' 174 | } 175 | 176 | isPreRelease() { 177 | echo "${1}" | grep -Eq '-' 178 | } 179 | 180 | useSnapshot() { 181 | local TIMESTAMP=`date +%Y%m%d%H%M%S` 182 | local SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-6) 183 | local SNAPSHOT_TAG="${TIMESTAMP}${SHORT_SHA}" 184 | TAGS="${TAGS} ${SNAPSHOT_TAG}" 185 | echo "snapshot-tag=${SNAPSHOT_TAG}" >> "$GITHUB_OUTPUT" 186 | } 187 | 188 | build() { 189 | local BUILD_TAGS="" 190 | for TAG in ${TAGS}; do 191 | BUILD_TAGS="${BUILD_TAGS}-t ${INPUT_NAME}:${TAG} " 192 | done 193 | if uses "${INPUT_PLATFORMS}"; then 194 | local PLATFORMS="--platform ${INPUT_PLATFORMS}" 195 | local PUSHING="--push" 196 | if usesBoolean "${INPUT_NO_PUSH}"; then 197 | PUSHING="" 198 | fi 199 | docker buildx build ${PUSHING} --metadata-file metadata.json ${PLATFORMS} ${INPUT_BUILDOPTIONS} ${BUILDPARAMS} ${BUILD_TAGS} ${CONTEXT} 200 | else 201 | docker build ${INPUT_BUILDOPTIONS} ${BUILDPARAMS} ${BUILD_TAGS} ${CONTEXT} 202 | fi 203 | } 204 | 205 | push() { 206 | for TAG in ${TAGS}; do 207 | docker push "${INPUT_NAME}:${TAG}" 208 | done 209 | } 210 | 211 | main 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Publishes docker containers 2 | [![Release](https://github.com/elgohr/Publish-Docker-Github-Action/workflows/Release/badge.svg)](https://github.com/elgohr/Publish-Docker-Github-Action/actions/workflows/release.yml) 3 | 4 | This Action for [Docker](https://www.docker.com/) uses the Git branch as the [Docker tag](https://docs.docker.com/engine/reference/commandline/tag/) for building and pushing the container. 5 | Hereby the master-branch is published as the latest-tag. 6 | 7 | ## Usage 8 | 9 | ## Example pipeline 10 | ```yaml 11 | name: Publish Docker 12 | on: [push] 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Publish to Registry 19 | uses: elgohr/Publish-Docker-Github-Action@v5 20 | with: 21 | name: myDocker/repository 22 | username: ${{ secrets.DOCKER_USERNAME }} 23 | password: ${{ secrets.DOCKER_PASSWORD }} 24 | ``` 25 | 26 | ## Mandatory Arguments 27 | 28 | `name` is the name of the image you would like to push 29 | `username` the login username for the registry 30 | `password` the authentication token [preferred] or login password for the registry. 31 | 32 | If you would like to publish the image to other registries, these actions might be helpful 33 | 34 | | Registry | Action | 35 | |------------------------------------------------------|-----------------------------------------------| 36 | | Amazon Webservices Elastic Container Registry (ECR) | https://github.com/elgohr/ecr-login-action | 37 | | Google Cloud Container Registry | https://github.com/elgohr/gcloud-login-action | 38 | 39 | ## Outputs 40 | 41 | `tag` is the tag, which was pushed 42 | `snapshot-tag` is the tag that is generated by the [snapshot-option](https://github.com/elgohr/Publish-Docker-Github-Action#snapshot) and pushed 43 | `digest` is the digest of the image, which was pushed 44 | 45 | ## Optional Arguments 46 | 47 | ### registry 48 | Use `registry` for pushing to a custom registry. 49 | 50 | As GitHub Packages Docker registry uses a different path format to GitHub Container Registry or Docker Hub. See [Configuring Docker for use with GitHub Package Registry](https://help.github.com/en/github/managing-packages-with-github-package-registry/configuring-docker-for-use-with-github-package-registry#publishing-a-package) for more information. 51 | For publishing to GitHub Container Registry please see [Migrating to GitHub Container Registry for Docker images](https://docs.github.com/en/packages/getting-started-with-github-container-registry/migrating-to-github-container-registry-for-docker-images). 52 | 53 | If you're using GitHub Packages Docker or GitHub Container Registry, you might also want to use `${{ github.actor }}` as the `username`. 54 | 55 | ```yaml 56 | with: 57 | name: owner/repository/image 58 | username: ${{ github.actor }} 59 | password: ${{ secrets.GITHUB_TOKEN }} 60 | registry: ghcr.io 61 | ``` 62 | 63 | ### snapshot 64 | Use `snapshot` to push an additional image, which is tagged with 65 | `{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}{SECOND}{first 6 digits of the git sha}`. 66 | The date was inserted to prevent new builds with external dependencies override older builds with the same sha. 67 | When you would like to think about versioning images, this might be useful. 68 | 69 | ```yaml 70 | with: 71 | name: myDocker/repository 72 | username: ${{ secrets.DOCKER_USERNAME }} 73 | password: ${{ secrets.DOCKER_PASSWORD }} 74 | snapshot: true 75 | ``` 76 | 77 | ### default_branch 78 | Use `default_branch` when you want to use a different branch than `master` as the default branch. 79 | 80 | ```yaml 81 | with: 82 | name: myDocker/repository 83 | username: ${{ secrets.DOCKER_USERNAME }} 84 | password: ${{ secrets.DOCKER_PASSWORD }} 85 | default_branch: trunk 86 | ``` 87 | 88 | ### dockerfile 89 | Use `dockerfile` when you would like to explicitly build a Dockerfile. 90 | This might be useful when you have multiple DockerImages. 91 | 92 | ```yaml 93 | with: 94 | name: myDocker/repository 95 | username: ${{ secrets.DOCKER_USERNAME }} 96 | password: ${{ secrets.DOCKER_PASSWORD }} 97 | dockerfile: MyDockerFileName 98 | ``` 99 | 100 | ### workdir 101 | Use `workdir` when you would like to change the directory for building. 102 | 103 | ```yaml 104 | with: 105 | name: myDocker/repository 106 | username: ${{ secrets.DOCKER_USERNAME }} 107 | password: ${{ secrets.DOCKER_PASSWORD }} 108 | workdir: mySubDirectory 109 | ``` 110 | 111 | ### context 112 | Use `context` when you would like to change the Docker build context. 113 | 114 | ```yaml 115 | with: 116 | name: myDocker/repository 117 | username: ${{ secrets.DOCKER_USERNAME }} 118 | password: ${{ secrets.DOCKER_PASSWORD }} 119 | context: myContextDirectory 120 | ``` 121 | 122 | ### buildargs 123 | Use `buildargs` when you want to pass a list of environment variables as [build-args](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg). Identifiers are separated by comma. 124 | All `buildargs` will be masked, so that they don't appear in the logs. 125 | 126 | ```yaml 127 | - name: Publish to Registry 128 | uses: elgohr/Publish-Docker-Github-Action@v5 129 | env: 130 | MY_FIRST: variableContent 131 | MY_SECOND: variableContent 132 | with: 133 | name: myDocker/repository 134 | username: ${{ secrets.DOCKER_USERNAME }} 135 | password: ${{ secrets.DOCKER_PASSWORD }} 136 | buildargs: MY_FIRST,MY_SECOND 137 | ``` 138 | 139 | ### buildoptions 140 | Use `buildoptions` when you want to configure [options](https://docs.docker.com/engine/reference/commandline/build/#options) for building. 141 | 142 | ```yaml 143 | - name: Publish to Registry 144 | uses: elgohr/Publish-Docker-Github-Action@v5 145 | with: 146 | name: myDocker/repository 147 | username: ${{ secrets.DOCKER_USERNAME }} 148 | password: ${{ secrets.DOCKER_PASSWORD }} 149 | buildoptions: "--compress --force-rm" 150 | ``` 151 | 152 | ### platforms 153 | Use `platforms` when you would like to build for specific target architectures. 154 | Architectures are separated by comma. 155 | 156 | `docker/setup-buildx-action` must be executed before a step that contains `platforms`. 157 | 158 | ```yaml 159 | - name: Set up Docker Buildx 160 | uses: docker/setup-buildx-action@v2 161 | - name: Publish to Registry 162 | uses: elgohr/Publish-Docker-Github-Action@v5 163 | with: 164 | name: myDocker/repository 165 | username: ${{ secrets.DOCKER_USERNAME }} 166 | password: ${{ secrets.DOCKER_PASSWORD }} 167 | platforms: linux/amd64,linux/arm64 168 | ``` 169 | 170 | ### cache 171 | Use `cache` when you have big images, that you would only like to build partially (changed layers). 172 | > CAUTION: Docker builds will cache non-repoducable commands, such as installing packages. If you use this option, your packages will never update. To avoid this, run this action on a schedule with caching **disabled** to rebuild the cache periodically. 173 | 174 | ```yaml 175 | name: Publish to Registry 176 | on: 177 | push: 178 | branches: 179 | - master 180 | schedule: 181 | - cron: '0 2 * * 0' # Weekly on Sundays at 02:00 182 | jobs: 183 | update: 184 | runs-on: ubuntu-latest 185 | steps: 186 | - uses: actions/checkout@v3 187 | - name: Publish to Registry 188 | uses: elgohr/Publish-Docker-Github-Action@v5 189 | with: 190 | name: myDocker/repository 191 | username: ${{ secrets.DOCKER_USERNAME }} 192 | password: ${{ secrets.DOCKER_PASSWORD }} 193 | cache: ${{ github.event_name != 'schedule' }} 194 | ``` 195 | 196 | ### no_push 197 | Use `no_push` when you want to build an image, but not push it to a registry. 198 | 199 | ```yaml 200 | with: 201 | name: myDocker/repository 202 | username: ${{ secrets.DOCKER_USERNAME }} 203 | password: ${{ secrets.DOCKER_PASSWORD }} 204 | no_push: ${{ github.event_name == 'push' }} 205 | ``` 206 | 207 | ### Tags 208 | 209 | This action supports multiple options that tags are handled. 210 | By default a tag is pushed as `latest`. 211 | Furthermore, one of the following options can be used. 212 | 213 | #### tags 214 | Use `tags` when you want to bring your own tags (separated by comma). 215 | 216 | ```yaml 217 | - name: Publish to Registry 218 | uses: elgohr/Publish-Docker-Github-Action@v5 219 | with: 220 | name: myDocker/repository 221 | username: ${{ secrets.DOCKER_USERNAME }} 222 | password: ${{ secrets.DOCKER_PASSWORD }} 223 | tags: "latest,another" 224 | ``` 225 | 226 | When using dynamic tag names the environment variable must be set via echo, as variables set in the environment will not auto resolve by convention. 227 | This example illustrates how you would push to latest along with creating a custom version tag in a release. Setting it to only run on published events will keep your tags from being filled with commit hashes and will only publish when a GitHub release is created, so if the GitHub release is 2.14 this will publish to the latest and 2.14 tags. 228 | 229 | ```yaml 230 | name: Publish to Registry 231 | on: 232 | release: 233 | types: [published] 234 | push: 235 | branches: 236 | - master 237 | schedule: 238 | - cron: '0 2 * * 0' # Weekly on Sundays at 02:00 239 | jobs: 240 | update: 241 | runs-on: ubuntu-latest 242 | steps: 243 | - uses: actions/checkout@v3 244 | - id: pre-step 245 | shell: bash 246 | run: echo "release-version=$(echo ${GITHUB_REF:10})" >> $GITHUB_OUTPUT 247 | - name: Publish to Registry 248 | uses: elgohr/Publish-Docker-Github-Action@v5 249 | with: 250 | name: myDocker/repository 251 | username: ${{ secrets.DOCKER_USERNAME }} 252 | password: ${{ secrets.DOCKER_PASSWORD }} 253 | tags: "latest,${{ steps.pre-step.outputs.release-version }}" 254 | ``` 255 | 256 | #### tag_names 257 | Use `tag_names` when you want to push tags/release by their git name (e.g. `refs/tags/MY_TAG_NAME`). 258 | > CAUTION: Images produced by this feature can be override by branches with the same name - without a way to restore. 259 | 260 | ```yaml 261 | with: 262 | name: myDocker/repository 263 | username: ${{ secrets.DOCKER_USERNAME }} 264 | password: ${{ secrets.DOCKER_PASSWORD }} 265 | tag_names: true 266 | ``` 267 | 268 | #### tag_semver 269 | Use `tag_semver` when you want to push tags using the semver syntax by their git name (e.g. `refs/tags/v1.2.3`). This will push four 270 | docker tags: `1.2.3`, `1.2` and `1`. A prefix 'v' will automatically be removed. 271 | > CAUTION: Images produced by this feature can be override by branches with the same name - without a way to restore. 272 | 273 | ```yaml 274 | with: 275 | name: myDocker/repository 276 | username: ${{ secrets.DOCKER_USERNAME }} 277 | password: ${{ secrets.DOCKER_PASSWORD }} 278 | tag_semver: true 279 | ``` 280 | 281 | ## Sponsors 282 | 283 | A big "Thank you!" to the people that help to make this code sustainable: 284 | SerhatG 285 | EdgeDB 286 | Pedro Sanders 287 | -------------------------------------------------------------------------------- /test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | setup(){ 4 | export PATH="/usr/local/sbin:/usr/local/mock:/usr/sbin:/usr/bin:/sbin:/bin" 5 | export GITHUB_OUTPUT="/tmp/githubOutput" 6 | 7 | cat /dev/null >| mockArgs 8 | cat /dev/null >| mockStdin 9 | 10 | declare -A -p MOCK_RETURNS=( 11 | ['/usr/local/mock/docker']="" 12 | ['sudo']="" 13 | ) > mockReturns 14 | 15 | export GITHUB_REF='refs/heads/master' 16 | export INPUT_USERNAME='USERNAME' 17 | export INPUT_PASSWORD='PASSWORD' 18 | export INPUT_NAME='my/repository' 19 | } 20 | 21 | teardown() { 22 | rm -f "${GITHUB_OUTPUT}" 23 | unset INPUT_TAG_NAMES 24 | unset INPUT_SNAPSHOT 25 | unset INPUT_DOCKERFILE 26 | unset INPUT_REGISTRY 27 | unset INPUT_CACHE 28 | unset INPUT_PLATFORMS 29 | unset GITHUB_SHA 30 | unset INPUT_PULL_REQUESTS 31 | unset MOCK_ERROR_CONDITION 32 | } 33 | 34 | @test "it pushes main branch to latest" { 35 | export GITHUB_REF='refs/heads/main' 36 | 37 | run /entrypoint.sh 38 | 39 | expectGitHubOutputContains "tag=latest" 40 | 41 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 42 | /usr/local/mock/docker build -t my/repository:latest . 43 | /usr/local/mock/docker push my/repository:latest 44 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:latest 45 | /usr/local/mock/docker logout" 46 | } 47 | 48 | @test "it pushes master branch to latest" { 49 | export GITHUB_REF='refs/heads/master' 50 | 51 | run /entrypoint.sh 52 | 53 | expectGitHubOutputContains "tag=latest" 54 | 55 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 56 | /usr/local/mock/docker build -t my/repository:latest . 57 | /usr/local/mock/docker push my/repository:latest 58 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:latest 59 | /usr/local/mock/docker logout" 60 | } 61 | 62 | @test "it pushes branch as name of the branch" { 63 | export GITHUB_REF='refs/heads/myBranch' 64 | 65 | run /entrypoint.sh 66 | 67 | expectGitHubOutputContains "tag=myBranch" 68 | 69 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:myBranch . 70 | /usr/local/mock/docker push my/repository:myBranch" 71 | } 72 | 73 | @test "it converts dashes in branch to hyphens" { 74 | export GITHUB_REF='refs/heads/myBranch/withDash' 75 | 76 | run /entrypoint.sh 77 | 78 | expectGitHubOutputContains "tag=myBranch-withDash" 79 | 80 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:myBranch-withDash . 81 | /usr/local/mock/docker push my/repository:myBranch-withDash" 82 | } 83 | 84 | @test "it pushes tags to latest" { 85 | export GITHUB_REF='refs/tags/myRelease' 86 | 87 | run /entrypoint.sh 88 | 89 | expectGitHubOutputContains "tag=latest" 90 | 91 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 92 | /usr/local/mock/docker build -t my/repository:latest . 93 | /usr/local/mock/docker push my/repository:latest 94 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:latest 95 | /usr/local/mock/docker logout" 96 | } 97 | 98 | @test "with tag names it pushes tags using the name" { 99 | export GITHUB_REF='refs/tags/myRelease' 100 | export INPUT_TAG_NAMES="true" 101 | 102 | run /entrypoint.sh 103 | 104 | expectGitHubOutputContains "tag=myRelease" 105 | 106 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:myRelease . 107 | /usr/local/mock/docker push my/repository:myRelease" 108 | } 109 | 110 | @test "with tag names set to false it doesn't push tags using the name" { 111 | export GITHUB_REF='refs/tags/myRelease' 112 | export INPUT_TAG_NAMES="false" 113 | 114 | run /entrypoint.sh 115 | 116 | expectGitHubOutputContains "tag=latest" 117 | 118 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest . 119 | /usr/local/mock/docker push my/repository:latest" 120 | } 121 | 122 | @test "with tag semver it pushes tags using the major and minor versions (single digit)" { 123 | export GITHUB_REF='refs/tags/v1.2.3' 124 | export INPUT_TAG_SEMVER="true" 125 | 126 | run /entrypoint.sh 127 | 128 | expectGitHubOutputContains "tag=1.2.3" 129 | 130 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 131 | /usr/local/mock/docker build -t my/repository:1.2.3 -t my/repository:1.2 -t my/repository:1 . 132 | /usr/local/mock/docker push my/repository:1.2.3 133 | /usr/local/mock/docker push my/repository:1.2 134 | /usr/local/mock/docker push my/repository:1 135 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:1.2.3 136 | /usr/local/mock/docker logout" 137 | } 138 | 139 | @test "with tag semver it pushes tags using the major and minor versions (multi digits)" { 140 | export GITHUB_REF='refs/tags/v12.345.5678' 141 | export INPUT_TAG_SEMVER="true" 142 | 143 | run /entrypoint.sh 144 | 145 | expectGitHubOutputContains "tag=12.345.5678" 146 | 147 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 148 | /usr/local/mock/docker build -t my/repository:12.345.5678 -t my/repository:12.345 -t my/repository:12 . 149 | /usr/local/mock/docker push my/repository:12.345.5678 150 | /usr/local/mock/docker push my/repository:12.345 151 | /usr/local/mock/docker push my/repository:12 152 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:12.345.5678 153 | /usr/local/mock/docker logout" 154 | } 155 | 156 | @test "with tag semver it pushes tags using the pre-release, but does not update the major, minor or patch version" { 157 | # as pre-release versions tend to be unstable 158 | # https://semver.org/#spec-item-11 159 | 160 | SUFFIXES=('alpha.1' 'alpha' 'ALPHA' 'ALPHA.11' 'beta' 'rc.11') 161 | for SUFFIX in "${SUFFIXES[@]}" 162 | do 163 | export GITHUB_REF="refs/tags/v1.1.1-${SUFFIX}" 164 | export INPUT_TAG_SEMVER="true" 165 | 166 | run /entrypoint.sh 167 | 168 | expectGitHubOutputContains "tag=1.1.1-${SUFFIX}" 169 | 170 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 171 | /usr/local/mock/docker build -t my/repository:1.1.1-${SUFFIX} . 172 | /usr/local/mock/docker push my/repository:1.1.1-${SUFFIX} 173 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:1.1.1-${SUFFIX} 174 | /usr/local/mock/docker logout" 175 | done 176 | } 177 | 178 | @test "with tag semver it pushes tags without 'v' prefix" { 179 | export GITHUB_REF='refs/tags/1.2.34' 180 | export INPUT_TAG_SEMVER="true" 181 | 182 | run /entrypoint.sh 183 | 184 | expectGitHubOutputContains "tag=1.2.34" 185 | 186 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 187 | /usr/local/mock/docker build -t my/repository:1.2.34 -t my/repository:1.2 -t my/repository:1 . 188 | /usr/local/mock/docker push my/repository:1.2.34 189 | /usr/local/mock/docker push my/repository:1.2 190 | /usr/local/mock/docker push my/repository:1 191 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:1.2.34 192 | /usr/local/mock/docker logout" 193 | } 194 | 195 | @test "with tag semver it pushes latest when tag has invalid semver version" { 196 | export GITHUB_REF='refs/tags/vAA.BB.CC' 197 | export INPUT_TAG_SEMVER="true" 198 | 199 | run /entrypoint.sh 200 | 201 | expectGitHubOutputContains "tag=latest" 202 | 203 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest . 204 | /usr/local/mock/docker push my/repository:latest" 205 | } 206 | 207 | @test "with tag semver set to false it doesn't push tags using semver" { 208 | export GITHUB_REF='refs/tags/v1.2.34' 209 | export INPUT_TAG_NAMES="false" 210 | 211 | run /entrypoint.sh 212 | 213 | expectGitHubOutputContains "tag=latest" 214 | 215 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest . 216 | /usr/local/mock/docker push my/repository:latest" 217 | } 218 | 219 | @test "it pushes specific Dockerfile to latest" { 220 | export INPUT_DOCKERFILE='MyDockerFileName' 221 | 222 | run /entrypoint.sh export GITHUB_REF='refs/heads/master' 223 | 224 | expectGitHubOutputContains "tag=latest" 225 | 226 | expectMockCalledContains "/usr/local/mock/docker build -f MyDockerFileName -t my/repository:latest . 227 | /usr/local/mock/docker push my/repository:latest" 228 | } 229 | 230 | @test "it pushes a snapshot by sha and date in addition" { 231 | export INPUT_SNAPSHOT='true' 232 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 233 | 234 | declare -A -p MOCK_RETURNS=( 235 | ['/usr/local/mock/docker']="" 236 | ['/usr/local/mock/date']="197001010101" 237 | ) > mockReturns 238 | 239 | run /entrypoint.sh 240 | 241 | expectGitHubOutputContains "snapshot-tag=19700101010112169etag=latest" 242 | 243 | expectMockCalledContains "/usr/local/mock/date +%Y%m%d%H%M%S 244 | /usr/local/mock/docker build -t my/repository:latest -t my/repository:19700101010112169e . 245 | /usr/local/mock/docker push my/repository:latest 246 | /usr/local/mock/docker push my/repository:19700101010112169e" 247 | } 248 | 249 | @test "it does not push a snapshot by sha and date in addition when turned off" { 250 | export INPUT_SNAPSHOT='false' 251 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 252 | 253 | declare -A -p MOCK_RETURNS=( 254 | ['/usr/local/mock/docker']="" 255 | ['/usr/local/mock/date']="197001010101" 256 | ) > mockReturns 257 | 258 | run /entrypoint.sh 259 | 260 | expectGitHubOutputContains "tag=latest" 261 | 262 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest . 263 | /usr/local/mock/docker push my/repository:latest" 264 | } 265 | 266 | @test "it caches image from former build and uses it for snapshot" { 267 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 268 | export INPUT_SNAPSHOT='true' 269 | export INPUT_CACHE='true' 270 | 271 | declare -A -p MOCK_RETURNS=( 272 | ['/usr/local/mock/docker']="" 273 | ['/usr/local/mock/date']="197001010101" 274 | ) > mockReturns 275 | 276 | run /entrypoint.sh 277 | 278 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 279 | /usr/local/mock/docker pull my/repository:latest 280 | /usr/local/mock/date +%Y%m%d%H%M%S 281 | /usr/local/mock/docker build --cache-from my/repository:latest -t my/repository:latest -t my/repository:19700101010112169e . 282 | /usr/local/mock/docker push my/repository:latest 283 | /usr/local/mock/docker push my/repository:19700101010112169e" 284 | } 285 | 286 | @test "it does not use the cache for building when pulling the former image failed" { 287 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 288 | export MOCK_DATE='197001010101' 289 | export INPUT_SNAPSHOT='true' 290 | export INPUT_CACHE='true' 291 | 292 | declare -A -p MOCK_RETURNS=( 293 | ['/usr/local/mock/docker']="_pull my/repository:latest" # errors when pulled 294 | ['/usr/local/mock/date']="197001010101" 295 | ) > mockReturns 296 | 297 | run /entrypoint.sh 298 | 299 | expectMockCalledContains "/usr/local/mock/docker pull my/repository:latest 300 | /usr/local/mock/date +%Y%m%d%H%M%S 301 | /usr/local/mock/docker build -t my/repository:latest -t my/repository:19700101010112169e . 302 | /usr/local/mock/docker push my/repository:latest 303 | /usr/local/mock/docker push my/repository:19700101010112169e" 304 | } 305 | 306 | @test "it pushes branch by sha and date with specific Dockerfile" { 307 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 308 | export INPUT_SNAPSHOT='true' 309 | export INPUT_DOCKERFILE='MyDockerFileName' 310 | 311 | declare -A -p MOCK_RETURNS=( 312 | ['/usr/local/mock/docker']="" 313 | ['/usr/local/mock/date']="197001010101" 314 | ) > mockReturns 315 | 316 | run /entrypoint.sh 317 | 318 | expectMockCalledContains " 319 | /usr/local/mock/date +%Y%m%d%H%M%S 320 | /usr/local/mock/docker build -f MyDockerFileName -t my/repository:latest -t my/repository:19700101010112169e . 321 | /usr/local/mock/docker push my/repository:latest 322 | /usr/local/mock/docker push my/repository:19700101010112169e" 323 | } 324 | 325 | @test "it caches image from former build and uses it for snapshot with specific Dockerfile" { 326 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 327 | export INPUT_SNAPSHOT='true' 328 | export INPUT_CACHE='true' 329 | export INPUT_DOCKERFILE='MyDockerFileName' 330 | 331 | declare -A -p MOCK_RETURNS=( 332 | ['/usr/local/mock/docker']="" 333 | ['/usr/local/mock/date']="197001010101" 334 | ) > mockReturns 335 | 336 | run /entrypoint.sh 337 | 338 | expectMockCalledContains "/usr/local/mock/docker pull my/repository:latest 339 | /usr/local/mock/date +%Y%m%d%H%M%S 340 | /usr/local/mock/docker build -f MyDockerFileName --cache-from my/repository:latest -t my/repository:latest -t my/repository:19700101010112169e . 341 | /usr/local/mock/docker push my/repository:latest 342 | /usr/local/mock/docker push my/repository:19700101010112169e" 343 | } 344 | 345 | @test "it pushes to another registry and adds the hostname" { 346 | export INPUT_REGISTRY='my.Registry.io' 347 | 348 | run /entrypoint.sh 349 | 350 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin my.registry.io 351 | /usr/local/mock/docker build -t my.registry.io/my/repository:latest . 352 | /usr/local/mock/docker push my.registry.io/my/repository:latest 353 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my.registry.io/my/repository:latest 354 | /usr/local/mock/docker logout" 355 | } 356 | 357 | @test "it pushes to another registry and is ok when the hostname is already present" { 358 | export INPUT_REGISTRY='my.Registry.io' 359 | export INPUT_NAME='my.Registry.io/my/repository' 360 | 361 | run /entrypoint.sh 362 | 363 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin my.registry.io 364 | /usr/local/mock/docker build -t my.registry.io/my/repository:latest . 365 | /usr/local/mock/docker push my.registry.io/my/repository:latest 366 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my.registry.io/my/repository:latest 367 | /usr/local/mock/docker logout" 368 | } 369 | 370 | @test "it pushes to another registry and removes the protocol from the hostname" { 371 | export INPUT_REGISTRY='https://my.Registry.io' 372 | export INPUT_NAME='my/repository' 373 | 374 | run /entrypoint.sh 375 | 376 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin https://my.registry.io 377 | /usr/local/mock/docker build -t my.registry.io/my/repository:latest . 378 | /usr/local/mock/docker push my.registry.io/my/repository:latest 379 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my.registry.io/my/repository:latest 380 | /usr/local/mock/docker logout" 381 | } 382 | 383 | @test "it caches the image from a former build" { 384 | export INPUT_CACHE='true' 385 | 386 | run /entrypoint.sh 387 | 388 | expectMockCalledContains "/usr/local/mock/docker pull my/repository:latest 389 | /usr/local/mock/docker build --cache-from my/repository:latest -t my/repository:latest . 390 | /usr/local/mock/docker push my/repository:latest" 391 | } 392 | 393 | @test "it does not cache the image from a former build if set to false" { 394 | export INPUT_CACHE='false' 395 | 396 | run /entrypoint.sh 397 | 398 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest . 399 | /usr/local/mock/docker push my/repository:latest" 400 | } 401 | 402 | @test "it pushes pull requests when configured" { 403 | export GITHUB_REF='refs/pull/24/merge' 404 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 405 | export INPUT_PULL_REQUESTS='true' 406 | 407 | run /entrypoint.sh 408 | 409 | expectGitHubOutputContains "tag=12169ed809255604e557a82617264e9c373faca7" 410 | 411 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:12169ed809255604e557a82617264e9c373faca7 . 412 | /usr/local/mock/docker push my/repository:12169ed809255604e557a82617264e9c373faca7" 413 | } 414 | 415 | @test "it pushes to the tag if configured in the name" { 416 | export INPUT_NAME='my/repository:custom-tag' 417 | 418 | run /entrypoint.sh 419 | 420 | expectGitHubOutputContains "tag=custom-tag" 421 | 422 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:custom-tag . 423 | /usr/local/mock/docker push my/repository:custom-tag" 424 | } 425 | 426 | @test "it uses buildargs for building, if configured" { 427 | export INPUT_BUILDARGS='MY_FIRST,MY_SECOND' 428 | 429 | run /entrypoint.sh 430 | 431 | expectStdOutContains " 432 | ::add-mask::MY_FIRST 433 | ::add-mask::MY_SECOND" 434 | expectGitHubOutputContains "tag=latest" 435 | 436 | expectMockCalledContains "/usr/local/mock/docker build --build-arg MY_FIRST --build-arg MY_SECOND -t my/repository:latest ." 437 | } 438 | 439 | @test "it uses buildargs for a single variable" { 440 | export INPUT_BUILDARGS='MY_ONLY' 441 | 442 | run /entrypoint.sh 443 | 444 | expectStdOutContains " 445 | ::add-mask::MY_ONLY" 446 | expectGitHubOutputContains "tag=latest" 447 | 448 | expectMockCalledContains "/usr/local/mock/docker build --build-arg MY_ONLY -t my/repository:latest ." 449 | } 450 | 451 | @test "it errors when with.name was not set" { 452 | unset INPUT_NAME 453 | 454 | run /entrypoint.sh 455 | 456 | local expected="Unable to find the name. Did you set with.name?" 457 | echo $output 458 | [ "$status" -eq 1 ] 459 | echo "$output" | grep "$expected" 460 | } 461 | 462 | @test "it errors when with.username was not set" { 463 | unset INPUT_USERNAME 464 | 465 | run /entrypoint.sh 466 | 467 | local expected="Unable to find the username. Did you set with.username?" 468 | echo $output 469 | [ "$status" -eq 1 ] 470 | echo "$output" | grep "$expected" 471 | } 472 | 473 | @test "it errors when with.password was not set" { 474 | unset INPUT_PASSWORD 475 | 476 | run /entrypoint.sh 477 | 478 | local expected="Unable to find the password. Did you set with.password?" 479 | echo $output 480 | [ "$status" -eq 1 ] 481 | echo "$output" | grep "$expected" 482 | } 483 | 484 | @test "it errors when the working directory is configured but not present" { 485 | export INPUT_WORKDIR='mySubDir' 486 | 487 | run /entrypoint.sh 488 | 489 | [ "$status" -eq 2 ] 490 | } 491 | 492 | @test "it can set a custom context" { 493 | export GITHUB_REF='refs/heads/master' 494 | export INPUT_CONTEXT='/myContextFolder' 495 | 496 | run /entrypoint.sh 497 | 498 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest /myContextFolder" 499 | } 500 | 501 | @test "it can set a custom context when building snapshot" { 502 | export GITHUB_REF='refs/heads/master' 503 | export INPUT_CONTEXT='/myContextFolder' 504 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 505 | export INPUT_SNAPSHOT='true' 506 | 507 | declare -A -p MOCK_RETURNS=( 508 | ['/usr/local/mock/docker']="" 509 | ['/usr/local/mock/date']="197001010101" 510 | ) > mockReturns 511 | 512 | run /entrypoint.sh 513 | 514 | expectMockCalledContains "/usr/local/mock/docker build -t my/repository:latest -t my/repository:19700101010112169e /myContextFolder" 515 | } 516 | 517 | @test "it populates the digest" { 518 | export GITHUB_REF='refs/heads/master' 519 | 520 | declare -A -p MOCK_RETURNS=( 521 | ['/usr/local/mock/docker inspect']="my/repository@sha256:53b76152042486bc741fe59f130bfe683b883060c8284271a2586342f35dcd0e" 522 | ['/usr/local/mock/date']="197001010101" 523 | ) > mockReturns 524 | 525 | run /entrypoint.sh 526 | 527 | expectGitHubOutputContains "digest=my/repository@sha256:53b76152042486bc741fe59f130bfe683b883060c8284271a2586342f35dcd0e" 528 | 529 | expectMockCalledContains "/usr/local/mock/docker push my/repository:latest 530 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:latest" 531 | } 532 | 533 | @test "it uses buildoptions for building, if configured" { 534 | export INPUT_BUILDOPTIONS='--compress --force-rm' 535 | 536 | run /entrypoint.sh 537 | 538 | expectMockCalledContains "/usr/local/mock/docker build --compress --force-rm -t my/repository:latest ." 539 | } 540 | 541 | @test "it uses buildoptions for building with snapshot, if configured" { 542 | export INPUT_BUILDOPTIONS='--compress --force-rm' 543 | export INPUT_SNAPSHOT='true' 544 | 545 | export GITHUB_SHA='12169ed809255604e557a82617264e9c373faca7' 546 | 547 | declare -A -p MOCK_RETURNS=( 548 | ['/usr/local/mock/docker']="" 549 | ['/usr/local/mock/date']="197001010101" 550 | ) > mockReturns 551 | 552 | run /entrypoint.sh 553 | 554 | expectMockCalledContains "/usr/local/mock/docker build --compress --force-rm -t my/repository:latest -t my/repository:19700101010112169e ." 555 | } 556 | 557 | @test "it provides a possibility to define multiple tags" { 558 | export GITHUB_REF='refs/heads/master' 559 | export INPUT_TAGS='A,B,C' 560 | 561 | run /entrypoint.sh 562 | 563 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 564 | /usr/local/mock/docker build -t my/repository:A -t my/repository:B -t my/repository:C . 565 | /usr/local/mock/docker push my/repository:A 566 | /usr/local/mock/docker push my/repository:B 567 | /usr/local/mock/docker push my/repository:C 568 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:A 569 | /usr/local/mock/docker logout" 570 | } 571 | 572 | @test "it provides a possibility to define one tag" { 573 | export GITHUB_REF='refs/heads/master' 574 | export INPUT_TAGS='A' 575 | 576 | run /entrypoint.sh 577 | 578 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 579 | /usr/local/mock/docker build -t my/repository:A . 580 | /usr/local/mock/docker push my/repository:A 581 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:A 582 | /usr/local/mock/docker logout" 583 | } 584 | 585 | @test "it caches the first image when multiple tags defined" { 586 | export GITHUB_REF='refs/heads/master' 587 | export INPUT_TAGS='A,B' 588 | export INPUT_CACHE='true' 589 | 590 | run /entrypoint.sh 591 | 592 | expectMockCalledContains "/usr/local/mock/docker pull my/repository:A 593 | /usr/local/mock/docker build --cache-from my/repository:A -t my/repository:A -t my/repository:B . 594 | /usr/local/mock/docker push my/repository:A 595 | /usr/local/mock/docker push my/repository:B" 596 | } 597 | 598 | @test "it is ok with complexer passwords" { 599 | export GITHUB_REF='refs/heads/master' 600 | export INPUT_PASSWORD='9eL89n92G@!#o^$!&3Nz89F@%9' 601 | 602 | run /entrypoint.sh 603 | 604 | expectMockArgs '/usr/local/mock/docker 9eL89n92G@!#o^$!&3Nz89F@%9' 605 | } 606 | 607 | @test "it can be used for building only" { 608 | export GITHUB_REF='refs/heads/master' 609 | export INPUT_NO_PUSH='true' 610 | 611 | run /entrypoint.sh 612 | 613 | expectStdOutIs "" 614 | 615 | expectMockCalledIs "/usr/local/mock/docker login -u USERNAME --password-stdin 616 | /usr/local/mock/docker build -t my/repository:latest . 617 | /usr/local/mock/docker logout" 618 | } 619 | 620 | @test "it can be used for building without login" { 621 | export GITHUB_REF='refs/heads/master' 622 | export INPUT_NO_PUSH='true' 623 | export INPUT_USERNAME='' 624 | 625 | run /entrypoint.sh 626 | 627 | expectStdOutIs "" 628 | 629 | expectMockCalledIs "/usr/local/mock/docker build -t my/repository:latest ." 630 | } 631 | 632 | @test "it can change the default branch" { 633 | export GITHUB_REF='refs/heads/trunk' 634 | export INPUT_DEFAULT_BRANCH='trunk' 635 | 636 | run /entrypoint.sh 637 | 638 | expectGitHubOutputContains "tag=latest" 639 | 640 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 641 | /usr/local/mock/docker build -t my/repository:latest . 642 | /usr/local/mock/docker push my/repository:latest 643 | /usr/local/mock/docker inspect --format={{index .RepoDigests 0}} my/repository:latest 644 | /usr/local/mock/docker logout" 645 | } 646 | 647 | @test "it supports building multiple platforms" { 648 | export GITHUB_REF='refs/heads/main' 649 | export INPUT_PLATFORMS='linux/amd64,linux/arm64' 650 | 651 | cat <> metadata.json 652 | { 653 | "containerimage.buildinfo/linux/amd64": { 654 | "frontend": "dockerfile.v0", 655 | "attrs": { 656 | "filename": "Dockerfile" 657 | }, 658 | "sources": [ 659 | { 660 | "type": "docker-image", 661 | "ref": "docker.io/library/alpine:latest", 662 | "pin": "sha256:7580ece7963bfa863801466c0a488f11c86f85d9988051a9f9c68cb27f6b7872" 663 | } 664 | ] 665 | }, 666 | "containerimage.buildinfo/linux/arm64": { 667 | "frontend": "dockerfile.v0", 668 | "attrs": { 669 | "filename": "Dockerfile" 670 | }, 671 | "sources": [ 672 | { 673 | "type": "docker-image", 674 | "ref": "docker.io/library/alpine:latest", 675 | "pin": "sha256:7580ece7963bfa863801466c0a488f11c86f85d9988051a9f9c68cb27f6b7872" 676 | } 677 | ] 678 | }, 679 | "containerimage.descriptor": { 680 | "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", 681 | "digest": "sha256:aa2c7631cc1bbf588192ec7e55b428ad92fe63834200303f28e93444d7fc114a", 682 | "size": 741 683 | }, 684 | "containerimage.digest": "sha256:aa2c7631cc1bbf588192ec7e55b428ad92fe63834200303f28e93444d7fc114a", 685 | "image.name": "my/repository:latest" 686 | } 687 | EOT 688 | 689 | run /entrypoint.sh 690 | 691 | expectGitHubOutputContains "tag=latestdigest=sha256:aa2c7631cc1bbf588192ec7e55b428ad92fe63834200303f28e93444d7fc114a" 692 | 693 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 694 | /usr/local/mock/docker buildx build --push --metadata-file metadata.json --platform linux/amd64,linux/arm64 -t my/repository:latest . 695 | /usr/local/mock/docker logout" 696 | expectMockArgs "" 697 | } 698 | 699 | @test "it respects no_push when building multiple platforms" { 700 | export GITHUB_REF='refs/heads/main' 701 | export INPUT_PLATFORMS='linux/amd64,linux/arm64' 702 | export INPUT_NO_PUSH='true' 703 | 704 | cat <> metadata.json 705 | { 706 | "containerimage.buildinfo/linux/amd64": { 707 | "frontend": "dockerfile.v0", 708 | "attrs": { 709 | "filename": "Dockerfile" 710 | }, 711 | "sources": [ 712 | { 713 | "type": "docker-image", 714 | "ref": "docker.io/library/alpine:latest", 715 | "pin": "sha256:7580ece7963bfa863801466c0a488f11c86f85d9988051a9f9c68cb27f6b7872" 716 | } 717 | ] 718 | }, 719 | "containerimage.buildinfo/linux/arm64": { 720 | "frontend": "dockerfile.v0", 721 | "attrs": { 722 | "filename": "Dockerfile" 723 | }, 724 | "sources": [ 725 | { 726 | "type": "docker-image", 727 | "ref": "docker.io/library/alpine:latest", 728 | "pin": "sha256:7580ece7963bfa863801466c0a488f11c86f85d9988051a9f9c68cb27f6b7872" 729 | } 730 | ] 731 | }, 732 | "containerimage.descriptor": { 733 | "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", 734 | "digest": "sha256:aa2c7631cc1bbf588192ec7e55b428ad92fe63834200303f28e93444d7fc114a", 735 | "size": 741 736 | }, 737 | "containerimage.digest": "sha256:aa2c7631cc1bbf588192ec7e55b428ad92fe63834200303f28e93444d7fc114a", 738 | "image.name": "my/repository:latest" 739 | } 740 | EOT 741 | 742 | run /entrypoint.sh 743 | 744 | expectMockCalledContains "/usr/local/mock/docker login -u USERNAME --password-stdin 745 | /usr/local/mock/docker buildx build --metadata-file metadata.json --platform linux/amd64,linux/arm64 -t my/repository:latest . 746 | /usr/local/mock/docker logout" 747 | expectMockArgs "" 748 | } 749 | 750 | expectStdOutIs() { 751 | local expected=$(echo "${1}" | tr -d '\n') 752 | local got=$(echo "${output}" | tr -d '\n') 753 | echo "Expected: |${expected}| 754 | Got: |${got}|" 755 | [ "${got}" == "${expected}" ] 756 | } 757 | 758 | expectStdOutContains() { 759 | local expected=$(echo "${1}" | tr -d '\n') 760 | local got=$(echo "${output}" | tr -d '\n') 761 | echo "Expected: |${expected}| 762 | Got: |${got}|" 763 | echo "${got}" | grep "${expected}" 764 | } 765 | 766 | expectGitHubOutputContains() { 767 | local expected=$(echo "${1}" | tr -d '\n') 768 | local got=$(cat "${GITHUB_OUTPUT}" | tr -d '\n') 769 | echo "Expected: |${expected}| 770 | Got: |${got}|" 771 | echo "${got}" | grep "${expected}" 772 | } 773 | 774 | expectMockCalledIs() { 775 | local expected=$(echo "${1}" | tr -d '\n') 776 | local got=$(cat mockArgs | tr -d '\n') 777 | echo "Expected: |${expected}| 778 | Got: |${got}|" 779 | [ "${got}" == "${expected}" ] 780 | } 781 | 782 | expectMockCalledContains() { 783 | local expected=$(echo "${1}" | tr -d '\n') 784 | local got=$(cat mockArgs | tr -d '\n') 785 | echo "Expected: |${expected}| 786 | Got: |${got}|" 787 | echo "${got}" | grep "${expected}" 788 | } 789 | 790 | expectMockArgs() { 791 | local expected=$(echo "${1}" | tr -d '\n') 792 | local got=$(cat mockStdin | tr -d '\n') 793 | echo "Expected: |${expected}| 794 | Got: |${got}|" 795 | echo "${got}" | grep "${expected}" 796 | } 797 | --------------------------------------------------------------------------------