| @@ -110,7 +110,8 @@ jobs: |
| 110 | # account, which is also the image owner) and DOCKER_PAT (an access token | 110 | # account, which is also the image owner) and DOCKER_PAT (an access token |
| 111 | # with read/write scope), exposed to the job as environment variables. | 111 | # with read/write scope), exposed to the job as environment variables. |
| 112 | docker-sandbox: | 112 | docker-sandbox: |
| 113 | - name: publish the Docker Sandbox template and kits ${{ github.ref_name }} | 113 | + # No expression in the name: Rickub shows it unexpanded. |
| | 114 | + name: publish the Docker Sandbox template and kits |
| 114 | runs-on: ubuntu-latest | 115 | runs-on: ubuntu-latest |
| 115 | needs: release | 116 | needs: release |
| 116 | env: | 117 | env: |
| @@ -122,13 +123,42 @@ jobs: |
| 122 | - name: Checkout | 123 | - name: Checkout |
| 123 | uses: actions/checkout@v4 | 124 | uses: actions/checkout@v4 |
| 124 | | 125 | |
| 125 | - - name: Check the Docker Hub credentials are set | 126 | + - name: Check the Docker Hub credentials |
| | 127 | + # Secrets pasted with a trailing newline or a stray CR are a classic; |
| | 128 | + # both are stripped before use. The handle must be the Docker ID, not |
| | 129 | + # the e-mail address: access tokens only pair with the username. |
| 126 | run: | | 130 | run: | |
| 127 | set -euo pipefail | 131 | set -euo pipefail |
| 128 | - if [ -z "${DOCKER_HANDLE}" ] || [ -z "${DOCKER_PAT}" ]; then | 132 | + handle="$(printf '%s' "${DOCKER_HANDLE}" | tr -d '\r\n' | sed 's/^ *//; s/ *$//')" |
| | 133 | + pat="$(printf '%s' "${DOCKER_PAT}" | tr -d '\r\n' | sed 's/^ *//; s/ *$//')" |
| | 134 | + if [ -z "${handle}" ] || [ -z "${pat}" ]; then |
| 129 | echo "❌ DOCKER_HANDLE and DOCKER_PAT must be set as repository secrets" | 135 | echo "❌ DOCKER_HANDLE and DOCKER_PAT must be set as repository secrets" |
| 130 | exit 1 | 136 | exit 1 |
| 131 | fi | 137 | fi |
| | 138 | + case "${handle}" in |
| | 139 | + *@*|*:*) echo "❌ DOCKER_HANDLE must be the Docker ID (username), got something with @ or : in it"; exit 1 ;; |
| | 140 | + esac |
| | 141 | + echo "handle: ${handle} (${#handle} chars), token: ${#pat} chars" |
| | 142 | + echo "DOCKER_HANDLE=${handle}" >> "$GITHUB_ENV" |
| | 143 | + echo "::add-mask::${pat}" |
| | 144 | + echo "DOCKER_PAT=${pat}" >> "$GITHUB_ENV" |
| | 145 | + |
| | 146 | + - name: Diagnose the path to Docker Hub |
| | 147 | + # An anonymous GET on the registry must answer 401 with a Bearer |
| | 148 | + # challenge. A 400 "malformed HTTP Authorization header" there means |
| | 149 | + # something between this runner and the Hub (a proxy) adds its own |
| | 150 | + # Authorization header — the request below carries none. The token |
| | 151 | + # endpoint check then says whether the credentials themselves are |
| | 152 | + # accepted, without going through the Docker daemon. |
| | 153 | + run: | |
| | 154 | + set -uo pipefail |
| | 155 | + echo "--- anonymous GET https://registry-1.docker.io/v2/ (expect 401 + www-authenticate: Bearer)" |
| | 156 | + curl -sS -o /dev/null -D - https://registry-1.docker.io/v2/ | grep -i -E '^HTTP/|^www-authenticate|^docker-distribution' || true |
| | 157 | + echo "--- token for ${DOCKER_HANDLE}/mm-sandbox with the access token (expect 200)" |
| | 158 | + code="$(curl -sS -o /dev/null -w '%{http_code}' -u "${DOCKER_HANDLE}:${DOCKER_PAT}" \ |
| | 159 | + "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${DOCKER_HANDLE}/mm-sandbox:pull,push")" |
| | 160 | + echo "auth.docker.io answered ${code}" |
| | 161 | + env | grep -i -E '^(https?_proxy|no_proxy|DOCKER_)' | sed 's/=.*/=…/' || true |
| 132 | | 162 | |
| 133 | - name: Set up QEMU | 163 | - name: Set up QEMU |
| 134 | # The final image stage runs `mm --version` for linux/arm64 on an | 164 | # The final image stage runs `mm --version` for linux/arm64 on an |
| @@ -139,7 +169,21 @@ jobs: |
| 139 | uses: docker/setup-buildx-action@v3 | 169 | uses: docker/setup-buildx-action@v3 |
| 140 | | 170 | |
| 141 | - name: Log in to Docker Hub | 171 | - name: Log in to Docker Hub |
| 142 | - run: printf '%s' "${DOCKER_PAT}" | docker login --username "${DOCKER_HANDLE}" --password-stdin | 172 | + # Not `docker login`: its ping of the registry goes through the Docker |
| | 173 | + # daemon and failed on Rickub with "malformed HTTP Authorization |
| | 174 | + # header". Writing the credential store directly is the same result — |
| | 175 | + # buildx forwards it to BuildKit for the push, and `sbx kit push` |
| | 176 | + # reads it (credentials.NewStoreFromDocker) — without the ping. |
| | 177 | + run: | |
| | 178 | + set -euo pipefail |
| | 179 | + mkdir -p "${HOME}/.docker" |
| | 180 | + auth="$(printf '%s:%s' "${DOCKER_HANDLE}" "${DOCKER_PAT}" | base64 | tr -d '\n')" |
| | 181 | + if [ -f "${HOME}/.docker/config.json" ]; then |
| | 182 | + cp "${HOME}/.docker/config.json" "${RUNNER_TEMP}/docker-config.orig.json" |
| | 183 | + fi |
| | 184 | + printf '{"auths":{"https://index.docker.io/v1/":{"auth":"%s"}}}\n' "${auth}" > "${HOME}/.docker/config.json" |
| | 185 | + chmod 0600 "${HOME}/.docker/config.json" |
| | 186 | + echo "credential store written for ${DOCKER_HANDLE}" |
| 143 | | 187 | |
| 144 | - name: Build and push the template image | 188 | - name: Build and push the template image |
| 145 | run: PUSH=1 IMAGE_OWNER="${DOCKER_HANDLE}" bash ./docker-sbx/build.sh "${GITHUB_REF_NAME}" | 189 | run: PUSH=1 IMAGE_OWNER="${DOCKER_HANDLE}" bash ./docker-sbx/build.sh "${GITHUB_REF_NAME}" |
| @@ -158,6 +202,6 @@ jobs: |
| 158 | - name: Publish the kits | 202 | - name: Publish the kits |
| 159 | run: IMAGE_OWNER="${DOCKER_HANDLE}" SBX="${RUNNER_TEMP}/sbx/sbx" bash ./docker-sbx/push-kits.sh "${GITHUB_REF_NAME}" | 203 | run: IMAGE_OWNER="${DOCKER_HANDLE}" SBX="${RUNNER_TEMP}/sbx/sbx" bash ./docker-sbx/push-kits.sh "${GITHUB_REF_NAME}" |
| 160 | | 204 | |
| 161 | - - name: Log out of Docker Hub | 205 | + - name: Remove the credential store |
| 162 | if: always() | 206 | if: always() |
| 163 | - run: docker logout || true | 207 | + run: rm -f "${HOME}/.docker/config.json" |