fix(git): open absolute-path files via osfs root "/" (fixes "public k… #733
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build simple-container-com CLI | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - 'main' | |
| # allow only one concurrent build | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Least-privilege root (CIS GHA 1.2). Only `docker-finalize` (welder | |
| # tag-release + deploy) and `finalize` (notify) need `contents: write`; | |
| # both grant it per-job below. | |
| permissions: | |
| contents: read | |
| jobs: | |
| prepare: | |
| name: Prepare build | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| # Trust-root ref guard. push.yaml signs production artifacts under | |
| # the cosign keyless identity `push.yaml@refs/heads/main` (documented | |
| # in docs/SECURITY.md). A `workflow_dispatch` from any non-main ref | |
| # would sign + publish artifacts that fail consumer verification — | |
| # the artifact ships looking-signed but no documented identity regex | |
| # accepts it. Fail fast instead. | |
| - name: Refuse to run on non-main ref | |
| if: github.ref != 'refs/heads/main' | |
| run: | | |
| echo "::error title=push.yaml restricted to main::Got github.ref=$GITHUB_REF; this workflow signs artifacts under the production trust root (refs/heads/main only). Re-dispatch from main or remove workflow_dispatch from the trigger." | |
| exit 1 | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Get next version | |
| uses: reecetech/version-increment@a29aa752dc3b8118a2dc2ed93faf0e95a73a9c7e # 2024.10.1 | |
| id: version | |
| with: | |
| scheme: "calver" | |
| increment: "patch" | |
| use_api: "true" | |
| build-setup: | |
| name: Build Setup (clean, tools, schemas, lint, fmt) | |
| runs-on: blacksmith-8vcpu-ubuntu-2204 | |
| needs: prepare | |
| outputs: | |
| cicd-bot-telegram-token: ${{ steps.telegram-secrets.outputs.cicd-bot-telegram-token }} | |
| cicd-bot-telegram-chat-id: ${{ steps.telegram-secrets.outputs.cicd-bot-telegram-chat-id }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - uses: fregante/setup-git-user@024bc0b8e177d7e77203b48dab6fb45666854b35 # v2.0.2 | |
| - name: Set up Go with Blacksmith caching | |
| uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6.0.1 | |
| with: | |
| go-version: '1.25' | |
| - name: install sc + welder tools | |
| uses: simple-container-com/actions/setup-sc-tooling@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| - name: prepare secrets for build | |
| run: | | |
| cat << EOF > ./.sc/cfg.default.yaml | |
| ${{ secrets.SC_CONFIG }} | |
| EOF | |
| cat << EOF > ./.sc/cfg.test.yaml | |
| ${{ secrets.SC_CONFIG }} | |
| EOF | |
| sc secrets reveal | |
| - name: get openai key | |
| id: get-openai-key | |
| run: | | |
| echo "openai-key=$(sc stack secret-get -s dist openai-api-key 2>/dev/null || echo '')" >> $GITHUB_OUTPUT | |
| - name: prepare sc tool (rebuild) | |
| shell: bash | |
| env: | |
| OPENAI_API_KEY: ${{ steps.get-openai-key.outputs.openai-key }} | |
| SKIP_EMBEDDINGS: "true" | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: |- | |
| gh auth setup-git | |
| welder run rebuild | |
| - name: clean | |
| run: | | |
| mkdir -p dist | |
| rm -fR dist/* | |
| mkdir -p .sc/stacks/dist/bundle | |
| rm -fR .sc/stacks/dist/bundle/* | |
| mkdir -p docs/site | |
| rm -fR docs/site/* | |
| mkdir -p docs/schemas | |
| rm -fR docs/schemas/* | |
| - name: tools | |
| run: | | |
| cat tools.go | grep _ | awk -F'"' '{print $2}' | xargs -tI % go get % | |
| go mod download | |
| go generate -tags tools | |
| go mod tidy | |
| - name: generate-schemas | |
| run: | | |
| echo "Generating JSON Schema files for Simple Container resources..." | |
| go build -o bin/schema-gen ./cmd/schema-gen | |
| bin/schema-gen docs/schemas | |
| echo "Successfully generated JSON Schema files in docs/schemas/" | |
| - name: fmt | |
| run: | | |
| go mod tidy | |
| bin/gofumpt -l -w ./ | |
| bin/golangci-lint run --fix --timeout 3m -v | |
| - name: get telegram secrets | |
| id: telegram-secrets | |
| run: | | |
| echo "cicd-bot-telegram-token=$(./bin/sc stack secret-get -s dist cicd-bot-telegram-token)" >> $GITHUB_OUTPUT | |
| echo "cicd-bot-telegram-chat-id=$(./bin/sc stack secret-get -s dist cicd-bot-telegram-chat-id)" >> $GITHUB_OUTPUT | |
| - name: save schemas cache | |
| uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: docs/schemas | |
| key: schemas-${{ github.run_id }} | |
| - name: upload bin directory artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: bin-tools | |
| path: bin | |
| retention-days: 1 | |
| build-platforms: | |
| name: Build sc for ${{ matrix.os }}/${{ matrix.arch }} | |
| runs-on: blacksmith-8vcpu-ubuntu-2204 | |
| needs: [prepare, build-setup] | |
| permissions: | |
| contents: read | |
| id-token: write # OIDC for keyless cosign + attest-build-provenance | |
| attestations: write | |
| strategy: | |
| matrix: | |
| include: | |
| - os: linux | |
| arch: amd64 | |
| - os: darwin | |
| arch: arm64 | |
| - os: darwin | |
| arch: amd64 | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go with Blacksmith caching | |
| uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6.0.1 | |
| with: | |
| go-version: '1.25' | |
| - name: create build directories | |
| run: | | |
| mkdir -p dist | |
| mkdir -p .sc/stacks/dist/bundle | |
| - name: build sc for ${{ matrix.os }}/${{ matrix.arch }} | |
| env: | |
| GOOS: ${{ matrix.os }} | |
| GOARCH: ${{ matrix.arch }} | |
| CGO_ENABLED: "0" | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| echo "Building for ${GOOS}/${GOARCH}..." | |
| if [ "${GOOS}" = "windows" ]; then export EXT=".exe"; else export EXT=""; fi | |
| go build -trimpath -buildvcs=false -ldflags "-s -w -X=github.com/simple-container-com/api/internal/build.Version=${VERSION}" -o dist/${GOOS}-${GOARCH}/sc${EXT} ./cmd/sc | |
| # Deterministic tarball: fixed entry order, zeroed mtime/owner, a | |
| # forced mode (independent of the builder umask) and gzip -9 -n | |
| # (no embedded timestamp) so a rebuild reproduces it bit-for-bit. | |
| tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner --mode='u=rwx,go=rx' -cf - -C dist/${GOOS}-${GOARCH} sc${EXT} | gzip -9 -n > .sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}.tar.gz | |
| cp .sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}.tar.gz .sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz | |
| # Phase 2: per-tarball SHA-256, CycloneDX SBOM, cosign keyless signature | |
| # (self-contained bundle), SLSA provenance. Soft-fail per step — the tarball | |
| # itself is the publish artifact; sidecars are best-effort during the 14-day | |
| # bake-in. See HARDENING.md Phase 2 plan + verify-attestations.yml gate. | |
| - name: SHA-256 sidecars for ${{ matrix.os }}/${{ matrix.arch }} | |
| id: sha256 | |
| env: | |
| GOOS: ${{ matrix.os }} | |
| GOARCH: ${{ matrix.arch }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| cd .sc/stacks/dist/bundle | |
| sha256sum "sc-${GOOS}-${GOARCH}.tar.gz" > "sc-${GOOS}-${GOARCH}.tar.gz.sha256" | |
| sha256sum "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz" > "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz.sha256" | |
| - name: Install attestation tools | |
| id: install_attest_tools | |
| continue-on-error: true # Best-effort during Phase 2 bake-in. If the | |
| # installer flakes, downstream sign/SBOM/SLSA | |
| # steps soft-skip via their own outcome | |
| # guards; the tarball + .sha256 still publish. | |
| uses: simple-container-com/actions/install-attest-tools@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| - name: Generate CycloneDX SBOM for sc-${{ matrix.os }}-${{ matrix.arch }} | |
| id: sbom_tarball | |
| continue-on-error: true | |
| env: | |
| GOOS: ${{ matrix.os }} | |
| GOARCH: ${{ matrix.arch }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| cd .sc/stacks/dist/bundle | |
| syft scan "dir:../../../../dist/${GOOS}-${GOARCH}" \ | |
| -o "cyclonedx-json=sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz.sbom.cdx.json" | |
| cp "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz.sbom.cdx.json" \ | |
| "sc-${GOOS}-${GOARCH}.tar.gz.sbom.cdx.json" | |
| - name: Cosign sign-blob (keyless, bundle) for sc-${{ matrix.os }}-${{ matrix.arch }} | |
| id: cosign_sign_tarball | |
| continue-on-error: true | |
| env: | |
| COSIGN_EXPERIMENTAL: "1" | |
| GOOS: ${{ matrix.os }} | |
| GOARCH: ${{ matrix.arch }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| cd .sc/stacks/dist/bundle | |
| for tarball in "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz" "sc-${GOOS}-${GOARCH}.tar.gz"; do | |
| cosign sign-blob --yes \ | |
| --bundle "${tarball}.cosign-bundle" \ | |
| "${tarball}" | |
| done | |
| - name: SLSA build provenance for sc-${{ matrix.os }}-${{ matrix.arch }} | |
| id: slsa_tarball | |
| continue-on-error: true | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-path: | | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v${{ needs.prepare.outputs.version }}.tar.gz | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}.tar.gz | |
| - name: Materialize SLSA provenance bundle next to tarballs | |
| if: steps.slsa_tarball.outcome == 'success' | |
| continue-on-error: true | |
| env: | |
| BUNDLE_PATH: ${{ steps.slsa_tarball.outputs.bundle-path }} | |
| GOOS: ${{ matrix.os }} | |
| GOARCH: ${{ matrix.arch }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| dest_dir=".sc/stacks/dist/bundle" | |
| # attest-build-provenance emits a single Sigstore bundle covering all | |
| # subjects; publish it under both tarball names so consumers can find | |
| # it via the same prefix as the tarball they downloaded. | |
| # Naming: .sigstore.json matches the actual format (Sigstore bundle, a | |
| # single JSON object) — historical .intoto.jsonl is misleading because | |
| # the file is not line-delimited. | |
| for tarball in "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz" "sc-${GOOS}-${GOARCH}.tar.gz"; do | |
| cp "$BUNDLE_PATH" "${dest_dir}/${tarball}.sigstore.json" | |
| done | |
| - name: Soft-fail aggregator for sc-${{ matrix.os }}-${{ matrix.arch }} attestation | |
| if: always() | |
| env: | |
| SBOM_OUTCOME: ${{ steps.sbom_tarball.outcome }} | |
| SIGN_OUTCOME: ${{ steps.cosign_sign_tarball.outcome }} | |
| SLSA_OUTCOME: ${{ steps.slsa_tarball.outcome }} | |
| run: | | |
| fail=0 | |
| for v in "$SBOM_OUTCOME" "$SIGN_OUTCOME" "$SLSA_OUTCOME"; do | |
| if [ "$v" != "success" ]; then fail=1; fi | |
| done | |
| if [ "$fail" -eq 1 ]; then | |
| echo "::warning title=Attestation incomplete::sc-${{ matrix.os }}-${{ matrix.arch }} tarball published but one or more attestation steps failed (sbom=$SBOM_OUTCOME sign=$SIGN_OUTCOME slsa=$SLSA_OUTCOME)." | |
| else | |
| echo "All tarball attestation steps succeeded for ${{ matrix.os }}/${{ matrix.arch }}." | |
| fi | |
| - name: upload build artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sc-${{ matrix.os }}-${{ matrix.arch }} | |
| # Match both versioned (sc-os-arch-vX.Y.Z.tar.gz) AND unversioned (sc-os-arch.tar.gz). | |
| # The old glob `sc-os-arch-*.tar.gz` required a hyphen before `*`, so it only matched | |
| # the versioned tarball — the unversioned "latest" pointer was silently dropped from | |
| # artifacts and therefore never re-uploaded to dist.simple-container.com. Result: | |
| # sc.sh (which downloads the unversioned tarball when SIMPLE_CONTAINER_VERSION is | |
| # empty) served stale v2026.3.6 for ~4 weeks after PR #186 merged. | |
| # Phase 2: also captures .sha256, .sbom.cdx.json, .cosign-bundle, .sigstore.json sidecars. | |
| path: | | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}*.tar.gz | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}*.tar.gz.sha256 | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}*.tar.gz.cosign-bundle | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}*.tar.gz.sigstore.json | |
| .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}*.tar.gz.sbom.cdx.json | |
| retention-days: 1 | |
| build-binaries: | |
| name: Build ${{ matrix.target }} | |
| runs-on: blacksmith-8vcpu-ubuntu-2204 | |
| needs: [prepare, build-setup] | |
| strategy: | |
| matrix: | |
| include: | |
| - target: github-actions | |
| cmd: github-actions | |
| output: dist/github-actions | |
| - target: cloud-helpers | |
| cmd: cloud-helpers | |
| output: dist/cloud-helpers | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go with Blacksmith caching | |
| uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6.0.1 | |
| with: | |
| go-version: '1.25' | |
| - name: create build directories | |
| run: | | |
| mkdir -p dist | |
| - name: build ${{ matrix.target }} | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| CGO_ENABLED: "0" | |
| run: | | |
| go build -trimpath -buildvcs=false -a -installsuffix cgo -ldflags "-s -w -X=github.com/simple-container-com/api/internal/build.Version=${VERSION}" -o ${{ matrix.output }} ./cmd/${{ matrix.cmd }} | |
| - name: upload ${{ matrix.target }} binary | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ matrix.target }}-binary | |
| path: ${{ matrix.output }} | |
| retention-days: 1 | |
| # Note (PR #257): the `build-github-actions-staging` job was removed | |
| # alongside its sole consumer — the `github-actions-staging` matrix | |
| # entry in docker-build. The `:staging` image tag is now exclusively | |
| # published by build-staging.yml under the staging trust root. | |
| test: | |
| name: Run tests | |
| runs-on: blacksmith-8vcpu-ubuntu-2204 | |
| needs: [prepare, build-setup] | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go with Blacksmith caching | |
| uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6.0.1 | |
| with: | |
| go-version: '1.25' | |
| - name: test | |
| run: | | |
| go test ./... | |
| build-docs: | |
| name: Build documentation | |
| runs-on: ubuntu-latest | |
| needs: [prepare, build-setup] | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: restore cached schemas | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: docs/schemas | |
| key: schemas-${{ github.run_id }} | |
| - name: build docs | |
| run: | | |
| # --require-hashes pins every transitive dep to a known SHA from | |
| # docs/requirements.txt (generated via `pip-compile --generate-hashes` | |
| # against docs/requirements.in). Closes Scorecard Pinned-Dependencies | |
| # warning on this line. | |
| # | |
| # python:3.12-slim, SHA-pinned digest. Required because the | |
| # patched versions of `requests` (>=2.33.0) and `urllib3` | |
| # (>=2.7.0) used to close GHSA-gc5v / -mf9v / -qccp transitive- | |
| # dep advisories declare `requires_python: >=3.10`. Python 3.9 | |
| # base would force keeping the vulnerable transitives. | |
| # Digest is multi-platform (OCI image index) — covers amd64 | |
| # / arm64 / arm/v5 / arm/v7 / 386 / ppc64le / riscv64 / s390x. | |
| docker run --rm -v $PWD/docs:/docs -w /docs python@sha256:401f6e1a67dad31a1bd78e9ad22d0ee0a3b52154e6bd30e90be696bb6a3d7461 sh -c "pip install --require-hashes -r requirements.txt && PATH=\$PATH:~/.local/bin mkdocs build" | |
| - name: upload docs artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: docs-site | |
| path: docs/site | |
| retention-days: 1 | |
| docker-build: | |
| name: Docker build and push ${{ matrix.image }} | |
| runs-on: blacksmith-8vcpu-ubuntu-2204 | |
| needs: [prepare, build-setup, build-platforms, build-binaries, test, build-docs] | |
| permissions: | |
| contents: read | |
| id-token: write # OIDC token for keyless cosign + attest-build-provenance | |
| attestations: write # required by actions/attest-build-provenance | |
| strategy: | |
| matrix: | |
| include: | |
| - image: kubectl | |
| dockerfile: kubectl.Dockerfile | |
| image_repo: simplecontainer/kubectl | |
| tags: | | |
| simplecontainer/kubectl:latest | |
| simplecontainer/kubectl:${{ needs.prepare.outputs.version }} | |
| - image: caddy | |
| dockerfile: caddy.Dockerfile | |
| image_repo: simplecontainer/caddy | |
| tags: | | |
| simplecontainer/caddy:latest | |
| simplecontainer/caddy:${{ needs.prepare.outputs.version }} | |
| - image: github-actions | |
| dockerfile: github-actions.Dockerfile | |
| image_repo: simplecontainer/github-actions | |
| tags: | | |
| simplecontainer/github-actions:latest | |
| simplecontainer/github-actions:${{ needs.prepare.outputs.version }} | |
| # The :staging tag is intentionally NOT published from this workflow. | |
| # build-staging.yml (triggered on the `staging` branch) is the | |
| # canonical owner of `simplecontainer/github-actions:staging` and signs | |
| # it under the staging trust root. Publishing :staging here from main | |
| # would either (a) double-sign with the production identity — breaking | |
| # the SECURITY.md identity-regex contract — or (b) leave it unsigned, | |
| # so consumers following the documented verification path would fail. | |
| - image: cloud-helpers-aws | |
| dockerfile: cloud-helpers.aws.Dockerfile | |
| image_repo: simplecontainer/cloud-helpers | |
| tags: | | |
| simplecontainer/cloud-helpers:aws-latest | |
| simplecontainer/cloud-helpers:aws-${{ needs.prepare.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: download github-actions binary | |
| if: matrix.image == 'github-actions' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: github-actions-binary | |
| path: dist | |
| - name: download cloud-helpers binary | |
| if: matrix.image == 'cloud-helpers-aws' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: cloud-helpers-binary | |
| path: dist | |
| - name: install sc tool | |
| uses: simple-container-com/actions/install-sc@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| - name: prepare secrets for build | |
| run: | | |
| cat << EOF > ./.sc/cfg.default.yaml | |
| ${{ secrets.SC_CONFIG }} | |
| EOF | |
| cat << EOF > ./.sc/cfg.test.yaml | |
| ${{ secrets.SC_CONFIG }} | |
| EOF | |
| sc secrets reveal | |
| - name: Setup Docker Buildx with advanced caching | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| with: | |
| driver-opts: | | |
| image=moby/buildkit:buildx-stable-1 | |
| - name: Docker login using SC secrets | |
| run: | | |
| sc stack secret-get -s dist dockerhub-cicd-token | docker login --username simplecontainer --password-stdin | |
| - name: Build and push ${{ matrix.image }} image | |
| id: build_and_push | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| platforms: linux/amd64 | |
| tags: ${{ matrix.tags }} | |
| push: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| provenance: false | |
| # Phase 2 attestation: keyless cosign sign + CycloneDX SBOM + SLSA L3 | |
| # provenance. The publish step above is the gating job. Sign/attest steps | |
| # below are continue-on-error so a sigstore-public outage does not block | |
| # releases. A trailing aggregator emits ::warning:: on any failure. After | |
| # 14 days of clean post-publish verification (see verify-attestations), | |
| # flip continue-on-error to false (tracked in HARDENING.md Phase 2 plan). | |
| # | |
| # `github-actions-staging` is intentionally skipped from attestation here. | |
| # The `:staging` tag is the canonical output of build-staging.yml under | |
| # the staging trust root (build-staging.yml@refs/heads/staging) and is | |
| # signed there. Signing it again from push.yaml@refs/heads/main would | |
| # mint a second valid signature under the production identity, which | |
| # contradicts the split identity-regex contract in SECURITY.md and | |
| # would cause consumer verification to flap between identities on | |
| # successive main / staging pushes. | |
| - name: Sign + attest ${{ matrix.image }} (SBOM + cosign + SLSA) | |
| id: signattest | |
| if: steps.build_and_push.outcome == 'success' | |
| uses: simple-container-com/actions/sign-and-attest@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| with: | |
| image-ref: ${{ matrix.image_repo }}@${{ steps.build_and_push.outputs.digest }} | |
| image-name: ${{ matrix.image }} | |
| subject-name: index.docker.io/${{ matrix.image_repo }} | |
| subject-digest: ${{ steps.build_and_push.outputs.digest }} | |
| # Phase 2 bake-in: keep soft-fail until 14 days of clean | |
| # verify-attestations on `main`. Flip to 'false' tracked in | |
| # HARDENING.md Phase 2. | |
| soft-fail: 'true' | |
| # Record the immutable digest so verify-attestations can verify by | |
| # @sha256:... instead of by mutable tag (closes the verify-by-tag TOCTOU | |
| # window flagged in PR #257 round-1 review). | |
| - name: Record image digest for verify-attestations | |
| if: steps.build_and_push.outcome == 'success' | |
| env: | |
| DIGEST: ${{ steps.build_and_push.outputs.digest }} | |
| IMAGE_REPO: ${{ matrix.image_repo }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p digests | |
| printf '%s@%s\n' "$IMAGE_REPO" "$DIGEST" > "digests/${{ matrix.image }}.txt" | |
| - name: Upload digest artifact for ${{ matrix.image }} | |
| if: steps.build_and_push.outcome == 'success' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: image-digest-${{ matrix.image }} | |
| path: digests/${{ matrix.image }}.txt | |
| retention-days: 1 | |
| - name: Soft-fail aggregator for ${{ matrix.image }} attestation | |
| if: always() && steps.build_and_push.outcome == 'success' | |
| env: | |
| SIGN_OUTCOME: ${{ steps.signattest.outputs.sign-outcome }} | |
| SBOM_OUTCOME: ${{ steps.signattest.outputs.sbom-outcome }} | |
| ATTEST_SBOM_OUTCOME: ${{ steps.signattest.outputs.attest-outcome }} | |
| SLSA_OUTCOME: ${{ steps.signattest.outputs.slsa-outcome }} | |
| run: | | |
| fail=0 | |
| for v in "$SIGN_OUTCOME" "$SBOM_OUTCOME" "$ATTEST_SBOM_OUTCOME" "$SLSA_OUTCOME"; do | |
| if [ "$v" != "success" ]; then fail=1; fi | |
| done | |
| if [ "$fail" -eq 1 ]; then | |
| echo "::warning title=Attestation incomplete::${{ matrix.image }} published but one or more attestation steps failed (sign=$SIGN_OUTCOME sbom=$SBOM_OUTCOME attest-sbom=$ATTEST_SBOM_OUTCOME slsa=$SLSA_OUTCOME). See verify-attestations workflow." | |
| else | |
| echo "All attestation steps succeeded for ${{ matrix.image }}." | |
| fi | |
| docker-finalize: | |
| name: Docker finalize (tag-release, deploy) | |
| runs-on: blacksmith-8vcpu-ubuntu-2204 | |
| needs: [prepare, build-setup, build-platforms, build-binaries, test, build-docs, docker-build] | |
| permissions: | |
| contents: write # `welder run tag-release` pushes the release git tag | |
| id-token: write # OIDC for keyless cosign sign-blob of sc.sh | |
| attestations: write | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - uses: fregante/setup-git-user@024bc0b8e177d7e77203b48dab6fb45666854b35 # v2.0.2 | |
| - name: install sc + welder tools | |
| uses: simple-container-com/actions/setup-sc-tooling@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| - name: Install attestation tools (cosign for sc.sh sign-blob) | |
| uses: simple-container-com/actions/install-attest-tools@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| - name: prepare secrets for build | |
| run: | | |
| cat << EOF > ./.sc/cfg.default.yaml | |
| ${{ secrets.SC_CONFIG }} | |
| EOF | |
| cat << EOF > ./.sc/cfg.test.yaml | |
| ${{ secrets.SC_CONFIG }} | |
| EOF | |
| sc secrets reveal | |
| - name: download sc tarball artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| # Filter to sc-* artifacts only — without a pattern, this step | |
| # also pulls the build-push-action's internal `*.dockerbuild` | |
| # cache artifacts (one per matrix item, often 100+ MB each) and | |
| # the `image-digest-*` artifacts we publish for | |
| # verify-attestations.yml to consume. None of those are needed | |
| # here, and the .dockerbuild ones flake the parallel-download | |
| # path: PR #257's first prod release (run 25959575686) failed | |
| # at this step with "Artifact download failed after 5 retries" | |
| # because the unfiltered download saturated the runner. | |
| pattern: sc-* | |
| path: artifacts | |
| - name: download bin tools artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: bin-tools | |
| path: bin | |
| - name: fix bin tools permissions | |
| run: chmod +x bin/* | |
| - name: download docs artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: docs-site | |
| path: docs/site | |
| - name: copy build artifacts to dist bundle | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .sc/stacks/dist/bundle | |
| # Phase 2: copy tarballs + every sidecar (sha256, sbom, cosign-bundle, | |
| # sigstore.json). Sidecars may be absent if sign/SBOM steps soft-failed | |
| # in build-platforms — that is by design during the 14-day bake-in; the | |
| # count-assertion step below records the gap rather than blocks the run. | |
| shopt -s nullglob | |
| for f in artifacts/sc-*/*.tar.gz \ | |
| artifacts/sc-*/*.tar.gz.sha256 \ | |
| artifacts/sc-*/*.tar.gz.cosign-bundle \ | |
| artifacts/sc-*/*.tar.gz.sigstore.json \ | |
| artifacts/sc-*/*.tar.gz.sbom.cdx.json; do | |
| cp "$f" .sc/stacks/dist/bundle/ | |
| done | |
| - name: Sidecar count assertion (Phase 2 bake-in — warn-only) | |
| run: | | |
| set -euo pipefail | |
| cd .sc/stacks/dist/bundle | |
| shopt -s nullglob | |
| arr_tarballs=( sc-*.tar.gz ); tarballs=${#arr_tarballs[@]} | |
| arr_shas=( sc-*.tar.gz.sha256 ); shas=${#arr_shas[@]} | |
| arr_bundles=( sc-*.tar.gz.cosign-bundle ); bundles=${#arr_bundles[@]} | |
| arr_provs=( sc-*.tar.gz.sigstore.json ); provs=${#arr_provs[@]} | |
| arr_sboms=( sc-*.tar.gz.sbom.cdx.json ); sboms=${#arr_sboms[@]} | |
| echo "tarballs=$tarballs sha256=$shas cosign-bundle=$bundles slsa=$provs sbom=$sboms" | |
| if [ "$tarballs" -eq 0 ]; then | |
| echo "::error title=No tarballs to publish::dist bundle has zero tarballs" | |
| exit 1 | |
| fi | |
| missing=0 | |
| # Every tarball has matching .sha256 / .cosign-bundle / .sigstore.json | |
| # / .sbom.cdx.json sidecars; build-platforms duplicates the per-build | |
| # outputs across the versioned and unversioned tarball names. | |
| for kind_count in "sha256=$shas" \ | |
| "cosign-bundle=$bundles" \ | |
| "slsa=$provs" \ | |
| "sbom=$sboms"; do | |
| label="${kind_count%%=*}" | |
| count="${kind_count#*=}" | |
| if [ "$count" -lt "$tarballs" ]; then | |
| echo "::warning title=Sidecar count mismatch::${label} has $count of $tarballs expected sidecars (Phase 2 soft-fail)" | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" -eq 0 ]; then | |
| echo "All sidecars present for every published tarball." | |
| fi | |
| - name: finalize dist bundle setup | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| cp sc.sh .sc/stacks/dist/bundle/sc.sh | |
| sed -i -e "s/VERSION=\"0\\.0\\.0\"/VERSION=\"${VERSION}\"/" .sc/stacks/dist/bundle/sc.sh | |
| echo "${VERSION}" > .sc/stacks/dist/bundle/version | |
| # Sign the post-template sc.sh that will land on dist.simple-container.com. | |
| # Signing the *post-sed* artifact is intentional: consumers verify the | |
| # bytes they actually fetch. Identity matches the tarball regex in | |
| # docs/SECURITY.md ("Verifying sc.sh itself") — push.yaml@refs/heads/main. | |
| # | |
| # Hard-fail on purpose (no continue-on-error): if signing breaks, the | |
| # `welder deploy` step that publishes sc.sh + sc.sh.cosign-bundle never | |
| # runs, so consumers never observe an sc.sh whose bundle is missing or | |
| # stale. Atomic w.r.t. the script — both publish or neither. | |
| - name: Cosign sign-blob (keyless, bundle) for sc.sh | |
| env: | |
| COSIGN_EXPERIMENTAL: "1" | |
| run: | | |
| set -euo pipefail | |
| cd .sc/stacks/dist/bundle | |
| cosign sign-blob --yes \ | |
| --bundle sc.sh.cosign-bundle \ | |
| sc.sh | |
| - name: Run tag-release task after images are built | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: |- | |
| gh auth setup-git | |
| welder run tag-release | |
| - name: publish sc tool | |
| shell: bash | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: |- | |
| welder deploy -e prod --timestamps | |
| # Create a GitHub Release on production tags only (skip preview / | |
| # release-candidate tags that include `-pre.` or `-preview.`). | |
| # | |
| # Why this matters: OpenSSF Scorecard's Signed-Releases check | |
| # queries GitHub's Releases API, NOT the tag list or the dist | |
| # CDN. We've been signing tarballs all along (Phase 2 / PR #257), | |
| # but the .sigstore.json + .cosign-bundle + .sha256 + .sbom.cdx.json | |
| # sidecars only live on dist.simple-container.com. Scorecard | |
| # returns score -1 ("no releases found") because the Releases API | |
| # is empty. | |
| # | |
| # Attaching the existing signed sidecars as Release assets makes | |
| # Scorecard discover them. The accepted signature suffixes per | |
| # the upstream check (probes/releasesAreSigned/impl.go) are: | |
| # .asc .minisig .sig .sign .sigstore .sigstore.json | |
| # We attach .sigstore.json — the SLSA build provenance bundle | |
| # emitted by actions/attest-build-provenance@v4. The .cosign-bundle | |
| # is attached too for verification flexibility (not recognised by | |
| # Scorecard's regex but useful for `cosign verify-blob --bundle` | |
| # consumers). | |
| - name: Create GitHub Release on production tags | |
| if: needs.prepare.outputs.version != '' && !contains(needs.prepare.outputs.version, '-pre.') && !contains(needs.prepare.outputs.version, '-preview.') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| run: ./scripts/create-github-release.sh | |
| finalize: | |
| name: Finalize build and deploy for ${{ needs.prepare.outputs.stack-name }} | |
| runs-on: ubuntu-latest | |
| if: ${{ always() }} | |
| permissions: | |
| contents: write | |
| needs: | |
| - prepare | |
| - build-setup | |
| - docker-finalize | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| if: ${{ always() }} | |
| with: | |
| persist-credentials: false | |
| - name: Extract git reference | |
| id: extract_git_ref | |
| if: ${{ always() }} | |
| shell: bash | |
| env: | |
| COMMIT_MESSAGE: ${{ github.event.head_commit.message || github.event.workflow_run.head_commit.message }} | |
| run: |- | |
| message="$(printf '%s' "$COMMIT_MESSAGE" | tr -d '\n')" | |
| # Truncate message if too long for Telegram (max ~200 chars to leave room for other content) | |
| if [ ${#message} -gt 200 ]; then | |
| # Take first 80 chars and last 80 chars with separator | |
| truncated_message="${message:0:80}...${message: -80}" | |
| message="$truncated_message" | |
| fi | |
| echo "branch=$GITHUB_REF_NAME" >> $GITHUB_OUTPUT | |
| echo "message=$message" >> $GITHUB_OUTPUT | |
| echo "author=$GITHUB_ACTOR" >> $GITHUB_OUTPUT | |
| echo "url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT | |
| - name: Notify Telegram (success) | |
| if: ${{ success() && !contains(needs.*.result, 'failure') }} | |
| continue-on-error: true | |
| uses: simple-container-com/actions/notify-telegram@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| with: | |
| chat-id: ${{ needs.build-setup.outputs.cicd-bot-telegram-chat-id }} | |
| token: ${{ needs.build-setup.outputs.cicd-bot-telegram-token }} | |
| text: '🚀 Released: ' | |
| link-url: ${{ github.event.head_commit.url || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }} | |
| link-text: 'v${{ needs.prepare.outputs.version }}' | |
| suffix: ' by ${{ steps.extract_git_ref.outputs.author }}' | |
| - name: Notify Telegram (failure) | |
| if: ${{ failure() || contains(needs.*.result, 'failure') }} | |
| continue-on-error: true | |
| uses: simple-container-com/actions/notify-telegram@0af5a697f24ea484991660619d0ae42d50343b9d # main | |
| with: | |
| chat-id: ${{ needs.build-setup.outputs.cicd-bot-telegram-chat-id }} | |
| token: ${{ needs.build-setup.outputs.cicd-bot-telegram-token }} | |
| text: '❗ Release failed: ' | |
| link-url: ${{ github.event.head_commit.url || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }} | |
| link-text: 'v${{ needs.prepare.outputs.version }}' | |
| suffix: ' by ${{ steps.extract_git_ref.outputs.author }}' | |
| - name: Build failed due to previously failed steps | |
| id: fail_if_needed | |
| if: ${{ failure() || contains(needs.*.result, 'failure') }} | |
| shell: bash | |
| run: |- | |
| exit 1 |