Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4fd0971
feat(github): expand github workflows and actions
gondoi Apr 27, 2026
cd800e7
fix(github): fix issues found by cursorbot
gondoi Apr 27, 2026
e7e51b6
feat(docker): add options for build and helm destinations
gondoi Apr 28, 2026
6293511
fix(helm): output to correct directory
gondoi Apr 28, 2026
2e1f35f
fix(docker): don't spin up an unused instance
gondoi Apr 28, 2026
e5505c8
fix(helm): update permissions for aws oidc
gondoi Apr 28, 2026
f51926f
fix(docker): remove duplicate IDs
gondoi Apr 28, 2026
5b3ad60
fix(docker): avoid shell/heredoc injection
gondoi Apr 28, 2026
3c68b88
fix(docker): remove potential duplicate metadata info
gondoi Apr 28, 2026
bdf1eb6
fix(docker): use consistent output
gondoi Apr 28, 2026
c1d23f6
fix(helm): reduce package ambiguity on push
gondoi Apr 28, 2026
3dc6efa
fix(docker): fix printf output
gondoi Apr 28, 2026
5490f7a
fix(docker): use consistent image output
gondoi Apr 28, 2026
480525e
chore(docker): change for testing
gondoi May 11, 2026
e992274
fix(github): fixup bad tr command
gondoi May 11, 2026
4eab368
fix(docker): use jq to parse output
gondoi May 11, 2026
8befca4
fix(github): missing digetsts
gondoi May 11, 2026
5e97f18
fix(docker): reference stoikov build
gondoi May 11, 2026
22d3efb
fix(github): fix push race condition
gondoi May 11, 2026
cef57bf
fix(github): remove testing references
gondoi May 11, 2026
6d51ed7
fix(github): update cursorbot findings
gondoi May 11, 2026
f37dc38
fix(github): prevent unsafe interpolation
gondoi May 11, 2026
3dab98c
fix(github): prevent unintented globbing
gondoi May 11, 2026
341437f
chore(github): final tests
gondoi May 11, 2026
51a1bf7
Revert "chore(github): final tests"
gondoi May 11, 2026
fdd6b12
fix(github): update self workflow call reference
gondoi May 11, 2026
d26b7e7
fix(github): allow backwards compatible publish-docker
gondoi May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# 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 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` | 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 |

**Not in scope for these composites:** PR-only Helm, `helm-cleanup-pr`, preview charts.

**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**.

## 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. Composite steps use a **fully qualified** `uses: FuelLabs/github-actions/.github/actions/<name>@<ref>`, where **`<ref>` 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:** in `docker-build-push.yml` and `helm-publish-oci.yml`, set the `...@<ref>` 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

| 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:
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** — 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://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`):

```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.
227 changes: 227 additions & 0 deletions .github/actions/docker-build-push/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Bump third-party majors via PR after review (supply chain).
name: Docker build and push
description: Build and push a container image (ECR private/public OIDC or registry login; Buildx or Warp).

inputs:
auth-mode:
description: 'ecr-oidc | ecr-public-oidc | registry-login'
required: true
aws-role-arn:
description: IAM role ARN for ECR (auth-mode ecr-oidc or ecr-public-oidc)
required: false
aws-region:
description: AWS region for ECR (public ECR generally uses us-east-1)
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-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'
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
default: 'true'

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 || '' }}
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)) || '' }}
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)) || '' }}
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 || '' }}
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
steps:
- name: Configure AWS credentials (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 (private)
if: inputs.auth-mode == 'ecr-oidc'
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
Comment thread
gondoi marked this conversation as resolved.

- 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
with:
registry: ${{ inputs.registry }}
username: ${{ inputs.username }}
password: ${{ inputs.password }}

- 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
INPUT_TAGS: ${{ inputs.tags }}
run: |
set -euo pipefail
if [ -r /proc/sys/kernel/random/uuid ]; then
delim="GHA_OUT_$(tr -d -- '-\n\r' < /proc/sys/kernel/random/uuid)"
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
if: inputs.setup-only != 'true'
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.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.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.metadata-only != 'true' && inputs.build-backend == 'buildx' && inputs.push-by-digest == 'true'
uses: docker/build-push-action@v6
id: publish-buildx-digest
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 }}
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.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:
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: ${{ 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.setup-only != 'true' && inputs.metadata-only != 'true' && 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 }}
Loading
Loading