From b2ce422a28b39bb1c3e99c18a3a127632940fd85 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Thu, 18 Jun 2026 16:21:29 +0200 Subject: [PATCH 1/5] feat(docker-publish): add reusable chunked docker publish workflow Add a reusable workflow that builds a container image and pushes it to the Harbor registry using chunked blob uploads via regctl, avoiding 413/504 failures when a layer exceeds a proxy's request-body limit (e.g. Cloudflare's 100 MB). Plain docker/buildx push cannot chunk a blob, so the image is built to an OCI archive and pushed with regctl (--blob-chunk / --blob-max). Optionally signs the manifest digest with cosign. Generic inputs (image-name, version, context, optional Gradle build-command, tags, chunk size, sign toggle) make it consumable by any OneLiteFeather project. Documented in the README with usage examples and required secrets. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docker-publish.yml | 232 +++++++++++++++++++++++++++ README.md | 56 +++++++ 2 files changed, 288 insertions(+) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..3f4e79c --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,232 @@ +name: Reusable - Docker Publish (chunked) + +# Builds a container image and pushes it to the OneLiteFeather Harbor registry +# using chunked blob uploads via regctl. This avoids the "request entity too +# large" / 504 gateway timeout failures that occur when a single image layer +# exceeds the request-body limit of a proxy in front of the registry (e.g. the +# 100 MB Cloudflare limit): blobs larger than `blob-chunk` are split into +# multiple smaller PATCH requests instead of one monolithic PUT. + +on: + workflow_call: + inputs: + image-name: + description: "Image repository path inside the registry, e.g. 'otis/otis' (the registry host comes from the HARBOR_REGISTRY secret)" + required: true + type: string + version: + description: "Version/tag of the image, e.g. '1.13.1' (without a leading 'v')" + required: true + type: string + context: + description: "Docker build context directory" + required: false + type: string + default: "." + dockerfile: + description: "Path to the Dockerfile (defaults to /Dockerfile)" + required: false + type: string + default: "" + build-command: + description: "Optional shell command run before the docker build to produce the context (e.g. a Gradle task). The version is available as $VERSION." + required: false + type: string + default: "" + setup-java: + description: "Set up JDK + Gradle before running build-command (for Gradle-produced contexts)" + required: false + type: boolean + default: false + java-version: + description: "JDK version to use when setup-java is true" + required: false + type: string + default: "25" + java-distribution: + description: "JDK distribution (temurin, zulu, ...)" + required: false + type: string + default: "temurin" + extra-tags: + description: "Additional docker/metadata-action tag lines appended to the default semver + sha tags" + required: false + type: string + default: "" + blob-chunk: + description: "Chunk size in bytes for blob uploads above which chunked push is used (default 50 MiB, safely below a 100 MB proxy limit)" + required: false + type: string + default: "52428800" + sign: + description: "Sign the pushed image with cosign (requires COSIGN_KEY / COSIGN_PASSWORD secrets)" + required: false + type: boolean + default: true + regctl-version: + description: "regctl release tag to install" + required: false + type: string + default: "v0.8.2" + runs-on: + description: "Runner image" + required: false + type: string + default: "ubuntu-latest" + secrets: + HARBOR_REGISTRY: + required: true + HARBOR_USERNAME: + required: true + HARBOR_PASSWORD: + required: true + COSIGN_KEY: + required: false + COSIGN_PASSWORD: + required: false + outputs: + image: + description: "Fully qualified image reference (registry/image-name)" + value: ${{ jobs.docker.outputs.image }} + digest: + description: "Manifest digest of the pushed image" + value: ${{ jobs.docker.outputs.digest }} + +concurrency: + group: docker-publish-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + docker: + name: Build & push (chunked) + runs-on: ${{ inputs.runs-on }} + outputs: + image: ${{ steps.vars.outputs.image }} + digest: ${{ steps.push.outputs.digest }} + env: + VERSION: ${{ inputs.version }} + BLOB_CHUNK: ${{ inputs.blob-chunk }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Resolve image reference + id: vars + shell: bash + env: + HARBOR_REGISTRY: ${{ secrets.HARBOR_REGISTRY }} + IMAGE_NAME: ${{ inputs.image-name }} + run: | + echo "image=${HARBOR_REGISTRY}/${IMAGE_NAME}" >> "$GITHUB_OUTPUT" + + - name: Validate Gradle wrapper + if: ${{ inputs.setup-java }} + uses: gradle/actions/wrapper-validation@v6 + + - name: Set up JDK ${{ inputs.java-version }} + if: ${{ inputs.setup-java }} + uses: actions/setup-java@v5 + with: + distribution: ${{ inputs.java-distribution }} + java-version: ${{ inputs.java-version }} + + - name: Setup Gradle + if: ${{ inputs.setup-java }} + uses: gradle/actions/setup-gradle@v6 + + - name: Build context + if: ${{ inputs.build-command != '' }} + shell: bash + run: ${{ inputs.build-command }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v6 + with: + images: | + ${{ steps.vars.outputs.image }} + tags: | + type=semver,pattern={{version}},value=${{ inputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ inputs.version }} + type=semver,pattern={{major}},value=${{ inputs.version }} + type=sha + ${{ inputs.extra-tags }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build image to OCI archive + uses: docker/build-push-action@v7 + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile != '' && inputs.dockerfile || format('{0}/Dockerfile', inputs.context) }} + push: false + provenance: false + sbom: false + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.vars.outputs.image }}:${{ inputs.version }} + outputs: type=oci,dest=${{ runner.temp }}/image.tar + + - name: Install regctl + shell: bash + run: | + curl -fsSL "https://github.com/regclient/regclient/releases/download/${{ inputs.regctl-version }}/regctl-linux-amd64" \ + -o "${RUNNER_TEMP}/regctl" + chmod +x "${RUNNER_TEMP}/regctl" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + + - name: Configure chunked uploads and login + shell: bash + env: + HARBOR_REGISTRY: ${{ secrets.HARBOR_REGISTRY }} + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + run: | + regctl registry set --blob-chunk "${BLOB_CHUNK}" --blob-max "${BLOB_CHUNK}" "${HARBOR_REGISTRY}" + printf '%s' "${HARBOR_PASSWORD}" | \ + regctl registry login "${HARBOR_REGISTRY}" -u "${HARBOR_USERNAME}" --pass-stdin + + - name: Push image (chunked) + id: push + shell: bash + env: + IMAGE: ${{ steps.vars.outputs.image }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + # Push the primary tag with chunked blob uploads, then add the + # remaining tags via in-registry copy (cross-repo blob mount = tiny requests). + regctl image import "${IMAGE}:${VERSION}" "${RUNNER_TEMP}/image.tar" + while IFS= read -r tag; do + [ -z "$tag" ] && continue + [ "$tag" = "${IMAGE}:${VERSION}" ] && continue + regctl image copy "${IMAGE}:${VERSION}" "$tag" + done <<< "${TAGS}" + DIGEST="$(regctl image digest "${IMAGE}:${VERSION}")" + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + + - name: Install Cosign + if: ${{ inputs.sign }} + uses: sigstore/cosign-installer@v4.1.2 + + - name: Sign image + if: ${{ inputs.sign }} + shell: bash + env: + IMAGE: ${{ steps.vars.outputs.image }} + DIGEST: ${{ steps.push.outputs.digest }} + COSIGN_USERNAME: ${{ secrets.HARBOR_USERNAME }} + COSIGN_PASSWORD_REG: ${{ secrets.HARBOR_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + run: | + # All tags share one manifest digest, so a single signature covers them all. + cosign sign --yes \ + --registry-username="${COSIGN_USERNAME}" \ + --registry-password="${COSIGN_PASSWORD_REG}" \ + --key env://COSIGN_PRIVATE_KEY \ + "${IMAGE}@${DIGEST}" diff --git a/README.md b/README.md index 47bb278..2c18dbb 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ repositories by referencing a tagged release of this repo. | --- | --- | | `.github/workflows/gradle-build-pr.yml` | Build & test a Gradle project on pull requests across a runner matrix. Skips when no Gradle-relevant files changed; aggregates JUnit results across the matrix; auto-enables verbose logging on debug re-runs. | | `.github/workflows/gradle-publish.yml` | Build & publish a Gradle project to the OneLiteFeather Maven repository on tag pushes. | +| `.github/workflows/docker-publish.yml` | Build a container image and push it to the OneLiteFeather Harbor registry using **chunked blob uploads** (via [`regctl`](https://regclient.org/)) so no single request exceeds the proxy body limit; optionally signs the image with cosign. | | `.github/workflows/release-please.yml` | Run [release-please](https://github.com/googleapis/release-please) for a repository. | | `.github/workflows/close-invalid-prs.yml` | Close PRs opened from a fork's default branch with a configurable message. | | `.github/workflows/markdown-lint.yml` | Lint Markdown files with [`markdownlint-cli2`](https://github.com/DavidAnson/markdownlint-cli2-action) and check links with [`lychee`](https://github.com/lycheeverse/lychee-action). | @@ -106,6 +107,53 @@ jobs: secrets: inherit ``` +### Publish a Docker image (chunked upload) + +Pushes the image one blob at a time in chunks below the proxy body limit, so +large layers no longer fail with `413 Request Entity Too Large` / `504 Gateway +Timeout` behind a proxy such as Cloudflare (100 MB limit). Plain `docker push` +/ `buildx` cannot chunk a blob; this workflow builds the image to an OCI archive +and pushes it with [`regctl`](https://regclient.org/) (`--blob-chunk` / +`--blob-max`) instead. + +Typical use is from a release job, gated on `release_created`: + +```yaml +jobs: + docker: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + uses: OneLiteFeatherNET/workflows/.github/workflows/docker-publish.yml@v2 + with: + image-name: "otis/otis" # registry host comes from HARBOR_REGISTRY + version: ${{ needs.release-please.outputs.version }} + # Build the container context with Gradle first ($VERSION is exported): + setup-java: true + build-command: "./gradlew jar optimizedBuildLayers optimizedDockerfile -Pversion=$VERSION" + context: "./backend/build/docker/optimized" + secrets: inherit +``` + +For a project with a plain `Dockerfile` checked into the repo, drop the Gradle +inputs: + +```yaml +jobs: + docker: + uses: OneLiteFeatherNET/workflows/.github/workflows/docker-publish.yml@v2 + with: + image-name: "myteam/myapp" + version: "1.2.3" + context: "." + sign: false # skip cosign if no signing key + secrets: inherit +``` + +Default tags are `{{version}}`, `{{major}}.{{minor}}`, `{{major}}` and a +`sha-` tag; add more via `extra-tags`. Tune the chunk size with `blob-chunk` +(bytes, default 50 MiB). The pushed manifest `digest` and full `image` +reference are exposed as workflow outputs. + ### release-please ```yaml @@ -168,6 +216,14 @@ these secrets to be available in the caller repository (and forwarded via - `ONELITEFEATHER_MAVEN_USERNAME` - `ONELITEFEATHER_MAVEN_PASSWORD` +`docker-publish` pushes to the Harbor registry and (optionally) signs with +cosign, so it expects: + +- `HARBOR_REGISTRY` — registry host (no scheme), e.g. `harbor.onelitefeather.dev` +- `HARBOR_USERNAME` +- `HARBOR_PASSWORD` +- `COSIGN_KEY` / `COSIGN_PASSWORD` — only when `sign: true` (the default) + ## Test results For `gradle-build-pr`, JUnit XML from every matrix job is uploaded as From bf3c74498d0fe1b320fc35a2843321f6753bb6da Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Thu, 18 Jun 2026 16:33:49 +0200 Subject: [PATCH 2/5] fix(docker-publish): authenticate via docker login instead of regctl ping regctl registry login performs a verification ping that fails with "unauthorized" against Harbor even when the credentials are valid. Use the proven docker/login-action to write ~/.docker/config.json (which regctl reuses for auth) and have regctl only attach the blob-chunk host config with --skip-check, so no separate regctl auth ping runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docker-publish.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 3f4e79c..2c998ca 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -180,16 +180,21 @@ jobs: chmod +x "${RUNNER_TEMP}/regctl" echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" - - name: Configure chunked uploads and login + - name: Log in to Harbor + uses: docker/login-action@v4 + with: + registry: ${{ secrets.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Configure chunked uploads shell: bash env: HARBOR_REGISTRY: ${{ secrets.HARBOR_REGISTRY }} - HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} - HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} run: | - regctl registry set --blob-chunk "${BLOB_CHUNK}" --blob-max "${BLOB_CHUNK}" "${HARBOR_REGISTRY}" - printf '%s' "${HARBOR_PASSWORD}" | \ - regctl registry login "${HARBOR_REGISTRY}" -u "${HARBOR_USERNAME}" --pass-stdin + # regctl reuses the credentials written by docker login (~/.docker/config.json); + # here we only attach the blob-chunk config. --skip-check avoids an auth ping. + regctl registry set --skip-check --blob-chunk "${BLOB_CHUNK}" --blob-max "${BLOB_CHUNK}" "${HARBOR_REGISTRY}" - name: Push image (chunked) id: push From bf895f05739ef0aee0b5157286682ef30c6a0bf3 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Thu, 18 Jun 2026 16:44:37 +0200 Subject: [PATCH 3/5] fix(docker-publish): use regctl login with --skip-check, not docker login Harbor is fronted by a Cloudflare tunnel that blocks `docker login` (403) but allows regctl (the intended chunked client). Authenticate with regctl and pass --skip-check so its anonymous connectivity ping (answered with 401 by a private registry) does not fail the step; credentials are exercised during the push. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docker-publish.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2c998ca..d97d1bb 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -180,21 +180,19 @@ jobs: chmod +x "${RUNNER_TEMP}/regctl" echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" - - name: Log in to Harbor - uses: docker/login-action@v4 - with: - registry: ${{ secrets.HARBOR_REGISTRY }} - username: ${{ secrets.HARBOR_USERNAME }} - password: ${{ secrets.HARBOR_PASSWORD }} - - - name: Configure chunked uploads + - name: Configure chunked uploads and login shell: bash env: HARBOR_REGISTRY: ${{ secrets.HARBOR_REGISTRY }} + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} run: | - # regctl reuses the credentials written by docker login (~/.docker/config.json); - # here we only attach the blob-chunk config. --skip-check avoids an auth ping. + # regctl is the registry client (it chunks blob uploads, unlike docker push). + # --skip-check skips its anonymous connectivity ping, which a private + # registry answers with 401; credentials are exercised during the push. regctl registry set --skip-check --blob-chunk "${BLOB_CHUNK}" --blob-max "${BLOB_CHUNK}" "${HARBOR_REGISTRY}" + printf '%s' "${HARBOR_PASSWORD}" | \ + regctl registry login "${HARBOR_REGISTRY}" -u "${HARBOR_USERNAME}" --pass-stdin --skip-check - name: Push image (chunked) id: push From d22e1d2d963c441e19840155b7479854ed400399 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Thu, 18 Jun 2026 17:01:14 +0200 Subject: [PATCH 4/5] feat(docker-publish): add req-concurrent input and larger-chunk guidance Expose req-concurrent (concurrent layer uploads; chunks within a layer stay sequential per the registry protocol) and document that larger blob-chunk values reduce round-trips and storage-backend multipart parts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docker-publish.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index d97d1bb..aad66cf 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -54,10 +54,15 @@ on: type: string default: "" blob-chunk: - description: "Chunk size in bytes for blob uploads above which chunked push is used (default 50 MiB, safely below a 100 MB proxy limit)" + description: "Chunk size in bytes for blob uploads above which chunked push is used (default 50 MiB, safely below a 100 MB proxy limit). Larger chunks = fewer round-trips and fewer storage-backend parts, but stay under the proxy cap." required: false type: string default: "52428800" + req-concurrent: + description: "How many layers (blobs) regctl uploads concurrently. Chunks within a single layer stay sequential. Lower this if the registry's storage backend returns 5xx under load." + required: false + type: string + default: "3" sign: description: "Sign the pushed image with cosign (requires COSIGN_KEY / COSIGN_PASSWORD secrets)" required: false @@ -109,6 +114,7 @@ jobs: env: VERSION: ${{ inputs.version }} BLOB_CHUNK: ${{ inputs.blob-chunk }} + REQ_CONCURRENT: ${{ inputs.req-concurrent }} steps: - name: Checkout uses: actions/checkout@v6 @@ -190,7 +196,7 @@ jobs: # regctl is the registry client (it chunks blob uploads, unlike docker push). # --skip-check skips its anonymous connectivity ping, which a private # registry answers with 401; credentials are exercised during the push. - regctl registry set --skip-check --blob-chunk "${BLOB_CHUNK}" --blob-max "${BLOB_CHUNK}" "${HARBOR_REGISTRY}" + regctl registry set --skip-check --blob-chunk "${BLOB_CHUNK}" --blob-max "${BLOB_CHUNK}" --req-concurrent "${REQ_CONCURRENT}" "${HARBOR_REGISTRY}" printf '%s' "${HARBOR_PASSWORD}" | \ regctl registry login "${HARBOR_REGISTRY}" -u "${HARBOR_USERNAME}" --pass-stdin --skip-check From d947b962caa623edbe2824fb2a00deacee4acc24 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Fri, 19 Jun 2026 15:43:31 +0200 Subject: [PATCH 5/5] feat(docker-publish): keyless cosign signing via GitHub OIDC Align the reusable workflow with the proven Otis pipeline: sign keyless using the GitHub Actions OIDC identity instead of a cosign key/password. Drop the COSIGN_KEY/COSIGN_PASSWORD secrets, add id-token: write, and document that the calling job must grant id-token: write when signing. Bump the default regctl-version to v0.11.5. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docker-publish.yml | 16 ++++++---------- README.md | 28 +++++++++++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index aad66cf..6ed0ea4 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -6,6 +6,7 @@ name: Reusable - Docker Publish (chunked) # exceeds the request-body limit of a proxy in front of the registry (e.g. the # 100 MB Cloudflare limit): blobs larger than `blob-chunk` are split into # multiple smaller PATCH requests instead of one monolithic PUT. +# The pushed image is optionally signed keyless with cosign via GitHub OIDC. on: workflow_call: @@ -64,7 +65,7 @@ on: type: string default: "3" sign: - description: "Sign the pushed image with cosign (requires COSIGN_KEY / COSIGN_PASSWORD secrets)" + description: "Keyless-sign the pushed image with cosign via the GitHub OIDC identity. The calling job must grant `id-token: write`." required: false type: boolean default: true @@ -72,7 +73,7 @@ on: description: "regctl release tag to install" required: false type: string - default: "v0.8.2" + default: "v0.11.5" runs-on: description: "Runner image" required: false @@ -85,10 +86,6 @@ on: required: true HARBOR_PASSWORD: required: true - COSIGN_KEY: - required: false - COSIGN_PASSWORD: - required: false outputs: image: description: "Fully qualified image reference (registry/image-name)" @@ -103,6 +100,7 @@ concurrency: permissions: contents: read + id-token: write # keyless cosign signing (caller must also grant id-token: write) jobs: docker: @@ -222,7 +220,7 @@ jobs: if: ${{ inputs.sign }} uses: sigstore/cosign-installer@v4.1.2 - - name: Sign image + - name: Sign image (keyless) if: ${{ inputs.sign }} shell: bash env: @@ -230,12 +228,10 @@ jobs: DIGEST: ${{ steps.push.outputs.digest }} COSIGN_USERNAME: ${{ secrets.HARBOR_USERNAME }} COSIGN_PASSWORD_REG: ${{ secrets.HARBOR_PASSWORD }} - COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_KEY }} - COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} run: | + # Keyless signing via the GitHub Actions OIDC identity (no key/password). # All tags share one manifest digest, so a single signature covers them all. cosign sign --yes \ --registry-username="${COSIGN_USERNAME}" \ --registry-password="${COSIGN_PASSWORD_REG}" \ - --key env://COSIGN_PRIVATE_KEY \ "${IMAGE}@${DIGEST}" diff --git a/README.md b/README.md index 2c18dbb..616edf6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ repositories by referencing a tagged release of this repo. | --- | --- | | `.github/workflows/gradle-build-pr.yml` | Build & test a Gradle project on pull requests across a runner matrix. Skips when no Gradle-relevant files changed; aggregates JUnit results across the matrix; auto-enables verbose logging on debug re-runs. | | `.github/workflows/gradle-publish.yml` | Build & publish a Gradle project to the OneLiteFeather Maven repository on tag pushes. | -| `.github/workflows/docker-publish.yml` | Build a container image and push it to the OneLiteFeather Harbor registry using **chunked blob uploads** (via [`regctl`](https://regclient.org/)) so no single request exceeds the proxy body limit; optionally signs the image with cosign. | +| `.github/workflows/docker-publish.yml` | Build a container image and push it to the OneLiteFeather Harbor registry using **chunked blob uploads** (via [`regctl`](https://regclient.org/)) so no single request exceeds the proxy body limit; optionally **keyless-signs** the image with cosign (GitHub OIDC). | | `.github/workflows/release-please.yml` | Run [release-please](https://github.com/googleapis/release-please) for a repository. | | `.github/workflows/close-invalid-prs.yml` | Close PRs opened from a fork's default branch with a configurable message. | | `.github/workflows/markdown-lint.yml` | Lint Markdown files with [`markdownlint-cli2`](https://github.com/DavidAnson/markdownlint-cli2-action) and check links with [`lychee`](https://github.com/lycheeverse/lychee-action). | @@ -118,11 +118,17 @@ and pushes it with [`regctl`](https://regclient.org/) (`--blob-chunk` / Typical use is from a release job, gated on `release_created`: +Typical use is from a release job, gated on `release_created`. Grant +`id-token: write` on the calling job so cosign can sign keyless: + ```yaml jobs: docker: needs: release-please if: needs.release-please.outputs.release_created == 'true' + permissions: + contents: read + id-token: write # required for keyless cosign signing uses: OneLiteFeatherNET/workflows/.github/workflows/docker-publish.yml@v2 with: image-name: "otis/otis" # registry host comes from HARBOR_REGISTRY @@ -135,7 +141,7 @@ jobs: ``` For a project with a plain `Dockerfile` checked into the repo, drop the Gradle -inputs: +inputs (and the `permissions` block if you set `sign: false`): ```yaml jobs: @@ -145,14 +151,17 @@ jobs: image-name: "myteam/myapp" version: "1.2.3" context: "." - sign: false # skip cosign if no signing key + sign: false # skip signing (no id-token needed) secrets: inherit ``` Default tags are `{{version}}`, `{{major}}.{{minor}}`, `{{major}}` and a -`sha-` tag; add more via `extra-tags`. Tune the chunk size with `blob-chunk` -(bytes, default 50 MiB). The pushed manifest `digest` and full `image` -reference are exposed as workflow outputs. +`sha-` tag; add more via `extra-tags`. Tune chunking with `blob-chunk` (bytes, +default 50 MiB) and parallel layer uploads with `req-concurrent`. Signing is +keyless via GitHub OIDC — no signing key/secret to manage; verify with the +workflow identity (`--certificate-identity-regexp` + `--certificate-oidc-issuer +https://token.actions.githubusercontent.com`). The pushed manifest `digest` and +full `image` reference are exposed as workflow outputs. ### release-please @@ -216,13 +225,14 @@ these secrets to be available in the caller repository (and forwarded via - `ONELITEFEATHER_MAVEN_USERNAME` - `ONELITEFEATHER_MAVEN_PASSWORD` -`docker-publish` pushes to the Harbor registry and (optionally) signs with -cosign, so it expects: +`docker-publish` pushes to the Harbor registry, so it expects: - `HARBOR_REGISTRY` — registry host (no scheme), e.g. `harbor.onelitefeather.dev` - `HARBOR_USERNAME` - `HARBOR_PASSWORD` -- `COSIGN_KEY` / `COSIGN_PASSWORD` — only when `sign: true` (the default) + +Signing is keyless (cosign + GitHub OIDC) — no signing secrets. The calling job +just needs `permissions: id-token: write` when `sign: true` (the default). ## Test results