diff --git a/.claude/references/github-actions.md b/.claude/references/github-actions.md index 3604b4e..fb2ccec 100644 --- a/.claude/references/github-actions.md +++ b/.claude/references/github-actions.md @@ -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_.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). diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index c5e0ad1..9a95aca 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -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). @@ -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 @@ -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- 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)" @@ -244,6 +90,12 @@ 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 @@ -251,5 +103,5 @@ jobs: 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e1319a6..0e68185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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** diff --git a/CLAUDE.md b/CLAUDE.md index 0e697a0..4ba50bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index 8ca9361..c6a4aed 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ All three images ship Node.js 24 and [Salesforce CLI v2](https://developer.sales All three are published to Docker Hub under [`gforceinnovation`](https://hub.docker.com/u/gforceinnovation): ```bash -docker pull gforceinnovation/sf-ci:1 # minimal CI runner -docker pull gforceinnovation/sf-devcontainer:1 # VS Code devcontainer -docker pull gforceinnovation/sf-bulk:1 # Alpine bulk-ops image +docker pull gforceinnovation/sf-ci:latest # minimal CI runner +docker pull gforceinnovation/sf-devcontainer:latest # VS Code devcontainer +docker pull gforceinnovation/sf-bulk:latest # Alpine bulk-ops image ``` Or extend one in your own Dockerfile: @@ -62,7 +62,7 @@ flowchart TD jobs: deploy: runs-on: ubuntu-latest - container: gforceinnovation/sf-ci:1 + container: gforceinnovation/sf-ci:latest steps: - uses: actions/checkout@v4 - name: Authenticate to Salesforce @@ -96,19 +96,17 @@ docker run --rm -v "$(pwd):/workspace" gforceinnovation/sf-bulk:latest sf org li ## Supported tags -Images are published with semver tags on every version release, in the style of the official -Docker Library images: +Every version release publishes exactly two tags per image: | Tag | Moves? | Points at | |-----|--------|-----------| -| `1.4.0` | **immutable** | one exact release — pin this in production | -| `1.4` | moving | latest `1.4.x` | -| `1` | moving | latest `1.x.x` | +| `1.6.1` | **immutable** | one exact release — pin this in production | | `latest` | moving | most recent release | -Pin an immutable tag (`gforceinnovation/sf-ci:1.4.0`) for reproducible pipelines; track a -moving tag (`:1`) to pick up patch and minor updates automatically. The tag matrix is generated -by CI from the pushed git tag (see [`.github/workflows/build-and-push.yml`](.github/workflows/build-and-push.yml)). +Pin an immutable tag (`gforceinnovation/sf-ci:1.6.1`) for reproducible pipelines; track +`:latest` to pick up updates automatically. Rolling major/minor tags (`:1`, `:1.6`) are no +longer published — existing ones stay frozen at 1.6.1. The tags are generated by CI from the +pushed git tag (see [`.github/workflows/build-and-push.yml`](.github/workflows/build-and-push.yml)). ## Security & provenance @@ -118,6 +116,19 @@ Every build in CI: to GitHub Security (code scanning). - **Generates an SBOM** and **provenance attestations** on push, so consumers can verify how and from what each image was built. +- **Signs** every pushed image with [cosign](https://docs.sigstore.dev/) (keyless, GitHub OIDC); + the signature is recorded in the Rekor transparency log. Verify any image with: + + ```bash + cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp \ + '^https://github\.com/Gforce-Innovation-Kft/shared-github-actions/\.github/workflows/docker-build-test-push\.yml@.+$' \ + gforceinnovation/sf-ci:latest + ``` + + The identity is the shared CI workflow that built and pushed the image — no keys to manage + or leak. Swap in `sf-devcontainer` / `sf-bulk` and any published tag. Found a vulnerability? See [`SECURITY.md`](SECURITY.md). @@ -166,8 +177,12 @@ git push origin v1.5.0 ``` CI then builds all three images multi-arch, runs the tests + Trivy scan, pushes to Docker Hub -with semver tags plus SBOM and provenance, and opens a GitHub Release with notes drawn from the -matching `CHANGELOG.md` section. +(tags `X.Y.Z` + `latest`) with SBOM, provenance, and a keyless cosign signature, and opens a +GitHub Release with notes drawn from the matching `CHANGELOG.md` section plus per-image +tool-version tables (Node, npm, SF CLI, plugins) read from the built images. The per-image +pipeline lives in the shared +[`docker-build-test-push`](https://github.com/Gforce-Innovation-Kft/shared-github-actions) +reusable workflow; this repo's workflow is a thin caller. ## AI-Assisted Development diff --git a/sf-bulk/README.md b/sf-bulk/README.md index 9dff4ea..2c6713f 100644 --- a/sf-bulk/README.md +++ b/sf-bulk/README.md @@ -10,15 +10,28 @@ operations. No Java, minimal footprint — kept **under 600 MB** uncompressed. ## Pull ```bash -docker pull gforceinnovation/sf-bulk:1 +docker pull gforceinnovation/sf-bulk:latest ``` ```dockerfile FROM gforceinnovation/sf-bulk:1.6.1 ``` -Multi-arch: `linux/amd64` + `linux/arm64`. Tags follow the repo-wide -[semver tag matrix](../README.md#supported-tags) (`1.6.1` immutable; `1.6`, `1`, `latest` moving). +Multi-arch: `linux/amd64` + `linux/arm64`. Two tags per release +(see [supported tags](../README.md#supported-tags)): the exact version (`1.6.1`, immutable — +pin this in production) and `latest` (moving). + +### Verify the signature + +Every published image is signed with cosign (keyless, GitHub OIDC): + +```bash +cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp \ + '^https://github\.com/Gforce-Innovation-Kft/shared-github-actions/\.github/workflows/docker-build-test-push\.yml@.+$' \ + gforceinnovation/sf-bulk:latest +``` ## Features diff --git a/sf-ci/README.md b/sf-ci/README.md index a6892ff..671ccc8 100644 --- a/sf-ci/README.md +++ b/sf-ci/README.md @@ -11,15 +11,28 @@ the build if editors or interactive shells sneak in. ## Pull ```bash -docker pull gforceinnovation/sf-ci:1 +docker pull gforceinnovation/sf-ci:latest ``` ```dockerfile FROM gforceinnovation/sf-ci:1.6.1 ``` -Multi-arch: `linux/amd64` + `linux/arm64`. Tags follow the repo-wide -[semver tag matrix](../README.md#supported-tags) (`1.6.1` immutable; `1.6`, `1`, `latest` moving). +Multi-arch: `linux/amd64` + `linux/arm64`. Two tags per release +(see [supported tags](../README.md#supported-tags)): the exact version (`1.6.1`, immutable — +pin this in production) and `latest` (moving). + +### Verify the signature + +Every published image is signed with cosign (keyless, GitHub OIDC): + +```bash +cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp \ + '^https://github\.com/Gforce-Innovation-Kft/shared-github-actions/\.github/workflows/docker-build-test-push\.yml@.+$' \ + gforceinnovation/sf-ci:latest +``` ## What's inside @@ -39,7 +52,7 @@ Multi-arch: `linux/amd64` + `linux/arm64`. Tags follow the repo-wide jobs: deploy: runs-on: ubuntu-latest - container: gforceinnovation/sf-ci:1 + container: gforceinnovation/sf-ci:latest steps: - uses: actions/checkout@v4 - name: Authenticate to Salesforce @@ -54,7 +67,7 @@ jobs: ```yaml deploy: - image: gforceinnovation/sf-ci:1 + image: gforceinnovation/sf-ci:latest script: - echo "$SF_AUTH_URL" > authfile - sf org login sfdx-url --sfdx-url-file authfile diff --git a/sf-devcontainer/README.md b/sf-devcontainer/README.md index ca22369..c27283d 100644 --- a/sf-devcontainer/README.md +++ b/sf-devcontainer/README.md @@ -13,15 +13,28 @@ plugins on top of the [`sf-ci`](../sf-ci/README.md) toolchain. ## Pull ```bash -docker pull gforceinnovation/sf-devcontainer:1 +docker pull gforceinnovation/sf-devcontainer:latest ``` ```dockerfile FROM gforceinnovation/sf-devcontainer:1.6.1 ``` -Multi-arch: `linux/amd64` + `linux/arm64`. Tags follow the repo-wide -[semver tag matrix](../README.md#supported-tags) (`1.6.1` immutable; `1.6`, `1`, `latest` moving). +Multi-arch: `linux/amd64` + `linux/arm64`. Two tags per release +(see [supported tags](../README.md#supported-tags)): the exact version (`1.6.1`, immutable — +pin this in production) and `latest` (moving). + +### Verify the signature + +Every published image is signed with cosign (keyless, GitHub OIDC): + +```bash +cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity-regexp \ + '^https://github\.com/Gforce-Innovation-Kft/shared-github-actions/\.github/workflows/docker-build-test-push\.yml@.+$' \ + gforceinnovation/sf-devcontainer:latest +``` ## What's inside