Skip to content
Merged
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
43 changes: 27 additions & 16 deletions .claude/references/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,38 @@
Short repo-specific rules. Full rationale:
[`.github/instructions/github-actions-ci-cd-best-practices.instructions.md`](../../.github/instructions/github-actions-ci-cd-best-practices.instructions.md).

## The one workflow: `.github/workflows/build-and-push.yml`
## The one workflow: `.github/workflows/build-and-push.yml` (thin caller)

- **Triggers:** PRs to `main` and version tags `v*.*.*` (pushes to `main` do not build).
- **Job graph:** `build` (matrix: sf-ci, sf-devcontainer, sf-bulk) → `test`
(pytest-testinfra + Trivy) → `push` (tags only) → `release` (tags only).
- **PRs build + test but never push or release.** Push/release run **only** on `v*.*.*` tags.
- **Job graph:** `images` (matrix: sf-ci, sf-devcontainer, sf-bulk — each invocation calls the
shared reusable workflow
`Gforce-Innovation-Kft/shared-github-actions/.github/workflows/docker-build-test-push.yml@v1`,
which runs build → test → push for that one image) → `release` (tags only, local).
- **PRs build + test but never push or release.** Push/release run **only** on `v*.*.*` tags
(the caller computes `push: startsWith(github.ref, 'refs/tags/v')`).

## Rules when editing the workflow
## Rules when editing the caller

- Pin actions to a major version tag (`@v4`) or SHA — never `@main`/`@latest`.
- Keep `permissions` least-privilege at the workflow level; the `release` job needs
`contents: write`, nothing else does.
- The `test` job runs `pytest tests/test_sf_<image>.py` (pytest-testinfra) plus Trivy.
- Keep multi-arch (`linux/amd64,linux/arm64`), `sbom: true`, `provenance: true` on the push job.
- Registry is **Docker Hub only** (`gforceinnovation/*`) via `DOCKERHUB_USERNAME` +
`secrets.DOCKERHUB_TOKEN`. Do not add other registries without an explicit decision.
- Semver tag expansion (metadata-action): `{{version}}`, `{{major}}.{{minor}}`, `{{major}}`, `latest`.
- Per-image pipeline changes (build/test/push/signing) belong in **shared-github-actions**,
not here. Do not copy that logic back into this repo.
- The `images` job must grant the reusable workflow its permissions:
`contents: read`, `checks: write`, `pull-requests: write`, `security-events: write`,
`id-token: write` (cosign keyless signing).
- Pin the reusable workflow to `@v1` (the shared repo's release process maintains the floating
major tag). Local actions pin to a major version tag (`@v4`) or SHA — never `@main`/`@latest`.
- Registry is **Docker Hub only** (`gforceinnovation/*`) via the `dockerhub-token` secret
(`secrets.DOCKERHUB_TOKEN`). Do not add other registries without an explicit decision.
- **Tag scheme:** `{{version}}` + `latest` only. Rolling `{{major}}.{{minor}}`/`{{major}}` tags
were deliberately dropped (existing ones stay frozen at 1.6.1).
- Images are **cosign-signed** (keyless, GitHub OIDC) on tag pushes. The certificate identity is
the shared workflow's path — renaming/moving that file in shared-github-actions breaks every
documented `cosign verify` command.
- Respect `.yamllint` (120-col, 2-space). The `.github/hooks/pre-commit` hook lints staged YAML.

## Release job

On a `v*.*.*` tag, after push succeeds, create a GitHub Release with generated notes
augmented by the matching `CHANGELOG.md` section. See [devops.md](./devops.md) for the tag
→ release flow and the [releasing skill](../skills/releasing/SKILL.md).
On a `v*.*.*` tag, after all three image pipelines succeed, the local `release` job creates a
GitHub Release: generated notes + the matching `CHANGELOG.md` section + per-image tool-version
tables (Node, npm, SF CLI, user plugins) downloaded from the `version-report-*` artifacts the
shared push jobs upload. See [devops.md](./devops.md) for the tag → release flow and the
[releasing skill](../skills/releasing/SKILL.md).
228 changes: 40 additions & 188 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Build and Push Docker Images

