diff --git a/.github/ISSUE_TEMPLATE/NEW_RELEASE.md b/.github/ISSUE_TEMPLATE/NEW_RELEASE.md new file mode 100644 index 0000000..fea9d3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/NEW_RELEASE.md @@ -0,0 +1,38 @@ +--- +name: New release +about: Checklist for releasing a new version of cluster-inventory-api +title: "Release vX.Y.Z" +labels: kind/release +--- + +## Release checklist + +### Prepare + +- [ ] Changelog added below +- [ ] 2-week lazy consensus period elapsed (no OWNER objections) + +### Tag + +- [ ] Release tag pushed: `git tag -s v && git push origin v` +- [ ] Draft GitHub release created: `gh release create v --draft --generate-notes --verify-tag` + +### Staging + +- [ ] `test-infra` postsubmit pushed staging images +- [ ] Staging images verified at `us-central1-docker.pkg.dev/k8s-staging-images/cluster-inventory-api/:` + +### Promote + +- [ ] Promotion PR created: `kpromo pr --fork --project cluster-inventory-api --tag v` +- [ ] Promotion PR reviewed and merged + +### Publish + +- [ ] Production images verified at `registry.k8s.io/cluster-inventory-api/:` +- [ ] Draft GitHub release published +- [ ] This issue closed +- [ ] Announcement sent + +## Changelog + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98c2917..2015ea7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,16 +13,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: false - name: Run golangci-lint - uses: golangci/golangci-lint-action@v9 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: v2.9.0 args: --timeout 5m0s @@ -32,10 +32,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: false @@ -49,10 +49,10 @@ jobs: needs: verify steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: false diff --git a/.github/workflows/e2e-plugins.yml b/.github/workflows/e2e-plugins.yml index d0703a2..4c4c5eb 100644 --- a/.github/workflows/e2e-plugins.yml +++ b/.github/workflows/e2e-plugins.yml @@ -28,16 +28,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod cache: true - name: Setup kind - uses: helm/kind-action@v1.13.0 + uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0 with: install_only: true version: v0.31.0 @@ -54,7 +54,7 @@ jobs: bash ${{ matrix.setup_script }} - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Run controller example in-cluster (image volume plugin) run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 4835baf..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Release - -on: - push: - tags: ['v*'] - workflow_dispatch: {} - -jobs: - discover: - permissions: - contents: read - packages: read - runs-on: ubuntu-slim - outputs: - plugins: ${{ steps.scan.outputs.plugins }} - version: ${{ steps.version.outputs.version }} - steps: - - uses: actions/checkout@v6 - - id: version - run: | - version="${GITHUB_REF_NAME#v}" - echo "version=${version}" >> "$GITHUB_OUTPUT" - - id: scan - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REGISTRY: ghcr.io/${{ github.repository }} - OWNER: ${{ github.repository_owner }} - REPO_NAME: ${{ github.event.repository.name }} - run: | - version="${GITHUB_REF_NAME#v}" - result='[]' - for d in plugins/*/; do - name=$(basename "$d") - [ -d "plugins/$name/cmd/plugin" ] || continue - encoded_pkg="${REPO_NAME}%2F${name}" - tags=$(gh api "/orgs/${OWNER}/packages/container/${encoded_pkg}/versions" \ - --jq '.[].metadata.container.tags[]' 2>/dev/null || \ - gh api "/users/${OWNER}/packages/container/${encoded_pkg}/versions" \ - --jq '.[].metadata.container.tags[]' 2>/dev/null || true) - if echo "$tags" | grep -qx "${version}"; then - echo "Skip ${name}: ${REGISTRY}/${name}:${version} already exists" - continue - fi - result=$(echo "$result" | jq -c --arg n "$name" '. + [$n]') - done - echo "plugins=${result}" >> "$GITHUB_OUTPUT" - - release: - permissions: - contents: read - packages: write - id-token: write # Required for cosign keyless signing via GitHub OIDC - needs: discover - if: needs.discover.outputs.plugins != '[]' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - plugin: ${{ fromJson(needs.discover.outputs.plugins) }} - steps: - - uses: actions/checkout@v6 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: docker/setup-buildx-action@v3 - - - uses: sigstore/cosign-installer@v3 - - - name: Release ${{ matrix.plugin }} - run: >- - make docker-buildx - PLUGIN_NAME=${{ matrix.plugin }} - REGISTRY=ghcr.io/${{ github.repository }} - VERSION=${{ needs.discover.outputs.version }} - PLATFORMS=linux/amd64,linux/arm64 - - - name: Get image digest - id: digest - env: - IMAGE: ghcr.io/${{ github.repository }}/${{ matrix.plugin }}:${{ needs.discover.outputs.version }} - run: | - digest="sha256:$(docker buildx imagetools inspect "${IMAGE}" --raw | sha256sum | cut -d ' ' -f 1)" - echo "digest=${digest}" >> "$GITHUB_OUTPUT" - - - name: Sign container image with cosign - env: - IMAGE: ghcr.io/${{ github.repository }}/${{ matrix.plugin }}@${{ steps.digest.outputs.digest }} - run: cosign sign --yes "${IMAGE}" diff --git a/Makefile b/Makefile index 6814097..8bb7722 100644 --- a/Makefile +++ b/Makefile @@ -140,6 +140,23 @@ docker-buildx: ## Build and push docker image for cross-platform support (PLUGIN --attest type=sbom \ . +PLUGIN_NAMES := $(sort $(patsubst plugins/%/cmd/plugin,%,$(wildcard plugins/*/cmd/plugin))) +STAGING_REGISTRY ?= us-central1-docker.pkg.dev/k8s-staging-images/cluster-inventory-api +RELEASE_REGISTRY := $(if $(REGISTRY),$(REGISTRY),$(STAGING_REGISTRY)) + +.PHONY: release-staging +release-staging: ## Build and push all plugin images to the staging registry (VERSION required) + @[ "$(origin VERSION)" = "command line" ] || [ "$(origin VERSION)" = "environment" ] \ + || { echo "VERSION must be set explicitly (e.g. VERSION=v0.1.0)"; exit 1; } + @set -e; \ + for plugin in $(PLUGIN_NAMES); do \ + $(MAKE) docker-buildx \ + PLUGIN_NAME=$$plugin \ + REGISTRY=$(RELEASE_REGISTRY) \ + VERSION=$(VERSION) \ + PLATFORMS=$(PLATFORMS); \ + done + .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist diff --git a/RELEASE.md b/RELEASE.md index 2c3c544..f3ebc4b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,50 +6,38 @@ Plugin binaries are released as OCI images built with [Docker Buildx](https://do ### Versioning -Plugin OCI images are tagged with the same version as the repository release tag. For example, pushing `v0.2.0` publishes all plugin images with tag `0.2.0`. +Plugin OCI images are tagged with the same version as the repository release tag. For example, pushing `v0.2.0` publishes all plugin images with tag `v0.2.0`. -### How to release - -1. Push a repository tag `v*` (e.g. `v1.0.0`). This triggers the `Release` workflow. -2. The workflow scans `plugins/*/` and checks whether the image already exists in the container registry for that version. Only plugins that haven't been published yet are built and pushed. -3. Container images are published to: - - `ghcr.io/kubernetes-sigs/cluster-inventory-api/:` - - Example: `ghcr.io/kubernetes-sigs/cluster-inventory-api/secretreader:1.0.0` - -### Image signing +### Prerequisites -Each released image is signed using [Cosign](https://docs.sigstore.dev/cosign/signing/overview/) keyless signing with GitHub Actions OIDC. No long-lived signing keys are used; an ephemeral key pair is generated for each signing event and the public key is recorded in the [Sigstore transparency log](https://docs.sigstore.dev/logging/overview/). +Promoting images requires [`kpromo`](https://github.com/kubernetes-sigs/promo-tools) and a fork of [kubernetes/k8s.io](https://github.com/kubernetes/k8s.io). -#### Verifying image signatures +Before the first release, the following must exist in `kubernetes/k8s.io`: -Install [Cosign](https://docs.sigstore.dev/cosign/system_config/installation/) and run: +- `registry.k8s.io/images/k8s-staging-cluster-inventory-api/OWNERS` +- `registry.k8s.io/images/k8s-staging-cluster-inventory-api/images.yaml` +- `registry.k8s.io/manifests/k8s-staging-cluster-inventory-api/promoter-manifest.yaml` (maps `k8s-staging-cluster-inventory-api` to `cluster-inventory-api` under `registry.k8s.io`) -```bash -cosign verify \ - --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ - --certificate-identity-regexp="^https://github\.com/kubernetes-sigs/cluster-inventory-api/\.github/workflows/release\.yml@refs/tags/v.*$" \ - ghcr.io/kubernetes-sigs/cluster-inventory-api/: -``` +See the [registry.k8s.io README](https://github.com/kubernetes/k8s.io/tree/main/registry.k8s.io) for setup details. -Example: - -```bash -cosign verify \ - --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ - --certificate-identity-regexp="^https://github\.com/kubernetes-sigs/cluster-inventory-api/\.github/workflows/release\.yml@refs/tags/v.*$" \ - ghcr.io/kubernetes-sigs/cluster-inventory-api/secretreader:0.1.0 -``` - -### SBOM and provenance +### How to release -Each released image includes attestations as OCI referrers: +1. Push a signed release tag (e.g. `git tag -s v1.0.0 && git push origin v1.0.0`). +2. Create a draft GitHub release for the tag (e.g. `gh release create v1.0.0 --draft --generate-notes --verify-tag`). +3. The test-infra postsubmit job builds all plugin images and pushes them to the staging registry (`us-central1-docker.pkg.dev/k8s-staging-images/cluster-inventory-api`). +4. Create a promotion PR in `kubernetes/k8s.io` using [`kpromo`](https://github.com/kubernetes-sigs/promo-tools): + ```bash + kpromo pr --fork --project cluster-inventory-api --tag v1.0.0 + ``` +5. After the promotion PR is reviewed and merged, the images become available at `registry.k8s.io/cluster-inventory-api/:`. +6. Publish the draft GitHub release and close the release issue. -- **SBOM** (SPDX): `cosign download sbom ghcr.io/kubernetes-sigs/cluster-inventory-api/:` -- **Provenance** (SLSA-style): attached by Buildx; verify with [cosign verify attestation](https://docs.sigstore.dev/cosign/verify-attestation/) or your preferred policy engine. +A release checklist is available as an [issue template](.github/ISSUE_TEMPLATE/NEW_RELEASE.md). -### Local build (no push) +### Local build - Run `make docker-build PLUGIN_NAME=` to build a single plugin image locally (e.g. `make docker-build PLUGIN_NAME=secretreader`). The image is tagged as `:latest` by default. +- Run `make release-staging VERSION= REGISTRY=` to build and push all plugin images to a staging registry. ## Project release (optional) @@ -57,6 +45,6 @@ For a high-level project release (e.g. announcing a set of plugin versions): 1. Open an issue proposing a release with a changelog since the last release. 2. The release proposal follows a **lazy consensus** model: the proposal is approved unless an [OWNER](OWNERS) objects within **two weeks** of the issue being opened. Silence is treated as approval. -3. An OWNER pushes a release tag (e.g. `v1.0.0`) to trigger the Release workflow. +3. An OWNER pushes a release tag (e.g. `v1.0.0`) to trigger the release process described above. 4. Close the release issue. 5. Optionally send an announcement (e.g. to the project mailing list or Slack). diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..a7a1308 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,15 @@ +# See https://cloud.google.com/cloud-build/docs/build-config +timeout: 7200s +options: + substitution_option: ALLOW_LOOSE + machineType: "E2_HIGHCPU_8" +steps: + - name: "gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20251215-d7853fe2a6" + args: + - make + - release-staging + - VERSION=$_PULL_BASE_REF + - REGISTRY=us-central1-docker.pkg.dev/k8s-staging-images/cluster-inventory-api + - PLATFORMS=linux/amd64,linux/arm64 +substitutions: + _PULL_BASE_REF: "main"