├── .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 | [](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 |
285 |
286 |
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 |
--------------------------------------------------------------------------------