# Thin caller: per-image build → test → push lives in the shared reusable
# workflow (see docs/reusable-workflow-migration-design.md). This workflow
# fans out over the three images and keeps the repo-specific release job.
on:
# Build/test/push only on version tags; PRs validate before merge.
# Pushes to main do not build (avoids a redundant run right before tagging).
Expand All @@ -14,210 +17,44 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
security-events: write

env:
DOCKERHUB_USERNAME: gforceinnovation

jobs:
build:
name: Build Docker Images
runs-on: ubuntu-latest
permissions:
contents: read

images:
name: ${{ matrix.image.name }}
strategy:
fail-fast: false
matrix:
image:
- name: sf-devcontainer
context: ./sf-devcontainer
description: Full-featured Salesforce devcontainer — SF CLI v2, Node 24, Java 17, zsh
- name: sf-ci
context: ./sf-ci
description: Minimal Salesforce CI/CD runner — SF CLI v2, Node 24, Java 17
- name: sf-bulk
context: ./sf-bulk

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: ${{ matrix.image.context }}
load: true
tags: ${{ matrix.image.name }}:test
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Report image size
run: |
SIZE=$(docker image inspect ${{ matrix.image.name }}:test --format='{{.Size}}')
echo "## ${{ matrix.image.name }}" >> $GITHUB_STEP_SUMMARY
echo "Image size: $(numfmt --to=iec-i --suffix=B $SIZE)" >> $GITHUB_STEP_SUMMARY

- name: Export Docker image
run: |
docker save ${{ matrix.image.name }}:test | gzip > ${{ matrix.image.name }}.tar.gz

- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.image.name }}-image
path: ${{ matrix.image.name }}.tar.gz
retention-days: 1

test:
name: Test Docker Images
runs-on: ubuntu-latest
needs: build
description: Ultra-light Alpine SF CLI image for bulk org operations — no Java
permissions:
contents: read
checks: write
pull-requests: write
security-events: write

strategy:
fail-fast: false
matrix:
image:
- name: sf-devcontainer
- name: sf-ci
- name: sf-bulk

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: ${{ matrix.image.name }}-image

- name: Load Docker image
run: |
docker load < ${{ matrix.image.name }}.tar.gz

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install test dependencies
run: |
pip install -r tests/requirements.txt

- name: Run tests for ${{ matrix.image.name }}
run: |
TEST_FILE=test_$(echo "${{ matrix.image.name }}" | tr '-' '_').py
pytest tests/${TEST_FILE} -v --junitxml=test-results-${{ matrix.image.name }}.xml

- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: test-results-${{ matrix.image.name }}.xml
check_name: Test Results - ${{ matrix.image.name }}

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: ${{ matrix.image.name }}:test
format: 'sarif'
output: 'trivy-results.sarif'

- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'

push:
name: Tag and Push Docker Images
runs-on: ubuntu-latest
needs: test
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read

strategy:
matrix:
image:
- name: sf-devcontainer
context: ./sf-devcontainer
description: Full-featured Salesforce devcontainer — SF CLI v2, Node 24, Java 17, zsh
- name: sf-ci
context: ./sf-ci
description: Minimal Salesforce CI/CD runner — SF CLI v2, Node 24, Java 17
- name: sf-bulk
context: ./sf-bulk
description: Ultra-light Alpine SF CLI image for bulk org operations — no Java

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Download image artifact
uses: actions/download-artifact@v4
with:
name: ${{ matrix.image.name }}-image

- name: Load Docker image
run: |
docker load < ${{ matrix.image.name }}.tar.gz

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image.name }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=true

- name: Re-tag and push for multi-platform
uses: docker/build-push-action@v5
with:
context: ${{ matrix.image.context }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
sbom: true
provenance: true

# Requires DOCKERHUB_TOKEN to have read/write scope (read-only tokens cannot
# update repository descriptions).
- name: Sync README to Docker Hub
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image.name }}
short-description: ${{ matrix.image.description }}
readme-filepath: ${{ matrix.image.context }}/README.md
# id-token is only consumed by the reusable push job (cosign keyless
# signing), which never runs on pull requests.
id-token: write
# @v1 is the shared repo's floating major tag, moved by its release process.
uses: Gforce-Innovation-Kft/shared-github-actions/.github/workflows/docker-build-test-push.yml@v1
with:
image-name: ${{ matrix.image.name }}
context: ${{ matrix.image.context }}
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
image-description: ${{ matrix.image.description }}
secrets:
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}

