Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 79 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ on:
required: false
type: boolean
default: false
enable_sign:
description: 'Sign the pushed image(s) by digest with cosign (keyless/Sigstore). Requires the calling job to grant id-token: write.'
required: false
type: boolean
default: false
enable_sbom:
description: 'Generate an SPDX SBOM for the pushed image(s) and attach it as a cosign attestation. Requires the calling job to grant id-token: write.'
required: false
type: boolean
default: false
checkout_submodules:
description: 'Whether to checkout git submodules: "true", "recursive", or "false"'
required: false
Expand All @@ -108,6 +118,10 @@ on:
required: false
type: number
default: 1
outputs:
digest:
description: 'Digest (sha256:…) of the pushed image manifest'
value: ${{ jobs.build.outputs.digest }}
secrets:
artifactory_username:
description: 'Username for JFrog Artifactory. Required if enable_jfrog is true'
Expand All @@ -130,6 +144,8 @@ jobs:
build:
name: Build and Push Image to Repository
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
Comment thread
cursor[bot] marked this conversation as resolved.
env:
JFROG_IMAGE_URL: ${{ inputs.artifactory_repository_url }}/${{ inputs.image_artifact_name }}
ECR_IMAGE_URL: ${{ inputs.ecr_repository_url }}/${{ inputs.image_artifact_name }}
Expand Down Expand Up @@ -306,3 +322,66 @@ jobs:
- name: Output image digest
run: |
echo "Image digest: ${{ steps.build.outputs.digest }}"

- name: Install cosign
if: ${{ inputs.enable_sign || inputs.enable_sbom }}
uses: sigstore/cosign-installer@v3

- name: Install Syft
id: syft
if: ${{ inputs.enable_sbom }}
uses: anchore/sbom-action/download-syft@v0

- name: Sign image(s) with cosign (keyless)
if: ${{ inputs.enable_sign }}
env:
COSIGN_YES: 'true'
DIGEST: ${{ steps.build.outputs.digest }}
ENABLE_JFROG: ${{ inputs.enable_jfrog }}
ENABLE_ECR: ${{ inputs.enable_public_ecr }}
run: |
set -euo pipefail
if [ -z "$DIGEST" ]; then
echo "Error: image digest is empty, cannot sign" >&2
exit 1
fi
if [ "$ENABLE_JFROG" == "true" ]; then
echo "Signing ${JFROG_IMAGE_URL}@${DIGEST}"
cosign sign "${JFROG_IMAGE_URL}@${DIGEST}"
fi
if [ "$ENABLE_ECR" == "true" ]; then
echo "Signing ${ECR_IMAGE_URL}@${DIGEST}"
cosign sign "${ECR_IMAGE_URL}@${DIGEST}"
fi

- name: Generate and attest SBOM (keyless)
if: ${{ inputs.enable_sbom }}
env:
COSIGN_YES: 'true'
DIGEST: ${{ steps.build.outputs.digest }}
ENABLE_JFROG: ${{ inputs.enable_jfrog }}
ENABLE_ECR: ${{ inputs.enable_public_ecr }}
SYFT: ${{ steps.syft.outputs.cmd }}
run: |
set -euo pipefail
if [ -z "$DIGEST" ]; then
echo "Error: image digest is empty, cannot generate SBOM" >&2
exit 1
fi
if [ -z "$SYFT" ]; then
echo "Error: Syft command path is empty, cannot generate SBOM" >&2
exit 1
fi
attest_sbom() {
ref="$1"
echo "Generating SBOM for ${ref}"
"$SYFT" scan "registry:${ref}" -o spdx-json=sbom.spdx.json
echo "Attesting SBOM for ${ref}"
cosign attest --predicate sbom.spdx.json --type spdxjson "${ref}"
}
Comment thread
cursor[bot] marked this conversation as resolved.
if [ "$ENABLE_JFROG" == "true" ]; then
attest_sbom "${JFROG_IMAGE_URL}@${DIGEST}"
fi
if [ "$ENABLE_ECR" == "true" ]; then
attest_sbom "${ECR_IMAGE_URL}@${DIGEST}"
fi
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ jobs:

Builds a multi-platform Docker image and pushes it to JFrog Artifactory and/or AWS Public
ECR. Optionally frees runner disk space, scans the `amd64` image with Anchore Grype before
pushing, applies extra tags, and emits provenance attestation. Uses a registry-based build
cache.
pushing, applies extra tags, and emits provenance attestation. Can also keyless-sign the
pushed image(s) with [cosign](https://github.com/sigstore/cosign) and attach an SPDX SBOM
(generated by [Syft](https://github.com/anchore/syft)) as a cosign attestation. Uses a
registry-based build cache.

**Usage**

Expand Down Expand Up @@ -84,6 +86,8 @@ jobs:
| `free_disk_space_tool_cache_storage` | Free disk space used by the tool cache | boolean | false | `false` |
| `free_disk_space_large_packages` | Free disk space used by large packages | boolean | false | `false` |
| `enable_provenance` | Enable provenance attestation for supply-chain security | boolean | false | `false` |
| `enable_sign` | Keyless-sign the pushed image(s) by digest with cosign | boolean | false | `false` |
| `enable_sbom` | Generate an SPDX SBOM per image (Syft) and attach it as a cosign attestation | boolean | false | `false` |

**Secrets**

Expand All @@ -93,6 +97,54 @@ jobs:
| `artifactory_password` | Password for JFrog Artifactory | Required if `enable_jfrog` |
| `ecr_role_arn` | Role ARN to pull/push images to AWS Public ECR | Required if `enable_public_ecr` |

**Outputs**

| Name | Description |
| -------- | ------------------------------------------------- |
| `digest` | Digest (`sha256:…`) of the pushed image manifest |

**Signing & SBOM (cosign / Syft)**

`enable_sign` and `enable_sbom` use **keyless** Sigstore signing: the identity is bound to
the GitHub Actions OIDC token and recorded in the public Rekor transparency log — there is no
long-lived key to manage. Signatures/attestations are stored in the same registry as the
image, so the login credentials already provided are reused.

Because keyless signing needs an OIDC token, the **calling job must grant `id-token: write`**
(the reusable workflow inherits the caller's permissions and cannot elevate them):

```yaml
jobs:
build:
permissions:
contents: read
id-token: write # required for keyless cosign signing / SBOM attestation
uses: truefoundry/github-workflows-public/.github/workflows/build.yml@main
with:
artifactory_registry_url: tfy.jfrog.io
artifactory_repository_url: tfy.jfrog.io/tfy-images
image_artifact_name: mlfoundry-server
image_tag: ${{ github.sha }}
enable_sign: true
enable_sbom: true
secrets:
artifactory_username: ${{ secrets.ARTIFACTORY_USERNAME }}
artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }}
```

Verify a signed image:

```bash
cosign verify <repository>/<image>@<digest> \
--certificate-identity-regexp 'https://github.com/<org>/<repo>/.github/workflows/.*@refs/tags/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com

# Inspect the attached SBOM attestation
cosign verify-attestation <repository>/<image>@<digest> --type spdxjson \
--certificate-identity-regexp 'https://github.com/<org>/<repo>/.github/workflows/.*@refs/tags/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```

---

### `build-and-push-soci-index.yml` — Build and push SOCI index
Expand Down