From 4fd0971b31ccceeaaedc4ec043b79c45be8ad095 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 27 Apr 2026 14:39:53 -0500 Subject: [PATCH 01/27] feat(github): expand github workflows and actions --- .github/README.md | 85 +++++++++ .github/actions/docker-build-push/action.yml | 175 ++++++++++++++++++ .github/actions/helm-publish-oci/action.yml | 114 ++++++++++++ .../actions/slack-notify-failure/action.yml | 26 +++ .github/workflows/docker-build-push.yml | 125 +++++++++++++ .github/workflows/helm-publish-oci.yml | 62 +++++++ .github/workflows/publish-docker-image.yml | 76 +++----- README.md | 14 +- 8 files changed, 625 insertions(+), 52 deletions(-) create mode 100644 .github/README.md create mode 100644 .github/actions/docker-build-push/action.yml create mode 100644 .github/actions/helm-publish-oci/action.yml create mode 100644 .github/actions/slack-notify-failure/action.yml create mode 100644 .github/workflows/docker-build-push.yml create mode 100644 .github/workflows/helm-publish-oci.yml diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..5095154 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,85 @@ +# Reusable OCI & Docker composites (`FuelLabs/github-actions`) + +**Public** repository. Anyone may **`uses:`** the composites and `workflow_call` workflows. Fuel teams typically pin a **semver ref**; internal policies are **your org’s** (approvals, private registries, etc.). + +This file documents the **OCI / Docker / Helm** composites and their callable workflow wrappers. **[`publish-docker-image.yml`](.github/workflows/publish-docker-image.yml)** is a **legacy** contract (GHCR `GITHUB_CONTAINER_*` secrets, `images` / `docker_file` input names) that **forwards to** [`docker-build-push.yml`](.github/workflows/docker-build-push.yml) in this repo. Prefer **`docker-build-push`** for new ECR OIDC / multi-arch / Warp; keep **`publish-docker-image`** only for existing `uses:` pins. [notify-slack-action.yml](.github/workflows/notify-slack-action.yml) is a separate **reusable** workflow used on failure (including from **`publish-docker-image`**). + +## Artifacts (this “Fuel OCI” set) + +| Kind | Path | Purpose | +|------|------|---------| +| Composite | `.github/actions/docker-build-push` | ECR OIDC or registry login; **Buildx** + QEMU, or **Warp** | +| Composite | `.github/actions/helm-publish-oci` | Non-PR Helm **OCI** publish (lint, push) | +| Composite | `.github/actions/slack-notify-failure` | Small Slack failure step (`ravsamhq/notify-slack-action`) | +| Reusable workflow | `.github/workflows/docker-build-push.yml` | Forwards `runs-on`, `platforms`, `build-backend`, `permissions` | +| Reusable workflow | `.github/workflows/helm-publish-oci.yml` | Same for Helm | +| Reusable workflow (legacy) | `.github/workflows/publish-docker-image.yml` | Same implementation as `docker-build-push` (wraps the row above) + old secret/input names + Slack on failure | + +**Not in scope for these composites:** PR-only Helm, `helm-cleanup-pr`, preview charts. + +## How callable workflows resolve composites + +Callers’ jobs check out the **consumer** repository. A reusable workflow in **this** repo must **not** use `./.github/actions/...` — that path would resolve in the **caller**, not here. The workflows set **`env.GITHUB_ACTIONS_REF`** and use: + +`FuelLabs/github-actions/.github/actions/@${{ env.GITHUB_ACTIONS_REF }}` + +**Releases:** set `GITHUB_ACTIONS_REF` in **both** `docker-build-push.yml` and `helm-publish-oci.yml` to the **same** tag/SHA you publish (e.g. `v1.0.0`); pin consumer `uses: .../docker-build-push.yml@v1.0.0` to match. On the default branch it may be `master` for development. + +## Secrets + +| Mechanism | In composite? | How to pass | +|-----------|----------------|-------------| +| `secrets.*` in `action.yml` | **No** | `with:` from the caller (`password: ${{ secrets.x }}` — still masked) | +| Reusable workflow | **Yes** | `on.workflow_call.secrets`, caller `secrets: inherit` or explicit map | + +`secrets: inherit` on **composite** actions is not supported; use a callable workflow if you want one secrets mapping. + +## Examples + +**Callable** — Docker (pin replaces `v1.0.0` when you release): + +```yaml +jobs: + image: + uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@v1.0.0 + secrets: inherit + with: + runs-on: ubuntu-latest + auth-mode: registry-login + dockerfile: Dockerfile + image: ghcr.io/fuellabs/myapp +``` + +**Callable** — Helm to GHCR (needs `packages: write` in the **called** job — the workflow already sets it; token must be `GITHUB_TOKEN` or a PAT with package write): + +```yaml +jobs: + chart: + uses: FuelLabs/github-actions/.github/workflows/helm-publish-oci.yml@v1.0.0 + secrets: + REGISTRY_USERNAME: ${{ github.actor }} + REGISTRY_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + chart-folder: helm/my-chart + registry-url: oci://ghcr.io/${{ github.repository_owner }}/charts +``` + +**Composite** (consumer writes full `permissions`): + +```yaml +- uses: FuelLabs/github-actions/.github/actions/docker-build-push@v1.0.0 + with: + auth-mode: ecr-oidc + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + image: 123.dkr.ecr.us-east-1.amazonaws.com/app + dockerfile: Dockerfile +``` + +### `slack-notify-failure` vs `notify-slack-action.yml` + +- **`.github/actions/slack-notify-failure`**: **composite** — add as a step, pass `github_token` + `slack_webhook` via `with:`. +- **`.github/workflows/notify-slack-action.yml`**: older **reusable workflow** (checkout, Rust toolchain) — use only if you already depend on it; new work should prefer the **composite** above. + +## Pinning + +Third-party `uses:` in composites are pinned. Bump in PRs. This repo is **not** the same as **Terraform** tags in `infrastructure-tools` — use **`github-actions`’ own** releases. diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml new file mode 100644 index 0000000..4db8fa8 --- /dev/null +++ b/.github/actions/docker-build-push/action.yml @@ -0,0 +1,175 @@ +# Bump third-party majors via PR after review (supply chain). +name: Docker build and push +description: Build and push a container image (ECR OIDC or registry login; Buildx or Warp). + +inputs: + auth-mode: + description: 'ecr-oidc | registry-login' + required: true + aws-role-arn: + description: IAM role ARN for ECR (auth-mode ecr-oidc) + required: false + aws-region: + description: AWS region for ECR + required: false + default: us-east-1 + registry: + description: Registry host (registry-login), e.g. ghcr.io + required: false + default: ghcr.io + username: + description: Registry username (registry-login) + required: false + password: + description: 'Registry password or PAT (registry-login). Caller passes secrets.* in workflow with: — masked in logs.' + required: false + image: + description: Image name for docker/metadata-action (without tag) + required: true + tags: + description: Multiline docker/metadata-action tag rules (empty = ECR-style sha + dated raw tag) + required: false + default: '' + flavor: + description: Optional global metadata / tag flavor (docker/metadata-action), e.g. 'latest=auto' + required: false + default: '' + labels: + description: Optional extra image labels (docker/metadata-action, multiline KEY=VAL) + required: false + default: '' + context: + description: Docker build context path + required: false + default: . + dockerfile: + description: Path to Dockerfile + required: true + build-args: + description: Build-args (multiline KEY=VAL) + required: false + platforms: + description: Comma-separated platforms, e.g. linux/amd64,linux/arm64 + required: false + default: linux/amd64 + build-backend: + description: 'buildx | warp' + required: false + default: buildx + profile-name: + description: Warp profile name (build-backend warp — required for org Warp projects) + required: false + push: + description: 'true | false' + required: false + default: 'true' + +outputs: + image: + description: Image reference (from metadata) + value: ${{ fromJSON(inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata)['image.name'] }} + imageid: + description: Image ID from build backend + value: ${{ inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || steps.publish-buildx.outputs.imageId }} + digest: + description: Image digest + value: ${{ inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || steps.publish-buildx.outputs.digest }} + metadata: + description: Build metadata JSON + value: ${{ inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata }} + +runs: + using: composite + steps: + - name: Configure AWS credentials (ECR OIDC) + if: inputs.auth-mode == 'ecr-oidc' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + + - name: Login to Amazon ECR + if: inputs.auth-mode == 'ecr-oidc' + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Log in to container registry + if: inputs.auth-mode == 'registry-login' + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.username }} + password: ${{ inputs.password }} + + - name: Resolve metadata tag lines + id: tag-lines + shell: bash + env: + # Do not embed inputs in the script body; pass through env to avoid shell/heredoc injection + INPUT_TAGS: ${{ inputs.tags }} + run: | + set -euo pipefail + if [ -r /proc/sys/kernel/random/uuid ]; then + delim="GHA_OUT_$(< /proc/sys/kernel/random/uuid tr -d '-\n\r')" + else + delim="GHA_OUT_${RANDOM}_$$_$(date +%s%N)" + fi + { + echo "tags<<${delim}" + if [ -n "$INPUT_TAGS" ]; then + printf '%s\n' "$INPUT_TAGS" + else + printf '%s\n' 'type=sha,prefix=' + printf '%s\n' "type=raw,value=sha-{{sha}}-{{date 'YYYYMMDDhhmmss'}}" + fi + echo "$delim" + } >> "$GITHUB_OUTPUT" + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ inputs.image }} + tags: ${{ steps.tag-lines.outputs.tags }} + flavor: | + ${{ inputs.flavor }} + labels: | + ${{ inputs.labels }} + + - name: Set up QEMU (Buildx cross-arch) + if: inputs.build-backend == 'buildx' && (contains(inputs.platforms, ',') || contains(inputs.platforms, 'arm')) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + if: inputs.build-backend == 'buildx' + uses: docker/setup-buildx-action@v3 + + - name: Build and push (Buildx) + if: inputs.build-backend == 'buildx' + uses: docker/build-push-action@v6 + id: publish-buildx + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + push: ${{ inputs.push == 'true' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: ${{ inputs.build-args }} + platforms: ${{ inputs.platforms }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push (Warp) + if: inputs.build-backend == 'warp' + uses: Warpbuilds/build-push-action@v6 + id: publish-warp + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + push: ${{ inputs.push == 'true' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: ${{ inputs.build-args }} + platforms: ${{ inputs.platforms }} + profile-name: ${{ inputs.profile-name }} diff --git a/.github/actions/helm-publish-oci/action.yml b/.github/actions/helm-publish-oci/action.yml new file mode 100644 index 0000000..95ff30b --- /dev/null +++ b/.github/actions/helm-publish-oci/action.yml @@ -0,0 +1,114 @@ +# Non-PR Helm OCI publish only (no is-pr / pr-number / Chart.yaml PR mutation). +# Bump third-party majors via PR after review (supply chain). +name: Helm publish (OCI) +description: Lint and push a Helm chart to an OCI registry using Chart.yaml version or an override. + +inputs: + chart-folder: + description: Path to the Helm chart directory + required: true + registry-url: + description: OCI registry URL for the chart (e.g. oci://ghcr.io/org/charts) + required: true + username: + description: Registry username + required: true + access-token: + description: Registry password or token. Caller should set from secrets — masked in logs. + required: true + chart-version: + description: Override chart version (otherwise read from Chart.yaml) + required: false + force: + description: 'true | false — force push' + required: false + default: 'false' + helm-version: + description: Helm CLI version for azure/setup-helm (e.g. v3.14.4 or latest) + required: false + default: v3.14.4 + +outputs: + chart-version: + description: Chart version that was published + value: ${{ steps.version.outputs.version }} + +runs: + using: composite + steps: + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: ${{ inputs.helm-version }} + + - name: Determine chart version + id: version + shell: bash + env: + CHART_FOLDER: ${{ inputs.chart-folder }} + CHART_VERSION_INPUT: ${{ inputs.chart-version }} + run: | + set -euo pipefail + chart_yaml="${CHART_FOLDER}/Chart.yaml" + if [ -n "$CHART_VERSION_INPUT" ]; then + if [[ "$CHART_VERSION_INPUT" == *$'\n'* ]]; then + echo "chart-version must be a single line" >&2 + exit 1 + fi + CHART_VERSION="$CHART_VERSION_INPUT" + else + CHART_VERSION=$(grep '^version:' "$chart_yaml" | head -1 | sed 's/^version:[[:space:]]*//' | tr -d "'\"") + fi + if [[ "$CHART_VERSION" == *$'\n'* || "$CHART_VERSION" == *$'\r'* ]]; then + echo "Chart version value must be a single line" >&2 + exit 1 + fi + # Single-line value only; avoid printf interpreting % in version strings + printf '%s\n' "version=$CHART_VERSION" >> "$GITHUB_OUTPUT" + echo "Chart version: ${CHART_VERSION}" + + - name: Apply chart version override to Chart.yaml + if: inputs.chart-version != '' + shell: bash + env: + CHART_FOLDER: ${{ inputs.chart-folder }} + CHART_VERSION: ${{ inputs.chart-version }} + run: | + set -euo pipefail + chart_yaml="${CHART_FOLDER}/Chart.yaml" + if [[ "$CHART_VERSION" == *$'\n'* ]]; then + echo "chart-version must be a single line" >&2 + exit 1 + fi + cp -a "$chart_yaml" "${chart_yaml}.pre-oci-publish.bak" + tmp="${chart_yaml}.oci.publish.tmp" + # awk: replace first top-level version line; avoids sed delimiter/injection in the value + awk -v nver="$CHART_VERSION" 'BEGIN{done=0} /^version:/{if(!done){print "version: " nver; done=1; next}} 1' "$chart_yaml" > "$tmp" && mv "$tmp" "$chart_yaml" + + - name: Lint Helm chart + shell: bash + env: + CHART_FOLDER: ${{ inputs.chart-folder }} + run: helm lint "$CHART_FOLDER" + + - name: Package and push chart (OCI) + uses: bsord/helm-push@v4.1.0 + with: + useOCIRegistry: true + registry-url: ${{ inputs.registry-url }} + username: ${{ inputs.username }} + access-token: ${{ inputs.access-token }} + force: ${{ inputs.force }} + chart-folder: ${{ inputs.chart-folder }} + + - name: Restore Chart.yaml after override + if: inputs.chart-version != '' && always() + shell: bash + env: + CHART_FOLDER: ${{ inputs.chart-folder }} + run: | + set -euo pipefail + chart_yaml="${CHART_FOLDER}/Chart.yaml" + if [ -f "${chart_yaml}.pre-oci-publish.bak" ]; then + mv -- "${chart_yaml}.pre-oci-publish.bak" "$chart_yaml" + fi diff --git a/.github/actions/slack-notify-failure/action.yml b/.github/actions/slack-notify-failure/action.yml new file mode 100644 index 0000000..e445b78 --- /dev/null +++ b/.github/actions/slack-notify-failure/action.yml @@ -0,0 +1,26 @@ +# Same pattern as FuelLabs/fuel-core .github/actions/slack-notify-template +name: Notify Slack on failure +description: Sends a Slack notification when the job fails (ravsamhq/notify-slack-action). + +inputs: + github_token: + description: 'GitHub token; caller passes secrets.GITHUB_TOKEN (or PAT) via with:. Composite metadata cannot use secrets.* context.' + required: true + slack_webhook: + description: 'Slack incoming webhook URL from caller repo/org secret — masked in logs.' + required: true + +runs: + using: composite + steps: + - name: Notify if job fails + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ job.status }} + token: ${{ inputs.github_token }} + notification_title: '{workflow} has {status_message}' + message_format: '{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}> : <{run_url}|View Run Results>' + footer: '' + notify_when: failure + env: + SLACK_WEBHOOK_URL: ${{ inputs.slack_webhook }} diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000..e1439b2 --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,125 @@ +# Callable reusable workflow — Docker build & push (FuelLabs/github-actions). +# Pin: uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@ +# +# Composites must use remote uses: (not ./) — the job workspace is the caller’s repo, so +# actions/checkout is the caller, not this repo. Bump GITHUB_ACTIONS_REF on each +# tag release; keep it in sync with the @ref you call this workflow with. +name: Reusable — Docker build and push + +on: + workflow_call: + inputs: + runs-on: + type: string + description: GitHub-hosted runner label for the job + default: ubuntu-latest + platforms: + type: string + description: Comma-separated platforms (e.g. linux/amd64,linux/arm64) + default: linux/amd64 + build-backend: + type: string + description: 'buildx | warp (Warp only for existing adopters or approved repos)' + default: buildx + auth-mode: + type: string + description: 'ecr-oidc | registry-login' + required: true + aws-role-arn: + type: string + description: IAM role for ECR OIDC (required when auth-mode is ecr-oidc) + required: false + aws-region: + type: string + default: us-east-1 + registry: + type: string + description: Registry host when using registry-login + default: ghcr.io + docker-context: + type: string + default: . + dockerfile: + type: string + required: true + image: + type: string + required: true + tags: + type: string + description: Optional multiline docker/metadata-action tag rules (see .github/README.md) + required: false + flavor: + type: string + description: Optional docker/metadata-action flavor (e.g. latest=auto) + required: false + default: '' + labels: + type: string + description: Optional docker/metadata-action custom labels (multiline) + required: false + default: '' + build-args: + type: string + required: false + profile-name: + type: string + description: Warp profile (required when build-backend is warp for Fuel projects) + required: false + secrets: + REGISTRY_USERNAME: + description: Username for registry-login (omit for pure ECR OIDC) + required: false + REGISTRY_PASSWORD: + description: Password or PAT for registry-login + required: false + outputs: + image: + description: Published image reference + value: ${{ jobs.docker.outputs.image }} + digest: + description: Image digest + value: ${{ jobs.docker.outputs.digest }} + metadata: + description: docker/build-push-action metadata JSON + value: ${{ jobs.docker.outputs.metadata }} + +# Ref for remote composite `uses` (must not be ./ — caller’s checkout is the consumer repo) +env: + # Bump when tagging this repo; keep in sync with the @ref on workflow_call + GITHUB_ACTIONS_REF: master + +jobs: + docker: + runs-on: ${{ inputs.runs-on }} + permissions: + id-token: write + contents: read + packages: write + outputs: + image: ${{ steps.build.outputs.image }} + digest: ${{ steps.build.outputs.digest }} + metadata: ${{ steps.build.outputs.metadata }} + steps: + - uses: actions/checkout@v4 + + - name: Build and push + id: build + uses: FuelLabs/github-actions/.github/actions/docker-build-push@${{ env.GITHUB_ACTIONS_REF }} + with: + auth-mode: ${{ inputs.auth-mode }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + registry: ${{ inputs.registry }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + image: ${{ inputs.image }} + tags: ${{ inputs.tags }} + flavor: ${{ inputs.flavor }} + labels: ${{ inputs.labels }} + context: ${{ inputs.docker-context }} + dockerfile: ${{ inputs.dockerfile }} + build-args: ${{ inputs.build-args }} + platforms: ${{ inputs.platforms }} + build-backend: ${{ inputs.build-backend }} + profile-name: ${{ inputs.profile-name }} diff --git a/.github/workflows/helm-publish-oci.yml b/.github/workflows/helm-publish-oci.yml new file mode 100644 index 0000000..4419d0e --- /dev/null +++ b/.github/workflows/helm-publish-oci.yml @@ -0,0 +1,62 @@ +# Callable reusable workflow — Helm OCI publish (non-PR flows only). +# Pin: uses: FuelLabs/github-actions/.github/workflows/helm-publish-oci.yml@ +# +# Composites use remote uses: (not ./) so they resolve in github-actions, not the caller +# repository (see docker-build-push.yml). Bump GITHUB_ACTIONS_REF on each tag release. +name: Reusable — Helm publish (OCI) + +on: + workflow_call: + inputs: + chart-folder: + type: string + required: true + registry-url: + type: string + required: true + chart-version: + type: string + description: Optional override; otherwise Chart.yaml version is used + required: false + force: + type: string + default: 'false' + helm-version: + type: string + default: v3.14.4 + secrets: + REGISTRY_USERNAME: + required: true + REGISTRY_ACCESS_TOKEN: + required: true + outputs: + chart-version: + description: Chart version published + value: ${{ jobs.helm.outputs.chart-version }} + +env: + GITHUB_ACTIONS_REF: master + +jobs: + helm: + runs-on: ubuntu-latest + permissions: + contents: read + # Required when REGISTRY_ACCESS_TOKEN is GITHUB_TOKEN pushing to ghcr.io (Packages) + packages: write + outputs: + chart-version: ${{ steps.publish.outputs.chart-version }} + steps: + - uses: actions/checkout@v4 + + - name: Publish chart + id: publish + uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@${{ env.GITHUB_ACTIONS_REF }} + with: + chart-folder: ${{ inputs.chart-folder }} + registry-url: ${{ inputs.registry-url }} + username: ${{ secrets.REGISTRY_USERNAME }} + access-token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} + chart-version: ${{ inputs.chart-version }} + force: ${{ inputs.force }} + helm-version: ${{ inputs.helm-version }} diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index fe38702..497438b 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -1,3 +1,6 @@ +# Legacy callable — GHCR username/password + metadata inputs. +# Implementation: forwards to `docker-build-push.yml` in this repo (same ref as this file). +# Pin: `uses: FuelLabs/github-actions/.github/workflows/publish-docker-image.yml@` name: Build and Publish Docker Image on: @@ -7,6 +10,7 @@ on: description: "Defines global behaviors for tags (e.g. 'latest=auto')" required: false type: string + default: "" docker_file: description: "Path to the deployment file" @@ -23,15 +27,16 @@ on: description: "Docker image labels (e.g. 'com.example.label=value')" required: false type: string + default: "" tags: - description: "Docker image tags (e.g. 'latest,1.0.0')" + description: "Docker image tags (e.g. multiline docker/metadata-action rules, or 'latest,1.0.0' per your existing usage)" required: true type: string secrets: GH_TOKEN: - description: "GitHub Token" + description: "GitHub Token (Slack notify job)" required: true GITHUB_CONTAINER_USERNAME: @@ -46,55 +51,28 @@ on: description: "Slack Webhook URL" required: true -env: - GITHUB_CONTAINNER_REGISTRY_URL: ghcr.io - jobs: build-and-publish-docker-image: - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Docker Meta - id: meta - uses: docker/metadata-action@v3 - with: - flavor: ${{ inputs.flavor }} - images: ${{ inputs.images }} - labels: ${{ inputs.labels }} - tags: ${{ inputs.tags }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ${{ env.GITHUB_CONTAINNER_REGISTRY_URL }} - username: ${{ secrets.GITHUB_CONTAINER_USERNAME }} - password: ${{ secrets.GITHUB_CONTAINER_PASSWORD }} - - - name: Build and publish image to Github Container Registry - uses: docker/build-push-action@v2 - with: - context: . - file: ${{ inputs.docker_file }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + uses: ./.github/workflows/docker-build-push.yml + with: + runs-on: ubuntu-latest + auth-mode: registry-login + registry: ghcr.io + dockerfile: ${{ inputs.docker_file }} + image: ${{ inputs.images }} + tags: ${{ inputs.tags }} + labels: ${{ inputs.labels }} + flavor: ${{ inputs.flavor }} + build-backend: buildx + platforms: linux/amd64 + secrets: + REGISTRY_USERNAME: ${{ secrets.GITHUB_CONTAINER_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.GITHUB_CONTAINER_PASSWORD }} - notify-slack-action: - needs: cargo-toml-lint - if: ${{failure()}} - uses: FuelLabs/github-actions/.github/workflows/notify-slack-action.yml@master + notify-slack-on-failure: + needs: [build-and-publish-docker-image] + if: failure() + uses: ./.github/workflows/notify-slack-action.yml secrets: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/README.md b/README.md index 17dcab3..1c07260 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ -# Github Actions +# GitHub Actions -Repoistory to host all of Fuel's reusable workflows +Repository for Fuel’s reusable **workflows** and **composite** actions (public). -## Group of Actions +## OCI, Docker, Helm, Slack (composites + `workflow_call`) + +| Location | Description | +| -------- | ----------- | +| [`.github/README.md`](.github/README.md) | **Docker/Helm/Slack** — `docker-build-push`, `helm-publish-oci`, `slack-notify-failure`, and callable `docker-build-push` / `helm-publish-oci` workflows (ECR OIDC, Buildx/QEMU, Warp, OCI). | + +**Legacy / separate:** [`.github/workflows/publish-docker-image.yml`](.github/workflows/publish-docker-image.yml) wraps the same path as `docker-build-push` (GHCR + legacy `GITHUB_CONTAINER_*` / `images` / `docker_file` inputs) and [`.github/workflows/notify-slack-action.yml`](.github/workflows/notify-slack-action.yml) (Slack reusable on failure). New work should use [`docker-build-push.yml`](.github/workflows/docker-build-push.yml) and the composites in [`.github/README.md`](.github/README.md) unless you must keep an old `uses:` pin. + +## Other groups | Group | Description | | --------------------------------- | ---------------------------------------------------------------- | From cd800e7ee88ac0d97f2e7c91e969eea16bba1698 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 27 Apr 2026 14:58:55 -0500 Subject: [PATCH 02/27] fix(github): fix issues found by cursorbot --- .github/README.md | 8 ++++---- .github/actions/docker-build-push/action.yml | 2 +- .github/workflows/docker-build-push.yml | 13 +++++------- .github/workflows/helm-publish-oci.yml | 9 +++------ .github/workflows/publish-crate.yml | 4 ++-- README.md | 21 +++++++++++--------- setups/docker/README.md | 2 +- 7 files changed, 28 insertions(+), 31 deletions(-) diff --git a/.github/README.md b/.github/README.md index 5095154..a0960a9 100644 --- a/.github/README.md +++ b/.github/README.md @@ -17,13 +17,13 @@ This file documents the **OCI / Docker / Helm** composites and their callable wo **Not in scope for these composites:** PR-only Helm, `helm-cleanup-pr`, preview charts. -## How callable workflows resolve composites +**Related (same repo, different path style):** [`setups/docker`](../setups/docker/) is a small composite for **GHCR login and compose**; use the table above for **build + push** or **Helm OCI**. -Callers’ jobs check out the **consumer** repository. A reusable workflow in **this** repo must **not** use `./.github/actions/...` — that path would resolve in the **caller**, not here. The workflows set **`env.GITHUB_ACTIONS_REF`** and use: +## How callable workflows resolve composites -`FuelLabs/github-actions/.github/actions/@${{ env.GITHUB_ACTIONS_REF }}` +Callers’ jobs check out the **consumer** repository. A reusable workflow in **this** repo must **not** use `./.github/actions/...` — that path would resolve in the **caller**, not here. Composite steps use a **fully qualified** `uses: FuelLabs/github-actions/.github/actions/@`, where **`` is a string literal in the workflow file** (e.g. `@master`). GitHub does **not** allow the `env` context in a step’s `uses:` (runtime error: `Unrecognized named-value: 'env'`). Do not use `...@${{ env.… }}`. -**Releases:** set `GITHUB_ACTIONS_REF` in **both** `docker-build-push.yml` and `helm-publish-oci.yml` to the **same** tag/SHA you publish (e.g. `v1.0.0`); pin consumer `uses: .../docker-build-push.yml@v1.0.0` to match. On the default branch it may be `master` for development. +**Releases:** in `docker-build-push.yml` and `helm-publish-oci.yml`, set the `...@` on the composite to the **same** tag/SHA you are about to publish (e.g. `...@v1.0.0` on the commit you tag). Default branch can keep `...@master` for development. Consumers who pin `uses: .../docker-build-push.yml@v1.0.0` get the workflow and composite at that ref together. ## Secrets diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 4db8fa8..591d742 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -120,7 +120,7 @@ runs: printf '%s\n' "$INPUT_TAGS" else printf '%s\n' 'type=sha,prefix=' - printf '%s\n' "type=raw,value=sha-{{sha}}-{{date 'YYYYMMDDhhmmss'}}" + printf '%s\n' "type=raw,value=sha-{{sha}}-{{date 'YYYYMMDDHHmmss'}}" fi echo "$delim" } >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index e1439b2..6737bd1 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -2,8 +2,10 @@ # Pin: uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@ # # Composites must use remote uses: (not ./) — the job workspace is the caller’s repo, so -# actions/checkout is the caller, not this repo. Bump GITHUB_ACTIONS_REF on each -# tag release; keep it in sync with the @ref you call this workflow with. +# actions/checkout is the caller, not this repo. The composite ref below must be a +# **literal** (not env — env is not allowed in `uses:`). On release, set it to the same +# tag you publish (e.g. @v1.0.0) before tagging, so consumers pinning this workflow get a +# matching composite. Default branch: @master. name: Reusable — Docker build and push on: @@ -84,11 +86,6 @@ on: description: docker/build-push-action metadata JSON value: ${{ jobs.docker.outputs.metadata }} -# Ref for remote composite `uses` (must not be ./ — caller’s checkout is the consumer repo) -env: - # Bump when tagging this repo; keep in sync with the @ref on workflow_call - GITHUB_ACTIONS_REF: master - jobs: docker: runs-on: ${{ inputs.runs-on }} @@ -105,7 +102,7 @@ jobs: - name: Build and push id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@${{ env.GITHUB_ACTIONS_REF }} + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} diff --git a/.github/workflows/helm-publish-oci.yml b/.github/workflows/helm-publish-oci.yml index 4419d0e..9f98588 100644 --- a/.github/workflows/helm-publish-oci.yml +++ b/.github/workflows/helm-publish-oci.yml @@ -1,8 +1,8 @@ # Callable reusable workflow — Helm OCI publish (non-PR flows only). # Pin: uses: FuelLabs/github-actions/.github/workflows/helm-publish-oci.yml@ # -# Composites use remote uses: (not ./) so they resolve in github-actions, not the caller -# repository (see docker-build-push.yml). Bump GITHUB_ACTIONS_REF on each tag release. +# Composites use remote `uses:` with a **literal** ref (not `env` — not allowed in `uses:`). +# On release, set it to the same tag you publish (e.g. @v1.0.0) before tagging. See docker-build-push.yml. name: Reusable — Helm publish (OCI) on: @@ -34,9 +34,6 @@ on: description: Chart version published value: ${{ jobs.helm.outputs.chart-version }} -env: - GITHUB_ACTIONS_REF: master - jobs: helm: runs-on: ubuntu-latest @@ -51,7 +48,7 @@ jobs: - name: Publish chart id: publish - uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@${{ env.GITHUB_ACTIONS_REF }} + uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@master with: chart-folder: ${{ inputs.chart-folder }} registry-url: ${{ inputs.registry-url }} diff --git a/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml index 1e85bb0..c15ed25 100644 --- a/.github/workflows/publish-crate.yml +++ b/.github/workflows/publish-crate.yml @@ -34,8 +34,8 @@ jobs: registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} notify-slack-action: - needs: cargo-toml-lint - if: ${{failure()}} + needs: [publish-crate] + if: failure() uses: FuelLabs/github-actions/.github/workflows/notify-slack-action.yml@master secrets: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 1c07260..1ae5fbe 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,18 @@ Repository for Fuel’s reusable **workflows** and **composite** actions (public ## Other groups -| Group | Description | -| --------------------------------- | ---------------------------------------------------------------- | -| [audit](./audits/) | Reusable workflows for auditing npm packages | -| [changeset](./changeset/) | Reusable workflow for create changesets and release npm packages | -| [gh-projects](./gh-projects/) | Automating interactions between GH Projects and repositories | -| [setups/node](./setups/node/) | Setup node and pnpm requirements on CI | -| [setups/docker](./setups/docker/) | Setup docker and docker compose on CI | -| [setups/npm](./setups/npm/) | Setup npm deployment requirements on CI | -| [update-sdk](./update-sdk/) | Reusable workflow for update the SDK packages | +Root-level actions use **`uses: FuelLabs/github-actions//...`** and a colocated **README** in each folder. **OCI (Docker / Helm) stacks** are different: composites live under [`.github/actions/`](.github/actions/) and are documented in [`.github/README.md`](.github/README.md) (callable workflows, cross-repo `uses:` with **literal** composite `@ref`, not `env`); that split exists so Docker/Helm can share one doc with **ECR, Warp, and OCI** patterns. + +| Group | Description | +| ---------------------------------- | ---------------------------------------------------------------- | +| [`.github/README.md`](.github/README.md) | **Docker, Helm, Slack** composites and `workflow_call` wrappers (see OCI block above) | +| [audit](./audits/) | Reusable workflows for auditing npm packages | +| [changeset](./changeset/) | Reusable workflow for create changesets and release npm packages | +| [gh-projects](./gh-projects/) | Automating interactions between GH Projects and repositories | +| [setups/node](./setups/node/) | Setup node and pnpm requirements on CI | +| [setups/docker](./setups/docker/) | **GHCR login** / docker compose (lightweight; not a full build+push) | +| [setups/npm](./setups/npm/) | Setup npm deployment requirements on CI | +| [update-sdk](./update-sdk/) | Reusable workflow for update the SDK packages | ## License diff --git a/setups/docker/README.md b/setups/docker/README.md index 9f5586c..bea3450 100644 --- a/setups/docker/README.md +++ b/setups/docker/README.md @@ -1,4 +1,4 @@ -### Setup node +### Setup docker A github action to setup Docker From e7e51b6f0d9eb1b068d75315073ef7bc5dd6c560 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:18:04 -0500 Subject: [PATCH 03/27] feat(docker): add options for build and helm destinations --- .github/README.md | 61 ++++++- .github/actions/docker-build-push/action.yml | 67 +++++-- .github/actions/helm-publish-oci/action.yml | 104 ++++++++++- .github/workflows/docker-build-push.yml | 182 ++++++++++++++++++- .github/workflows/helm-publish-oci.yml | 25 ++- 5 files changed, 401 insertions(+), 38 deletions(-) diff --git a/.github/README.md b/.github/README.md index a0960a9..111ed22 100644 --- a/.github/README.md +++ b/.github/README.md @@ -8,10 +8,10 @@ This file documents the **OCI / Docker / Helm** composites and their callable wo | Kind | Path | Purpose | |------|------|---------| -| Composite | `.github/actions/docker-build-push` | ECR OIDC or registry login; **Buildx** + QEMU, or **Warp** | -| Composite | `.github/actions/helm-publish-oci` | Non-PR Helm **OCI** publish (lint, push) | +| Composite | `.github/actions/docker-build-push` | ECR private/public OIDC or registry login; **Buildx** + QEMU, or **Warp** | +| Composite | `.github/actions/helm-publish-oci` | Non-PR Helm **OCI** publish (lint, push) via registry token or AWS OIDC (ECR) | | Composite | `.github/actions/slack-notify-failure` | Small Slack failure step (`ravsamhq/notify-slack-action`) | -| Reusable workflow | `.github/workflows/docker-build-push.yml` | Forwards `runs-on`, `platforms`, `build-backend`, `permissions` | +| Reusable workflow | `.github/workflows/docker-build-push.yml` | Native per-platform runner builds + digest merge (default), or Warp direct push | | Reusable workflow | `.github/workflows/helm-publish-oci.yml` | Same for Helm | | Reusable workflow (legacy) | `.github/workflows/publish-docker-image.yml` | Same implementation as `docker-build-push` (wraps the row above) + old secret/input names + Slack on failure | @@ -44,24 +44,73 @@ jobs: uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@v1.0.0 secrets: inherit with: - runs-on: ubuntu-latest auth-mode: registry-login dockerfile: Dockerfile image: ghcr.io/fuellabs/myapp + build-backend: native + runs-on-amd64: ubuntu-latest + runs-on-arm64: ubuntu-24.04-arm ``` -**Callable** — Helm to GHCR (needs `packages: write` in the **called** job — the workflow already sets it; token must be `GITHUB_TOKEN` or a PAT with package write): +**Callable** — Docker to ECR Public (OIDC): + +```yaml +jobs: + image: + uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@v1.0.0 + with: + auth-mode: ecr-public-oidc + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-region: us-east-1 + dockerfile: Dockerfile + image: public.ecr.aws/your-alias/myapp + build-backend: native +``` + +**Callable** — Docker via Warp (no native digest merge): + +```yaml +jobs: + image: + uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@v1.0.0 + secrets: inherit + with: + auth-mode: ecr-oidc + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + dockerfile: Dockerfile + image: 123.dkr.ecr.us-east-1.amazonaws.com/myapp + build-backend: warp + profile-name: my-warp-profile +``` +**Callable** — Helm to GHCR (`registry-login`; needs `packages: write` in the **called** job — workflow already sets it): ```yaml jobs: chart: uses: FuelLabs/github-actions/.github/workflows/helm-publish-oci.yml@v1.0.0 + with: + auth-mode: registry-login + chart-folder: helm/my-chart + registry-url: oci://ghcr.io/${{ github.repository_owner }}/charts secrets: REGISTRY_USERNAME: ${{ github.actor }} REGISTRY_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +**Callable** — Helm to AWS ECR (`ecr-oidc`): + +```yaml +jobs: + chart: + uses: FuelLabs/github-actions/.github/workflows/helm-publish-oci.yml@v1.0.0 with: + auth-mode: ecr-oidc + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-region: us-east-1 chart-folder: helm/my-chart - registry-url: oci://ghcr.io/${{ github.repository_owner }}/charts + registry-url: oci://123456789012.dkr.ecr.us-east-1.amazonaws.com/charts + # Optional if registry-url includes host (recommended) + # registry-host: 123456789012.dkr.ecr.us-east-1.amazonaws.com ``` **Composite** (consumer writes full `permissions`): diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 591d742..9819023 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -1,16 +1,16 @@ # Bump third-party majors via PR after review (supply chain). name: Docker build and push -description: Build and push a container image (ECR OIDC or registry login; Buildx or Warp). +description: Build and push a container image (ECR private/public OIDC or registry login; Buildx or Warp). inputs: auth-mode: - description: 'ecr-oidc | registry-login' + description: 'ecr-oidc | ecr-public-oidc | registry-login' required: true aws-role-arn: - description: IAM role ARN for ECR (auth-mode ecr-oidc) + description: IAM role ARN for ECR (auth-mode ecr-oidc or ecr-public-oidc) required: false aws-region: - description: AWS region for ECR + description: AWS region for ECR (public ECR generally uses us-east-1) required: false default: us-east-1 registry: @@ -59,6 +59,18 @@ inputs: profile-name: description: Warp profile name (build-backend warp — required for org Warp projects) required: false + push-by-digest: + description: 'true | false (buildx only). true pushes canonical digest refs (for manifest merge flows).' + required: false + default: 'false' + cache-scope: + description: Optional GHA cache scope (buildx only), e.g. linux-amd64 + required: false + default: '' + setup-only: + description: 'true | false. If true, only auth/login setup is performed (no metadata/build/push).' + required: false + default: 'false' push: description: 'true | false' required: false @@ -67,32 +79,39 @@ inputs: outputs: image: description: Image reference (from metadata) - value: ${{ fromJSON(inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata)['image.name'] }} + value: ${{ inputs.setup-only == 'true' && '' || fromJSON(inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata)['image.name'] }} imageid: description: Image ID from build backend - value: ${{ inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || steps.publish-buildx.outputs.imageId }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || steps.publish-buildx.outputs.imageId) }} digest: description: Image digest - value: ${{ inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || steps.publish-buildx.outputs.digest }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || steps.publish-buildx.outputs.digest) }} metadata: description: Build metadata JSON - value: ${{ inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata) }} runs: using: composite steps: - name: Configure AWS credentials (ECR OIDC) - if: inputs.auth-mode == 'ecr-oidc' + if: inputs.auth-mode == 'ecr-oidc' || inputs.auth-mode == 'ecr-public-oidc' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ inputs.aws-role-arn }} aws-region: ${{ inputs.aws-region }} - - name: Login to Amazon ECR + - name: Login to Amazon ECR (private) if: inputs.auth-mode == 'ecr-oidc' id: login-ecr uses: aws-actions/amazon-ecr-login@v2 + - name: Login to Amazon ECR Public + if: inputs.auth-mode == 'ecr-public-oidc' + id: login-ecr-public + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + - name: Log in to container registry if: inputs.auth-mode == 'registry-login' uses: docker/login-action@v3 @@ -103,6 +122,7 @@ runs: - name: Resolve metadata tag lines id: tag-lines + if: inputs.setup-only != 'true' shell: bash env: # Do not embed inputs in the script body; pass through env to avoid shell/heredoc injection @@ -127,6 +147,7 @@ runs: - name: Docker meta id: meta + if: inputs.setup-only != 'true' uses: docker/metadata-action@v5 with: images: | @@ -138,15 +159,29 @@ runs: ${{ inputs.labels }} - name: Set up QEMU (Buildx cross-arch) - if: inputs.build-backend == 'buildx' && (contains(inputs.platforms, ',') || contains(inputs.platforms, 'arm')) + if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && (contains(inputs.platforms, ',') || contains(inputs.platforms, 'arm')) uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - if: inputs.build-backend == 'buildx' + if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' uses: docker/setup-buildx-action@v3 + - name: Build and push (Buildx canonical digest) + if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest == 'true' + uses: docker/build-push-action@v6 + id: publish-buildx + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + outputs: type=image,name=${{ inputs.image }},push-by-digest=true,name-canonical=true,push=true + labels: ${{ steps.meta.outputs.labels }} + build-args: ${{ inputs.build-args }} + platforms: ${{ inputs.platforms }} + cache-from: ${{ inputs.cache-scope != '' && format('type=gha,scope={0}', inputs.cache-scope) || 'type=gha' }} + cache-to: ${{ inputs.cache-scope != '' && format('type=gha,mode=max,scope={0}', inputs.cache-scope) || 'type=gha,mode=max' }} + - name: Build and push (Buildx) - if: inputs.build-backend == 'buildx' + if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest != 'true' uses: docker/build-push-action@v6 id: publish-buildx with: @@ -157,11 +192,11 @@ runs: labels: ${{ steps.meta.outputs.labels }} build-args: ${{ inputs.build-args }} platforms: ${{ inputs.platforms }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: ${{ inputs.cache-scope != '' && format('type=gha,scope={0}', inputs.cache-scope) || 'type=gha' }} + cache-to: ${{ inputs.cache-scope != '' && format('type=gha,mode=max,scope={0}', inputs.cache-scope) || 'type=gha,mode=max' }} - name: Build and push (Warp) - if: inputs.build-backend == 'warp' + if: inputs.setup-only != 'true' && inputs.build-backend == 'warp' uses: Warpbuilds/build-push-action@v6 id: publish-warp with: diff --git a/.github/actions/helm-publish-oci/action.yml b/.github/actions/helm-publish-oci/action.yml index 95ff30b..aaf5d17 100644 --- a/.github/actions/helm-publish-oci/action.yml +++ b/.github/actions/helm-publish-oci/action.yml @@ -4,18 +4,33 @@ name: Helm publish (OCI) description: Lint and push a Helm chart to an OCI registry using Chart.yaml version or an override. inputs: + auth-mode: + description: 'registry-login | ecr-oidc' + required: false + default: registry-login chart-folder: description: Path to the Helm chart directory required: true registry-url: description: OCI registry URL for the chart (e.g. oci://ghcr.io/org/charts) required: true + registry-host: + description: Registry host for helm registry login in ecr-oidc mode (e.g. 123.dkr.ecr.us-east-1.amazonaws.com). If empty, derived from registry-url. + required: false + default: '' username: - description: Registry username - required: true + description: Registry username (required for registry-login) + required: false access-token: - description: Registry password or token. Caller should set from secrets — masked in logs. - required: true + description: Registry password or token (required for registry-login). Caller should set from secrets — masked in logs. + required: false + aws-role-arn: + description: IAM role ARN for OIDC auth (required for ecr-oidc) + required: false + aws-region: + description: AWS region for ECR in ecr-oidc mode + required: false + default: us-east-1 chart-version: description: Override chart version (otherwise read from Chart.yaml) required: false @@ -41,6 +56,71 @@ runs: with: version: ${{ inputs.helm-version }} + - name: Validate auth-mode input combination + shell: bash + env: + AUTH_MODE: ${{ inputs.auth-mode }} + USERNAME: ${{ inputs.username }} + ACCESS_TOKEN: ${{ inputs.access-token }} + AWS_ROLE_ARN: ${{ inputs.aws-role-arn }} + run: | + set -euo pipefail + case "$AUTH_MODE" in + registry-login) + if [ -z "$USERNAME" ] || [ -z "$ACCESS_TOKEN" ]; then + echo "registry-login requires username and access-token" >&2 + exit 1 + fi + ;; + ecr-oidc) + if [ -z "$AWS_ROLE_ARN" ]; then + echo "ecr-oidc requires aws-role-arn" >&2 + exit 1 + fi + ;; + *) + echo "Unsupported auth-mode: $AUTH_MODE" >&2 + exit 1 + ;; + esac + + - name: Configure AWS credentials (OIDC) + if: inputs.auth-mode == 'ecr-oidc' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + + - name: Resolve registry host for ECR login + if: inputs.auth-mode == 'ecr-oidc' + id: registry-host + shell: bash + env: + REGISTRY_HOST_INPUT: ${{ inputs.registry-host }} + REGISTRY_URL: ${{ inputs.registry-url }} + run: | + set -euo pipefail + host="$REGISTRY_HOST_INPUT" + if [ -z "$host" ]; then + host="${REGISTRY_URL#oci://}" + host="${host%%/*}" + fi + if [ -z "$host" ]; then + echo "Unable to resolve registry host from registry-url: $REGISTRY_URL" >&2 + exit 1 + fi + echo "host=$host" >> "$GITHUB_OUTPUT" + + - name: Helm registry login to ECR + if: inputs.auth-mode == 'ecr-oidc' + shell: bash + env: + AWS_REGION: ${{ inputs.aws-region }} + REGISTRY_HOST: ${{ steps.registry-host.outputs.host }} + run: | + set -euo pipefail + aws ecr get-login-password --region "$AWS_REGION" | helm registry login --username AWS --password-stdin "$REGISTRY_HOST" + - name: Determine chart version id: version shell: bash @@ -91,7 +171,8 @@ runs: CHART_FOLDER: ${{ inputs.chart-folder }} run: helm lint "$CHART_FOLDER" - - name: Package and push chart (OCI) + - name: Package and push chart (OCI, registry-login) + if: inputs.auth-mode == 'registry-login' uses: bsord/helm-push@v4.1.0 with: useOCIRegistry: true @@ -101,6 +182,19 @@ runs: force: ${{ inputs.force }} chart-folder: ${{ inputs.chart-folder }} + - name: Package and push chart (OCI, ecr-oidc) + if: inputs.auth-mode == 'ecr-oidc' + shell: bash + env: + CHART_FOLDER: ${{ inputs.chart-folder }} + CHART_VERSION: ${{ steps.version.outputs.version }} + REGISTRY_URL: ${{ inputs.registry-url }} + run: | + set -euo pipefail + helm package "$CHART_FOLDER" --version "$CHART_VERSION" + helm push "$CHART_FOLDER"/*.tgz "$REGISTRY_URL" + rm -f "$CHART_FOLDER"/*.tgz + - name: Restore Chart.yaml after override if: inputs.chart-version != '' && always() shell: bash diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 6737bd1..4d8f90e 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -21,19 +21,27 @@ on: default: linux/amd64 build-backend: type: string - description: 'buildx | warp (Warp only for existing adopters or approved repos)' + description: 'buildx | native | warp (buildx/native = native runner + digest merge path)' default: buildx auth-mode: type: string - description: 'ecr-oidc | registry-login' + description: 'ecr-oidc | ecr-public-oidc | registry-login' required: true aws-role-arn: type: string - description: IAM role for ECR OIDC (required when auth-mode is ecr-oidc) + description: IAM role for ECR OIDC (required when auth-mode is ecr-oidc or ecr-public-oidc) required: false aws-region: type: string default: us-east-1 + runs-on-amd64: + type: string + description: Runner label for linux/amd64 native build + default: ubuntu-latest + runs-on-arm64: + type: string + description: Runner label for linux/arm64 native build + default: ubuntu-24.04-arm registry: type: string description: Registry host when using registry-login @@ -78,16 +86,172 @@ on: outputs: image: description: Published image reference - value: ${{ jobs.docker.outputs.image }} + value: ${{ jobs.native-merge.outputs.image || jobs.warp.outputs.image }} digest: description: Image digest - value: ${{ jobs.docker.outputs.digest }} + value: ${{ jobs.native-merge.outputs.digest || jobs.warp.outputs.digest }} metadata: description: docker/build-push-action metadata JSON - value: ${{ jobs.docker.outputs.metadata }} + value: ${{ jobs.native-merge.outputs.metadata || jobs.warp.outputs.metadata }} jobs: - docker: + native-build: + if: ${{ inputs.build-backend != 'warp' }} + runs-on: ${{ matrix.runner }} + permissions: + id-token: write + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ${{ inputs.runs-on-amd64 }} + - platform: linux/arm64 + runner: ${{ inputs.runs-on-arm64 }} + steps: + - uses: actions/checkout@v4 + + - name: Derive platform pair + id: platform + if: contains(inputs.platforms, matrix.platform) + shell: bash + run: | + platform="${{ matrix.platform }}" + echo "pair=${platform//\//-}" >> "$GITHUB_OUTPUT" + + - name: Build and push by digest (native) + id: build + if: contains(inputs.platforms, matrix.platform) + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + with: + auth-mode: ${{ inputs.auth-mode }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + registry: ${{ inputs.registry }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + image: ${{ inputs.image }} + tags: ${{ inputs.tags }} + flavor: ${{ inputs.flavor }} + labels: ${{ inputs.labels }} + context: ${{ inputs.docker-context }} + dockerfile: ${{ inputs.dockerfile }} + build-backend: buildx + push-by-digest: 'true' + cache-scope: ${{ steps.platform.outputs.pair }} + platforms: ${{ matrix.platform }} + build-args: ${{ inputs.build-args }} + + - name: Export digest + if: contains(inputs.platforms, matrix.platform) + shell: bash + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + if: contains(inputs.platforms, matrix.platform) + uses: actions/upload-artifact@v4 + with: + name: digests-${{ steps.platform.outputs.pair }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + native-merge: + if: ${{ inputs.build-backend != 'warp' }} + needs: native-build + runs-on: ${{ inputs.runs-on }} + permissions: + id-token: write + contents: read + packages: write + outputs: + image: ${{ steps.image.outputs.image }} + digest: ${{ steps.inspect.outputs.digest }} + metadata: ${{ steps.meta.outputs.json }} + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Login/setup only (reuse composite) + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + with: + auth-mode: ${{ inputs.auth-mode }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} + registry: ${{ inputs.registry }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + image: ${{ inputs.image }} + dockerfile: ${{ inputs.dockerfile }} + setup-only: 'true' + + - name: Resolve metadata tag lines + id: tag-lines + shell: bash + env: + INPUT_TAGS: ${{ inputs.tags }} + run: | + set -euo pipefail + if [ -r /proc/sys/kernel/random/uuid ]; then + delim="GHA_OUT_$(< /proc/sys/kernel/random/uuid tr -d '-\n\r')" + else + delim="GHA_OUT_${RANDOM}_$$_$(date +%s%N)" + fi + { + echo "tags<<${delim}" + if [ -n "$INPUT_TAGS" ]; then + printf '%s\n' "$INPUT_TAGS" + else + printf '%s\n' 'type=sha,prefix=' + printf '%s\n' "type=raw,value=sha-{{sha}}-{{date 'YYYYMMDDHHmmss'}}" + fi + echo "$delim" + } >> "$GITHUB_OUTPUT" + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ inputs.image }} + tags: ${{ steps.tag-lines.outputs.tags }} + flavor: | + ${{ inputs.flavor }} + labels: | + ${{ inputs.labels }} + + - name: Create manifest list and push + working-directory: /tmp/digests + shell: bash + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ inputs.image }}@sha256:%s ' *) + + - name: Set image output + id: image + shell: bash + run: echo "image=${{ inputs.image }}" >> "$GITHUB_OUTPUT" + + - name: Inspect pushed manifest digest + id: inspect + shell: bash + run: | + set -euo pipefail + first_tag="$(jq -r '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON")" + digest="$(docker buildx imagetools inspect "$first_tag" | awk '/Digest:/ {print $2; exit}')" + echo "digest=$digest" >> "$GITHUB_OUTPUT" + + warp: + if: ${{ inputs.build-backend == 'warp' }} runs-on: ${{ inputs.runs-on }} permissions: id-token: write @@ -100,7 +264,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Build and push + - name: Build and push (Warp) id: build uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: @@ -118,5 +282,5 @@ jobs: dockerfile: ${{ inputs.dockerfile }} build-args: ${{ inputs.build-args }} platforms: ${{ inputs.platforms }} - build-backend: ${{ inputs.build-backend }} + build-backend: warp profile-name: ${{ inputs.profile-name }} diff --git a/.github/workflows/helm-publish-oci.yml b/.github/workflows/helm-publish-oci.yml index 9f98588..e52b732 100644 --- a/.github/workflows/helm-publish-oci.yml +++ b/.github/workflows/helm-publish-oci.yml @@ -8,12 +8,29 @@ name: Reusable — Helm publish (OCI) on: workflow_call: inputs: + auth-mode: + type: string + description: 'registry-login | ecr-oidc' + required: false + default: registry-login chart-folder: type: string required: true registry-url: type: string required: true + registry-host: + type: string + description: Optional registry host for ecr-oidc (derived from registry-url when empty) + required: false + default: '' + aws-role-arn: + type: string + description: IAM role ARN for ecr-oidc mode + required: false + aws-region: + type: string + default: us-east-1 chart-version: type: string description: Optional override; otherwise Chart.yaml version is used @@ -26,9 +43,9 @@ on: default: v3.14.4 secrets: REGISTRY_USERNAME: - required: true + required: false REGISTRY_ACCESS_TOKEN: - required: true + required: false outputs: chart-version: description: Chart version published @@ -50,8 +67,12 @@ jobs: id: publish uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@master with: + auth-mode: ${{ inputs.auth-mode }} chart-folder: ${{ inputs.chart-folder }} registry-url: ${{ inputs.registry-url }} + registry-host: ${{ inputs.registry-host }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} username: ${{ secrets.REGISTRY_USERNAME }} access-token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} chart-version: ${{ inputs.chart-version }} From 6293511d0692979b1fbd27bda8600b61391646df Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:32:31 -0500 Subject: [PATCH 04/27] fix(helm): output to correct directory --- .github/actions/helm-publish-oci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/helm-publish-oci/action.yml b/.github/actions/helm-publish-oci/action.yml index aaf5d17..f85057b 100644 --- a/.github/actions/helm-publish-oci/action.yml +++ b/.github/actions/helm-publish-oci/action.yml @@ -191,7 +191,7 @@ runs: REGISTRY_URL: ${{ inputs.registry-url }} run: | set -euo pipefail - helm package "$CHART_FOLDER" --version "$CHART_VERSION" + helm package "$CHART_FOLDER" --version "$CHART_VERSION" --destination "$CHART_FOLDER" helm push "$CHART_FOLDER"/*.tgz "$REGISTRY_URL" rm -f "$CHART_FOLDER"/*.tgz From 2e1f35f0a7d6c72a43d0aa3d8cce9dae991e45c6 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:32:51 -0500 Subject: [PATCH 05/27] fix(docker): don't spin up an unused instance --- .github/workflows/docker-build-push.yml | 67 +++++++++++++++++++++---- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 4d8f90e..ed3fe16 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -95,8 +95,64 @@ on: value: ${{ jobs.native-merge.outputs.metadata || jobs.warp.outputs.metadata }} jobs: + native-plan: + if: ${{ inputs.build-backend != 'warp' }} + runs-on: ${{ inputs.runs-on }} + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + steps: + - name: Build native matrix from requested platforms + id: plan + shell: bash + env: + PLATFORMS: ${{ inputs.platforms }} + RUNNER_AMD64: ${{ inputs.runs-on-amd64 }} + RUNNER_ARM64: ${{ inputs.runs-on-arm64 }} + run: | + set -euo pipefail + python3 - <<'PY' >> "$GITHUB_OUTPUT" + import json + import os + + raw = os.environ.get("PLATFORMS", "") + runner_amd64 = os.environ.get("RUNNER_AMD64", "ubuntu-latest") + runner_arm64 = os.environ.get("RUNNER_ARM64", "ubuntu-24.04-arm") + allowed = { + "linux/amd64": runner_amd64, + "linux/arm64": runner_arm64, + } + + requested = [p.strip() for p in raw.split(",") if p.strip()] + if not requested: + raise SystemExit("platforms input must contain at least one platform") + + include = [] + seen = set() + invalid = [] + for p in requested: + if p not in allowed: + invalid.append(p) + continue + if p in seen: + continue + seen.add(p) + include.append({"platform": p, "runner": allowed[p]}) + + if invalid: + raise SystemExit( + "Unsupported platform(s): " + + ", ".join(invalid) + + ". Allowed: linux/amd64, linux/arm64" + ) + if not include: + raise SystemExit("No valid platforms to build") + + print(f"matrix={json.dumps({'include': include}, separators=(',', ':'))}") + PY + native-build: if: ${{ inputs.build-backend != 'warp' }} + needs: native-plan runs-on: ${{ matrix.runner }} permissions: id-token: write @@ -104,18 +160,12 @@ jobs: packages: write strategy: fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ${{ inputs.runs-on-amd64 }} - - platform: linux/arm64 - runner: ${{ inputs.runs-on-arm64 }} + matrix: ${{ fromJSON(needs.native-plan.outputs.matrix) }} steps: - uses: actions/checkout@v4 - name: Derive platform pair id: platform - if: contains(inputs.platforms, matrix.platform) shell: bash run: | platform="${{ matrix.platform }}" @@ -123,7 +173,6 @@ jobs: - name: Build and push by digest (native) id: build - if: contains(inputs.platforms, matrix.platform) uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: auth-mode: ${{ inputs.auth-mode }} @@ -145,7 +194,6 @@ jobs: build-args: ${{ inputs.build-args }} - name: Export digest - if: contains(inputs.platforms, matrix.platform) shell: bash run: | mkdir -p /tmp/digests @@ -153,7 +201,6 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - if: contains(inputs.platforms, matrix.platform) uses: actions/upload-artifact@v4 with: name: digests-${{ steps.platform.outputs.pair }} From e5505c881a774adb38bda880e4f62aa1a43f5ca9 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:37:22 -0500 Subject: [PATCH 06/27] fix(helm): update permissions for aws oidc --- .github/workflows/helm-publish-oci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/helm-publish-oci.yml b/.github/workflows/helm-publish-oci.yml index e52b732..deac9ec 100644 --- a/.github/workflows/helm-publish-oci.yml +++ b/.github/workflows/helm-publish-oci.yml @@ -55,6 +55,8 @@ jobs: helm: runs-on: ubuntu-latest permissions: + # Required for aws-actions/configure-aws-credentials OIDC (ecr-oidc mode) + id-token: write contents: read # Required when REGISTRY_ACCESS_TOKEN is GITHUB_TOKEN pushing to ghcr.io (Packages) packages: write From f51926fb7702c54bcef84ffb6676be624b657eb3 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:47:13 -0500 Subject: [PATCH 07/27] fix(docker): remove duplicate IDs --- .github/actions/docker-build-push/action.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 9819023..4bd9452 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -79,16 +79,16 @@ inputs: outputs: image: description: Image reference (from metadata) - value: ${{ inputs.setup-only == 'true' && '' || fromJSON(inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata)['image.name'] }} + value: ${{ inputs.setup-only == 'true' && '' || fromJSON(inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata))['image.name'] }} imageid: description: Image ID from build backend - value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || steps.publish-buildx.outputs.imageId) }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.imageId || steps.publish-buildx.outputs.imageId)) }} digest: description: Image digest - value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || steps.publish-buildx.outputs.digest) }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.digest || steps.publish-buildx.outputs.digest)) }} metadata: description: Build metadata JSON - value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || steps.publish-buildx.outputs.metadata) }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)) }} runs: using: composite @@ -169,7 +169,7 @@ runs: - name: Build and push (Buildx canonical digest) if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest == 'true' uses: docker/build-push-action@v6 - id: publish-buildx + id: publish-buildx-digest with: context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} From 5b3ad601ecf582a518d2f39a6960e688073b383e Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:49:05 -0500 Subject: [PATCH 08/27] fix(docker): avoid shell/heredoc injection --- .github/workflows/docker-build-push.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index ed3fe16..5e231ef 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -279,14 +279,21 @@ jobs: - name: Create manifest list and push working-directory: /tmp/digests shell: bash + env: + IMAGE_REF: ${{ inputs.image }} run: | + set -euo pipefail docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ inputs.image }}@sha256:%s ' *) + $(printf '%s@sha256:%s ' "$IMAGE_REF" *) - name: Set image output id: image shell: bash - run: echo "image=${{ inputs.image }}" >> "$GITHUB_OUTPUT" + env: + IMAGE_REF: ${{ inputs.image }} + run: | + set -euo pipefail + printf '%s\n' "image=$IMAGE_REF" >> "$GITHUB_OUTPUT" - name: Inspect pushed manifest digest id: inspect From 3c68b886287d0fadf07d7dc12587101248616b36 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 10:52:49 -0500 Subject: [PATCH 09/27] fix(docker): remove potential duplicate metadata info --- .github/actions/docker-build-push/action.yml | 28 +++++++---- .github/workflows/docker-build-push.yml | 52 +++++--------------- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 4bd9452..8ebb15e 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -71,6 +71,10 @@ inputs: description: 'true | false. If true, only auth/login setup is performed (no metadata/build/push).' required: false default: 'false' + metadata-only: + description: 'true | false. If true, run auth/login + metadata generation only (no build/push).' + required: false + default: 'false' push: description: 'true | false' required: false @@ -79,16 +83,22 @@ inputs: outputs: image: description: Image reference (from metadata) - value: ${{ inputs.setup-only == 'true' && '' || fromJSON(inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata))['image.name'] }} + value: ${{ inputs.setup-only == 'true' && '' || fromJSON((inputs.metadata-only == 'true' && steps.meta.outputs.json) || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)))['image.name'] }} imageid: description: Image ID from build backend - value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.imageId || steps.publish-buildx.outputs.imageId)) }} + value: ${{ (inputs.setup-only == 'true' || inputs.metadata-only == 'true') && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.imageId || steps.publish-buildx.outputs.imageId)) }} digest: description: Image digest - value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.digest || steps.publish-buildx.outputs.digest)) }} + value: ${{ (inputs.setup-only == 'true' || inputs.metadata-only == 'true') && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.digest || steps.publish-buildx.outputs.digest)) }} metadata: description: Build metadata JSON - value: ${{ inputs.setup-only == 'true' && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)) }} + value: ${{ inputs.setup-only == 'true' && '' || (inputs.metadata-only == 'true' && steps.meta.outputs.json) || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)) }} + tags: + description: docker/metadata-action computed tags list + value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.tags }} + labels: + description: docker/metadata-action computed labels list + value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.labels }} runs: using: composite @@ -159,15 +169,15 @@ runs: ${{ inputs.labels }} - name: Set up QEMU (Buildx cross-arch) - if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && (contains(inputs.platforms, ',') || contains(inputs.platforms, 'arm')) + if: inputs.setup-only != 'true' && inputs.metadata-only != 'true' && inputs.build-backend == 'buildx' && (contains(inputs.platforms, ',') || contains(inputs.platforms, 'arm')) uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' + if: inputs.setup-only != 'true' && inputs.metadata-only != 'true' && inputs.build-backend == 'buildx' uses: docker/setup-buildx-action@v3 - name: Build and push (Buildx canonical digest) - if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest == 'true' + if: inputs.setup-only != 'true' && inputs.metadata-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest == 'true' uses: docker/build-push-action@v6 id: publish-buildx-digest with: @@ -181,7 +191,7 @@ runs: cache-to: ${{ inputs.cache-scope != '' && format('type=gha,mode=max,scope={0}', inputs.cache-scope) || 'type=gha,mode=max' }} - name: Build and push (Buildx) - if: inputs.setup-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest != 'true' + if: inputs.setup-only != 'true' && inputs.metadata-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest != 'true' uses: docker/build-push-action@v6 id: publish-buildx with: @@ -196,7 +206,7 @@ runs: cache-to: ${{ inputs.cache-scope != '' && format('type=gha,mode=max,scope={0}', inputs.cache-scope) || 'type=gha,mode=max' }} - name: Build and push (Warp) - if: inputs.setup-only != 'true' && inputs.build-backend == 'warp' + if: inputs.setup-only != 'true' && inputs.metadata-only != 'true' && inputs.build-backend == 'warp' uses: Warpbuilds/build-push-action@v6 id: publish-warp with: diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 5e231ef..b698849 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -219,7 +219,7 @@ jobs: outputs: image: ${{ steps.image.outputs.image }} digest: ${{ steps.inspect.outputs.digest }} - metadata: ${{ steps.meta.outputs.json }} + metadata: ${{ steps.meta.outputs.metadata }} steps: - name: Download digests uses: actions/download-artifact@v4 @@ -228,8 +228,9 @@ jobs: pattern: digests-* merge-multiple: true - - name: Login/setup only (reuse composite) + - name: Login and metadata only (reuse composite) uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + id: meta with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} @@ -238,52 +239,21 @@ jobs: username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} image: ${{ inputs.image }} + tags: ${{ inputs.tags }} + flavor: ${{ inputs.flavor }} + labels: ${{ inputs.labels }} dockerfile: ${{ inputs.dockerfile }} - setup-only: 'true' - - - name: Resolve metadata tag lines - id: tag-lines - shell: bash - env: - INPUT_TAGS: ${{ inputs.tags }} - run: | - set -euo pipefail - if [ -r /proc/sys/kernel/random/uuid ]; then - delim="GHA_OUT_$(< /proc/sys/kernel/random/uuid tr -d '-\n\r')" - else - delim="GHA_OUT_${RANDOM}_$$_$(date +%s%N)" - fi - { - echo "tags<<${delim}" - if [ -n "$INPUT_TAGS" ]; then - printf '%s\n' "$INPUT_TAGS" - else - printf '%s\n' 'type=sha,prefix=' - printf '%s\n' "type=raw,value=sha-{{sha}}-{{date 'YYYYMMDDHHmmss'}}" - fi - echo "$delim" - } >> "$GITHUB_OUTPUT" - - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ${{ inputs.image }} - tags: ${{ steps.tag-lines.outputs.tags }} - flavor: | - ${{ inputs.flavor }} - labels: | - ${{ inputs.labels }} + metadata-only: 'true' - name: Create manifest list and push working-directory: /tmp/digests shell: bash env: IMAGE_REF: ${{ inputs.image }} + METADATA_JSON: ${{ steps.meta.outputs.metadata }} run: | set -euo pipefail - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$METADATA_JSON") \ $(printf '%s@sha256:%s ' "$IMAGE_REF" *) - name: Set image output @@ -298,9 +268,11 @@ jobs: - name: Inspect pushed manifest digest id: inspect shell: bash + env: + METADATA_JSON: ${{ steps.meta.outputs.metadata }} run: | set -euo pipefail - first_tag="$(jq -r '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON")" + first_tag="$(jq -r '.tags[0]' <<< "$METADATA_JSON")" digest="$(docker buildx imagetools inspect "$first_tag" | awk '/Digest:/ {print $2; exit}')" echo "digest=$digest" >> "$GITHUB_OUTPUT" From bdf1eb641172d7386dc74ef0e1dbf0ae323058d1 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 11:07:40 -0500 Subject: [PATCH 10/27] fix(docker): use consistent output --- .github/actions/docker-build-push/action.yml | 6 +++--- .github/workflows/docker-build-push.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 8ebb15e..c74a088 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -82,7 +82,7 @@ inputs: outputs: image: - description: Image reference (from metadata) + description: Image reference (from metadata / build-push where applicable) value: ${{ inputs.setup-only == 'true' && '' || fromJSON((inputs.metadata-only == 'true' && steps.meta.outputs.json) || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)))['image.name'] }} imageid: description: Image ID from build backend @@ -91,8 +91,8 @@ outputs: description: Image digest value: ${{ (inputs.setup-only == 'true' || inputs.metadata-only == 'true') && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.digest || steps.publish-buildx.outputs.digest)) }} metadata: - description: Build metadata JSON - value: ${{ inputs.setup-only == 'true' && '' || (inputs.metadata-only == 'true' && steps.meta.outputs.json) || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)) }} + description: docker/metadata-action bake JSON (same schema for Buildx digest merge, Buildx push, Warp — excludes setup-only login-only runs) + value: ${{ inputs.setup-only != 'true' && steps.meta.outputs.json || '' }} tags: description: docker/metadata-action computed tags list value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.tags }} diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index b698849..1630510 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -91,7 +91,7 @@ on: description: Image digest value: ${{ jobs.native-merge.outputs.digest || jobs.warp.outputs.digest }} metadata: - description: docker/build-push-action metadata JSON + description: docker/metadata-action bake JSON (stable schema across native-merge and Warp) value: ${{ jobs.native-merge.outputs.metadata || jobs.warp.outputs.metadata }} jobs: From c1d23f61d9e38fc0a8eb4babb9796d7d28573db6 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 11:23:33 -0500 Subject: [PATCH 11/27] fix(helm): reduce package ambiguity on push --- .github/actions/helm-publish-oci/action.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/actions/helm-publish-oci/action.yml b/.github/actions/helm-publish-oci/action.yml index f85057b..e3879a6 100644 --- a/.github/actions/helm-publish-oci/action.yml +++ b/.github/actions/helm-publish-oci/action.yml @@ -191,9 +191,17 @@ runs: REGISTRY_URL: ${{ inputs.registry-url }} run: | set -euo pipefail - helm package "$CHART_FOLDER" --version "$CHART_VERSION" --destination "$CHART_FOLDER" - helm push "$CHART_FOLDER"/*.tgz "$REGISTRY_URL" - rm -f "$CHART_FOLDER"/*.tgz + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' EXIT + helm package "$CHART_FOLDER" --version "$CHART_VERSION" --destination "$tmpdir" + shopt -s nullglob + pkgs=("$tmpdir"/*.tgz) + shopt -u nullglob + if [ "${#pkgs[@]}" -ne 1 ]; then + echo "Expected exactly one chart package in $tmpdir, got ${#pkgs[@]}" >&2 + exit 1 + fi + helm push "${pkgs[0]}" "$REGISTRY_URL" - name: Restore Chart.yaml after override if: inputs.chart-version != '' && always() From 3dc6efa729867008c365950ec2d35e8d0825f8e9 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 11:46:57 -0500 Subject: [PATCH 12/27] fix(docker): fix printf output --- .github/workflows/docker-build-push.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 1630510..d9c6a96 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -253,8 +253,18 @@ jobs: METADATA_JSON: ${{ steps.meta.outputs.metadata }} run: | set -euo pipefail + shopt -s nullglob + manifests=() + for d in *; do + manifests+=("${IMAGE_REF}@sha256:${d}") + done + shopt -u nullglob + if [ "${#manifests[@]}" -eq 0 ]; then + echo "No digest files found under $(pwd)" >&2 + exit 1 + fi docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$METADATA_JSON") \ - $(printf '%s@sha256:%s ' "$IMAGE_REF" *) + "${manifests[@]}" - name: Set image output id: image From 5490f7a575a5542ce50985f705be35f28a9350e7 Mon Sep 17 00:00:00 2001 From: BK Box Date: Tue, 28 Apr 2026 11:48:44 -0500 Subject: [PATCH 13/27] fix(docker): use consistent image output --- .github/actions/docker-build-push/action.yml | 4 ++-- .github/workflows/docker-build-push.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index c74a088..ff3d3ce 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -82,8 +82,8 @@ inputs: outputs: image: - description: Image reference (from metadata / build-push where applicable) - value: ${{ inputs.setup-only == 'true' && '' || fromJSON((inputs.metadata-only == 'true' && steps.meta.outputs.json) || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.metadata || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.metadata || steps.publish-buildx.outputs.metadata)))['image.name'] }} + description: Repository/image name without tag (matches inputs.image — not docker/metadata bake image.name, which can be comma-separated tagged refs) + value: ${{ inputs.setup-only == 'true' && '' || inputs.image }} imageid: description: Image ID from build backend value: ${{ (inputs.setup-only == 'true' || inputs.metadata-only == 'true') && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.imageId || steps.publish-buildx.outputs.imageId)) }} diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index d9c6a96..32d4e4d 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -85,7 +85,7 @@ on: required: false outputs: image: - description: Published image reference + description: Repository/image name without tag (inputs.image — stable across native-merge and Warp) value: ${{ jobs.native-merge.outputs.image || jobs.warp.outputs.image }} digest: description: Image digest From 480525e73122cc9b8ee36d8e9e8665f8f883839a Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 09:28:47 -0500 Subject: [PATCH 14/27] chore(docker): change for testing --- .github/workflows/docker-build-push.yml | 8 ++++---- .github/workflows/helm-publish-oci.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 32d4e4d..be6a889 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -5,7 +5,7 @@ # actions/checkout is the caller, not this repo. The composite ref below must be a # **literal** (not env — env is not allowed in `uses:`). On release, set it to the same # tag you publish (e.g. @v1.0.0) before tagging, so consumers pinning this workflow get a -# matching composite. Default branch: @master. +# matching composite. On feature branches, pin the same ref as this workflow file (not @master). name: Reusable — Docker build and push on: @@ -173,7 +173,7 @@ jobs: - name: Build and push by digest (native) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} @@ -229,7 +229,7 @@ jobs: merge-multiple: true - name: Login and metadata only (reuse composite) - uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions id: meta with: auth-mode: ${{ inputs.auth-mode }} @@ -302,7 +302,7 @@ jobs: - name: Build and push (Warp) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} diff --git a/.github/workflows/helm-publish-oci.yml b/.github/workflows/helm-publish-oci.yml index deac9ec..b3b6780 100644 --- a/.github/workflows/helm-publish-oci.yml +++ b/.github/workflows/helm-publish-oci.yml @@ -67,7 +67,7 @@ jobs: - name: Publish chart id: publish - uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@master + uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@feature/devops-1276-create-shared-github-actions with: auth-mode: ${{ inputs.auth-mode }} chart-folder: ${{ inputs.chart-folder }} From e99227430d8f9190f21b90c8555e5454cabb4762 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 09:36:26 -0500 Subject: [PATCH 15/27] fix(github): fixup bad tr command --- .github/actions/docker-build-push/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index ff3d3ce..6fd2b84 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -140,7 +140,7 @@ runs: run: | set -euo pipefail if [ -r /proc/sys/kernel/random/uuid ]; then - delim="GHA_OUT_$(< /proc/sys/kernel/random/uuid tr -d '-\n\r')" + delim="GHA_OUT_$(tr -d -- '-\n\r' < /proc/sys/kernel/random/uuid)" else delim="GHA_OUT_${RANDOM}_$$_$(date +%s%N)" fi From 4eab368882b9e1a47576272599c56ef81ab11f93 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 09:45:09 -0500 Subject: [PATCH 16/27] fix(docker): use jq to parse output --- .github/workflows/docker-build-push.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index be6a889..ad3ff60 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -283,7 +283,16 @@ jobs: run: | set -euo pipefail first_tag="$(jq -r '.tags[0]' <<< "$METADATA_JSON")" - digest="$(docker buildx imagetools inspect "$first_tag" | awk '/Digest:/ {print $2; exit}')" + # Do not pipe inspect to awk with early exit: imagetools keeps writing the manifest + # tree and hits SIGPIPE under pipefail. Use --format + jq instead. + digest="$( + docker buildx imagetools inspect "$first_tag" --format '{{json .Manifest}}' \ + | jq -r '.digest // empty' + )" + if [ -z "${digest}" ]; then + echo "Could not read manifest digest for ${first_tag}" >&2 + exit 1 + fi echo "digest=$digest" >> "$GITHUB_OUTPUT" warp: From 8befca4bcb4268bdf0150f5db95edc98786d73bd Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 09:52:11 -0500 Subject: [PATCH 17/27] fix(github): missing digetsts --- .github/actions/docker-build-push/action.yml | 4 ++++ .github/workflows/docker-build-push.yml | 23 +++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 6fd2b84..3449e0f 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -183,6 +183,10 @@ runs: with: context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} + # Default provenance/SBOM adds extra manifests; digest merge + ECR then often + # references a descriptor imagetools cannot resolve ("not found"). + provenance: false + sbom: false outputs: type=image,name=${{ inputs.image }},push-by-digest=true,name-canonical=true,push=true labels: ${{ steps.meta.outputs.labels }} build-args: ${{ inputs.build-args }} diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index ad3ff60..4a52a1c 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -198,7 +198,13 @@ jobs: run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" + digest="${digest#sha256:}" + digest="${digest//[[:space:]]/}" + if [[ ! "$digest" =~ ^[0-9a-fA-F]{64}$ ]]; then + echo "Invalid digest from build (expected 64 hex chars): ${{ steps.build.outputs.digest }}" >&2 + exit 1 + fi + touch "/tmp/digests/${digest,,}" - name: Upload digest uses: actions/upload-artifact@v4 @@ -253,14 +259,19 @@ jobs: METADATA_JSON: ${{ steps.meta.outputs.metadata }} run: | set -euo pipefail - shopt -s nullglob manifests=() - for d in *; do + declare -A seen=() + while IFS= read -r -d '' f; do + d="$(basename "$f")" + d="${d,,}" + [[ "$d" =~ ^[0-9a-f]{64}$ ]] || continue + [[ -n "${seen[$d]:-}" ]] && continue + seen[$d]=1 manifests+=("${IMAGE_REF}@sha256:${d}") - done - shopt -u nullglob + done < <(find . -type f -print0) if [ "${#manifests[@]}" -eq 0 ]; then - echo "No digest files found under $(pwd)" >&2 + echo "No valid sha256 digest marker files under $(pwd)" >&2 + find . -ls >&2 || true exit 1 fi docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$METADATA_JSON") \ From 5e97f18dae7e9ada1ed29d2d54b96ae9afef93e2 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 09:55:45 -0500 Subject: [PATCH 18/27] fix(docker): reference stoikov build --- .github/actions/docker-build-push/action.yml | 3 +++ .github/workflows/docker-build-push.yml | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 3449e0f..001b863 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -99,6 +99,9 @@ outputs: labels: description: docker/metadata-action computed labels list value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.labels }} + version: + description: docker/metadata-action version (primary tag component for image:version) + value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.version }} runs: using: composite diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 4a52a1c..405a14c 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -251,6 +251,10 @@ jobs: dockerfile: ${{ inputs.dockerfile }} metadata-only: 'true' + # Same as o2-mm-stoikov merge job: metadata-only composite skips setup-buildx; imagetools needs it. + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Create manifest list and push working-directory: /tmp/digests shell: bash @@ -290,18 +294,21 @@ jobs: id: inspect shell: bash env: - METADATA_JSON: ${{ steps.meta.outputs.metadata }} + IMAGE_REF: ${{ inputs.image }} + VERSION: ${{ steps.meta.outputs.version }} run: | set -euo pipefail - first_tag="$(jq -r '.tags[0]' <<< "$METADATA_JSON")" - # Do not pipe inspect to awk with early exit: imagetools keeps writing the manifest - # tree and hits SIGPIPE under pipefail. Use --format + jq instead. + # Match o2-mm-stoikov: inspect repo:version (metadata-action version == primary tag suffix). + if [ -z "${VERSION}" ]; then + echo "Empty metadata version; cannot inspect merged manifest" >&2 + exit 1 + fi digest="$( - docker buildx imagetools inspect "$first_tag" --format '{{json .Manifest}}' \ + docker buildx imagetools inspect "${IMAGE_REF}:${VERSION}" --format '{{json .Manifest}}' \ | jq -r '.digest // empty' )" if [ -z "${digest}" ]; then - echo "Could not read manifest digest for ${first_tag}" >&2 + echo "Could not read manifest digest for ${IMAGE_REF}:${VERSION}" >&2 exit 1 fi echo "digest=$digest" >> "$GITHUB_OUTPUT" From 22d3efb0560c6f01b83079cbbc6496dbe14e25e1 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 10:06:53 -0500 Subject: [PATCH 19/27] fix(github): fix push race condition --- .github/workflows/docker-build-push.yml | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 405a14c..b21b823 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -76,6 +76,14 @@ on: type: string description: Warp profile (required when build-backend is warp for Fuel projects) required: false + digest-artifact-key: + type: string + description: > + Suffix for digest upload-artifact names. MUST be unique per image when the caller runs this + workflow in a matrix (GitHub replaces artifacts with the same name). Use e.g. matrix.name. + If empty, a 16-char hash of inputs.image is used. + required: false + default: '' secrets: REGISTRY_USERNAME: description: Username for registry-login (omit for pure ECR OIDC) @@ -100,6 +108,7 @@ jobs: runs-on: ${{ inputs.runs-on }} outputs: matrix: ${{ steps.plan.outputs.matrix }} + digest_artifact_key: ${{ steps.artifact-key.outputs.digest_artifact_key }} steps: - name: Build native matrix from requested platforms id: plan @@ -150,6 +159,23 @@ jobs: print(f"matrix={json.dumps({'include': include}, separators=(',', ':'))}") PY + - name: Digest artifact key + id: artifact-key + shell: bash + env: + EXPLICIT: ${{ inputs.digest-artifact-key }} + IMAGE: ${{ inputs.image }} + run: | + set -euo pipefail + if [ -n "${EXPLICIT}" ]; then + key="${EXPLICIT}" + key="${key//[^a-zA-Z0-9._-]/-}" + key="${key:0:120}" + else + key=$(printf '%s' "$IMAGE" | sha256sum | awk '{print substr($1,1,16)}') + fi + printf '%s\n' "digest_artifact_key=$key" >> "$GITHUB_OUTPUT" + native-build: if: ${{ inputs.build-backend != 'warp' }} needs: native-plan @@ -209,14 +235,16 @@ jobs: - name: Upload digest uses: actions/upload-artifact@v4 with: - name: digests-${{ steps.platform.outputs.pair }} + name: digests-${{ steps.platform.outputs.pair }}-${{ needs.native-plan.outputs.digest_artifact_key }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 native-merge: if: ${{ inputs.build-backend != 'warp' }} - needs: native-build + needs: + - native-plan + - native-build runs-on: ${{ inputs.runs-on }} permissions: id-token: write @@ -231,7 +259,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/digests - pattern: digests-* + pattern: digests-*-${{ needs.native-plan.outputs.digest_artifact_key }} merge-multiple: true - name: Login and metadata only (reuse composite) From cef57bf4b6cb1a79b8443d964281786dc32f5fe7 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 10:18:45 -0500 Subject: [PATCH 20/27] fix(github): remove testing references --- .github/workflows/docker-build-push.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index b21b823..0ea3407 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -199,7 +199,7 @@ jobs: - name: Build and push by digest (native) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} @@ -263,7 +263,7 @@ jobs: merge-multiple: true - name: Login and metadata only (reuse composite) - uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master id: meta with: auth-mode: ${{ inputs.auth-mode }} @@ -357,7 +357,7 @@ jobs: - name: Build and push (Warp) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} From 6d51ed742adf52fb2e39ec7cfba0e9b90e7cb5e3 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 10:37:42 -0500 Subject: [PATCH 21/27] fix(github): update cursorbot findings --- .github/actions/docker-build-push/action.yml | 12 ++++++------ .github/workflows/helm-publish-oci.yml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 001b863..cd320c7 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -83,25 +83,25 @@ inputs: outputs: image: description: Repository/image name without tag (matches inputs.image — not docker/metadata bake image.name, which can be comma-separated tagged refs) - value: ${{ inputs.setup-only == 'true' && '' || inputs.image }} + value: ${{ inputs.setup-only != 'true' && inputs.image || '' }} imageid: description: Image ID from build backend - value: ${{ (inputs.setup-only == 'true' || inputs.metadata-only == 'true') && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.imageId || steps.publish-buildx.outputs.imageId)) }} + value: ${{ (inputs.setup-only != 'true' && inputs.metadata-only != 'true') && (inputs.build-backend == 'warp' && steps.publish-warp.outputs.imageId || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.imageId || steps.publish-buildx.outputs.imageId)) || '' }} digest: description: Image digest - value: ${{ (inputs.setup-only == 'true' || inputs.metadata-only == 'true') && '' || (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.digest || steps.publish-buildx.outputs.digest)) }} + value: ${{ (inputs.setup-only != 'true' && inputs.metadata-only != 'true') && (inputs.build-backend == 'warp' && steps.publish-warp.outputs.digest || (inputs.push-by-digest == 'true' && steps.publish-buildx-digest.outputs.digest || steps.publish-buildx.outputs.digest)) || '' }} metadata: description: docker/metadata-action bake JSON (same schema for Buildx digest merge, Buildx push, Warp — excludes setup-only login-only runs) value: ${{ inputs.setup-only != 'true' && steps.meta.outputs.json || '' }} tags: description: docker/metadata-action computed tags list - value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.tags }} + value: ${{ inputs.setup-only != 'true' && steps.meta.outputs.tags || '' }} labels: description: docker/metadata-action computed labels list - value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.labels }} + value: ${{ inputs.setup-only != 'true' && steps.meta.outputs.labels || '' }} version: description: docker/metadata-action version (primary tag component for image:version) - value: ${{ inputs.setup-only == 'true' && '' || steps.meta.outputs.version }} + value: ${{ inputs.setup-only != 'true' && steps.meta.outputs.version || '' }} runs: using: composite diff --git a/.github/workflows/helm-publish-oci.yml b/.github/workflows/helm-publish-oci.yml index b3b6780..deac9ec 100644 --- a/.github/workflows/helm-publish-oci.yml +++ b/.github/workflows/helm-publish-oci.yml @@ -67,7 +67,7 @@ jobs: - name: Publish chart id: publish - uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/helm-publish-oci@master with: auth-mode: ${{ inputs.auth-mode }} chart-folder: ${{ inputs.chart-folder }} From f37dc3895788f64927e03ca86596ed2122e5e790 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 10:59:47 -0500 Subject: [PATCH 22/27] fix(github): prevent unsafe interpolation --- .github/workflows/docker-build-push.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 0ea3407..90cd75a 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -193,8 +193,11 @@ jobs: - name: Derive platform pair id: platform shell: bash + env: + MATRIX_PLATFORM: ${{ matrix.platform }} run: | - platform="${{ matrix.platform }}" + set -euo pipefail + platform="${MATRIX_PLATFORM}" echo "pair=${platform//\//-}" >> "$GITHUB_OUTPUT" - name: Build and push by digest (native) @@ -221,13 +224,16 @@ jobs: - name: Export digest shell: bash + env: + BUILD_DIGEST: ${{ steps.build.outputs.digest }} run: | + set -euo pipefail mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" + digest="${BUILD_DIGEST}" digest="${digest#sha256:}" digest="${digest//[[:space:]]/}" if [[ ! "$digest" =~ ^[0-9a-fA-F]{64}$ ]]; then - echo "Invalid digest from build (expected 64 hex chars): ${{ steps.build.outputs.digest }}" >&2 + echo "Invalid digest from build (expected 64 hex chars): ${BUILD_DIGEST}" >&2 exit 1 fi touch "/tmp/digests/${digest,,}" From 3dab98cdbedc93d1fd2c6bf6735582754af075ab Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 11:01:11 -0500 Subject: [PATCH 23/27] fix(github): prevent unintented globbing --- .github/workflows/docker-build-push.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 90cd75a..71f40ba 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -312,6 +312,7 @@ jobs: find . -ls >&2 || true exit 1 fi + set -f docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$METADATA_JSON") \ "${manifests[@]}" From 341437f48aa75149603587282e04b2e8c6184bc6 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 11:43:06 -0500 Subject: [PATCH 24/27] chore(github): final tests --- .github/workflows/docker-build-push.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 71f40ba..69fa6c3 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -202,7 +202,7 @@ jobs: - name: Build and push by digest (native) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} @@ -269,7 +269,7 @@ jobs: merge-multiple: true - name: Login and metadata only (reuse composite) - uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions id: meta with: auth-mode: ${{ inputs.auth-mode }} @@ -364,7 +364,7 @@ jobs: - name: Build and push (Warp) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@master + uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} From 51a1bf732d6c5be4fbb119a141f20959c3d9c975 Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 11:47:44 -0500 Subject: [PATCH 25/27] Revert "chore(github): final tests" This reverts commit 341437f48aa75149603587282e04b2e8c6184bc6. --- .github/workflows/docker-build-push.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 69fa6c3..71f40ba 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -202,7 +202,7 @@ jobs: - name: Build and push by digest (native) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} @@ -269,7 +269,7 @@ jobs: merge-multiple: true - name: Login and metadata only (reuse composite) - uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master id: meta with: auth-mode: ${{ inputs.auth-mode }} @@ -364,7 +364,7 @@ jobs: - name: Build and push (Warp) id: build - uses: FuelLabs/github-actions/.github/actions/docker-build-push@feature/devops-1276-create-shared-github-actions + uses: FuelLabs/github-actions/.github/actions/docker-build-push@master with: auth-mode: ${{ inputs.auth-mode }} aws-role-arn: ${{ inputs.aws-role-arn }} From fdd6b12ce0a621c172bd4b323d92e0fe22e3d83e Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 11:59:35 -0500 Subject: [PATCH 26/27] fix(github): update self workflow call reference --- .github/workflows/publish-docker-image.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index 497438b..ae45844 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -1,5 +1,5 @@ # Legacy callable — GHCR username/password + metadata inputs. -# Implementation: forwards to `docker-build-push.yml` in this repo (same ref as this file). +# Nested jobs use FuelLabs/github-actions/... (not ./): cross-repo workflow_call resolves ./ to the caller. # Pin: `uses: FuelLabs/github-actions/.github/workflows/publish-docker-image.yml@` name: Build and Publish Docker Image @@ -53,7 +53,7 @@ on: jobs: build-and-publish-docker-image: - uses: ./.github/workflows/docker-build-push.yml + uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@master with: runs-on: ubuntu-latest auth-mode: registry-login @@ -72,7 +72,7 @@ jobs: notify-slack-on-failure: needs: [build-and-publish-docker-image] if: failure() - uses: ./.github/workflows/notify-slack-action.yml + uses: FuelLabs/github-actions/.github/workflows/notify-slack-action.yml@master secrets: GH_TOKEN: ${{ secrets.GH_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} From d26b7e776b101c3cd008aca5a7bf77d7d79b6a2f Mon Sep 17 00:00:00 2001 From: BK Box Date: Mon, 11 May 2026 12:22:01 -0500 Subject: [PATCH 27/27] fix(github): allow backwards compatible publish-docker --- .github/workflows/publish-docker-image.yml | 39 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index ae45844..92d4253 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -19,7 +19,10 @@ on: default: "deployment/Dockerfile" images: - description: "Docker image names (e.g. 'ghcr.io/FuelLabs/repository-name')" + description: > + One or more image repository names (comma-separated). Each is published separately; + multi-value strings must not be passed as a single docker-build-push image input (commas + break the buildx outputs CSV). Same idea as docker/metadata-action multi-image lists. required: true type: string @@ -52,14 +55,42 @@ on: required: true jobs: + prepare-images: + runs-on: ubuntu-latest + outputs: + images_json: ${{ steps.split.outputs.images_json }} + steps: + - name: Split images input + id: split + shell: bash + env: + IMAGES: ${{ inputs.images }} + run: | + set -euo pipefail + python3 - <<'PY' >> "$GITHUB_OUTPUT" + import json + import os + + raw = os.environ.get("IMAGES", "") + parts = [p.strip() for p in raw.split(",") if p.strip()] + if not parts: + raise SystemExit("images input must contain at least one image name") + print(f"images_json={json.dumps(parts)}") + PY + build-and-publish-docker-image: + needs: prepare-images + strategy: + fail-fast: false + matrix: + image: ${{ fromJSON(needs.prepare-images.outputs.images_json) }} uses: FuelLabs/github-actions/.github/workflows/docker-build-push.yml@master with: runs-on: ubuntu-latest auth-mode: registry-login registry: ghcr.io dockerfile: ${{ inputs.docker_file }} - image: ${{ inputs.images }} + image: ${{ matrix.image }} tags: ${{ inputs.tags }} labels: ${{ inputs.labels }} flavor: ${{ inputs.flavor }} @@ -70,8 +101,8 @@ jobs: REGISTRY_PASSWORD: ${{ secrets.GITHUB_CONTAINER_PASSWORD }} notify-slack-on-failure: - needs: [build-and-publish-docker-image] - if: failure() + needs: [prepare-images, build-and-publish-docker-image] + if: ${{ always() && (needs.prepare-images.result == 'failure' || needs['build-and-publish-docker-image'].result == 'failure') }} uses: FuelLabs/github-actions/.github/workflows/notify-slack-action.yml@master secrets: GH_TOKEN: ${{ secrets.GH_TOKEN }}