release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: push
needs: images
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
Expand All @@ -228,8 +65,17 @@ jobs:
with:
fetch-depth: 0

- name: Extract changelog section for this tag
id: changelog
# Each reusable push job uploads a version-report-<image> artifact with
# the tool versions read from the built image.
- name: Download version reports
uses: actions/download-artifact@v4
with:
pattern: version-report-*
path: version-reports
merge-multiple: true

- name: Assemble release notes
id: notes
run: |
VERSION="${GITHUB_REF_NAME#v}"
NOTES_FILE="$(mktemp)"
Expand All @@ -244,12 +90,18 @@ jobs:
if [ ! -s "$NOTES_FILE" ]; then
echo "Release ${GITHUB_REF_NAME} — see the auto-generated notes below." > "$NOTES_FILE"
fi
{
echo
echo "## Image tool versions"
echo
cat version-reports/version-report-*.md
} >> "$NOTES_FILE"
echo "notes_file=${NOTES_FILE}" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: ${{ steps.changelog.outputs.notes_file }}
body_path: ${{ steps.notes.outputs.notes_file }}
generate_release_notes: true
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
per-image READMEs
- `sf-bulk/.dockerignore` (previously missing; sf-ci and sf-devcontainer already had one)
- CI: Docker Hub README/description sync on release (`peter-evans/dockerhub-description`)
- CI: **keyless cosign signing** (GitHub OIDC) of every pushed image; verification commands
documented in the root and per-image READMEs
- Release notes now include per-image tool-version tables (Node, npm, SF CLI, user plugins)
read from the built images

### Changed
- GitHub repo metadata: description, topics, and Docker Hub homepage link set
- CI: dropped the unused `packages: write` permission (images push to Docker Hub, not GHCR)
- CI: the per-image build → test → push pipeline moved to the shared
`docker-build-test-push` reusable workflow in `shared-github-actions`;
`build-and-push.yml` is now a thin matrix caller with a local release job
- **Docker tag scheme: releases publish `X.Y.Z` + `latest` only** — rolling `:1` / `:1.6`
tags are no longer pushed (existing ones stay frozen at 1.6.1); pin an exact version or
track `latest`

### Security
- All Dockerfiles: base images now pinned by tag **plus multi-arch index digest**
Expand Down
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,18 @@ docker buildx build --platform linux/amd64,linux/arm64 --tag gforceinnovation/sf

## CI/CD Workflows

### `.github/workflows/build-and-push.yml` -- Build and Push
### `.github/workflows/build-and-push.yml` -- Build and Push (thin caller)
- **Triggers:** PRs to `main` and version tags (`v*.*.*`). Pushes to `main` do not build.
- **Jobs:** build (matrix) -> test (pytest-testinfra + Trivy) -> push (Docker Hub on version tags only, with Docker Hub README sync) -> release (GitHub Release on version tags only).
- Pushes with semver tags (e.g., `1.2.3`, `1.2`, `1`, `latest`). Generates SBOM and provenance attestations.
- The per-image **build -> test -> push** pipeline lives in the shared reusable workflow
`Gforce-Innovation-Kft/shared-github-actions/.github/workflows/docker-build-test-push.yml@v1`;
this repo's workflow fans out over the three images with a matrix and keeps only the
repo-specific `release` job (CHANGELOG section + per-image tool-version tables assembled from
the `version-report-*` artifacts).
- On version tags: multi-arch push to Docker Hub with **two tags only** (`1.2.3` + `latest` —
rolling `1.2`/`1` tags are no longer published), SBOM + provenance attestations, and a
**keyless cosign signature** (GitHub OIDC; identity = the shared workflow's path).
- Registry is **Docker Hub only** (`gforceinnovation/*`).
- Do not copy per-image pipeline logic back into this repo — change the shared workflow instead.

### Release Process
```bash
Expand Down
Loading
Loading