From 346cfaabad27878f0d161be5795103468cc162cb Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 17:08:05 +0200 Subject: [PATCH 01/31] feat(workflows): add shared Go lint, test, build, release, and e2e reusable workflows Adds 5 new reusable workflows to support standardized CI/CD across cloudoperators repos (shoot-grafter, repo-guard, permission-manager, cloudctl, greenhouse-extensions): - shared-go-lint.yaml: golangci-lint + optional govulncheck - shared-go-test.yaml: parameterized make test target + optional coverage upload - shared-go-build.yaml: Go binary build + optional Docker multi-arch build/push - shared-release.yaml: semver bump, Chart.yaml update, release PR + GitHub release + optional greenhouse-extensions dispatch - shared-e2e.yaml: KinD-based e2e wrapping the existing workflows/e2e composite action Closes #2086 Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 79 ++++++++++++++ .github/workflows/shared-go-build.yaml | 102 ++++++++++++++++++ .github/workflows/shared-go-lint.yaml | 57 ++++++++++ .github/workflows/shared-go-test.yaml | 81 ++++++++++++++ .github/workflows/shared-release.yaml | 143 +++++++++++++++++++++++++ 5 files changed, 462 insertions(+) create mode 100644 .github/workflows/shared-e2e.yaml create mode 100644 .github/workflows/shared-go-build.yaml create mode 100644 .github/workflows/shared-go-lint.yaml create mode 100644 .github/workflows/shared-go-test.yaml create mode 100644 .github/workflows/shared-release.yaml diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml new file mode 100644 index 0000000..a2206e9 --- /dev/null +++ b/.github/workflows/shared-e2e.yaml @@ -0,0 +1,79 @@ +name: Shared E2E + +on: + workflow_call: + inputs: + runs-on: + description: "The runner to use for the job" + required: false + default: "ubuntu-latest" + type: string + greenhouse-ref: + description: "Ref (branch, tag, or SHA) of cloudoperators/greenhouse to deploy" + required: false + default: "main" + type: string + admin-k8s-version: + description: "Kubernetes node image tag for the admin KinD cluster (e.g. v1.31.0)" + required: true + type: string + remote-k8s-version: + description: "Kubernetes node image tag for the remote KinD cluster (e.g. v1.31.0)" + required: true + type: string + scenario: + description: "E2E scenario name passed to the composite action and make target" + required: true + type: string + with-remote-cluster: + description: "Spin up a second KinD cluster as the remote cluster" + required: false + default: false + type: boolean + test-target: + description: "The make target in the calling repo used to run e2e tests" + required: false + default: "e2e" + type: string + working-directory: + description: "Working directory for the make test target" + required: false + default: "." + type: string + environment: + description: "GitHub environment name to use for this job (optional)" + required: false + default: "" + type: string + secrets: + kubeconfig: + description: "Kubeconfig for an external cluster (optional, used instead of spinning up KinD)" + required: false + +jobs: + e2e: + runs-on: ${{ inputs.runs-on }} + environment: ${{ inputs.environment != '' && inputs.environment || null }} + defaults: + run: + working-directory: ${{ inputs.working-directory }} + steps: + - name: Checkout calling repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version-file: ${{ inputs.working-directory != '.' && format('{0}/go.mod', inputs.working-directory) || 'go.mod' }} + cache: true + + - name: Run Greenhouse E2E composite action + uses: cloudoperators/common/workflows/e2e@main + with: + admin-k8s-version: ${{ inputs.admin-k8s-version }} + remote-k8s-version: ${{ inputs.remote-k8s-version }} + scenario: ${{ inputs.scenario }} + ref: ${{ inputs.greenhouse-ref }} + + - name: Run e2e tests + run: make ${{ inputs.test-target }} diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml new file mode 100644 index 0000000..5f07f91 --- /dev/null +++ b/.github/workflows/shared-go-build.yaml @@ -0,0 +1,102 @@ +name: Shared Go Build + +on: + workflow_call: + inputs: + runs-on: + description: "The runner to use for the job" + required: false + default: "ubuntu-latest" + type: string + go-version: + description: "Go version to use. If not set, version is read from go.mod" + required: false + default: "" + type: string + working-directory: + description: "Working directory for the job" + required: false + default: "." + type: string + build-target: + description: "The make target to run for the build (e.g. build, build-all, docker-build)" + required: false + default: "build" + type: string + docker-build: + description: "Build and optionally push a Docker image after the Go build" + required: false + default: false + type: boolean + image-name: + description: "Full image name including registry, e.g. ghcr.io/cloudoperators/myapp" + required: false + default: "" + type: string + platforms: + description: "Comma-separated list of target platforms for the Docker image" + required: false + default: "linux/amd64,linux/arm64" + type: string + push: + description: "Push the built Docker image to the registry" + required: false + default: false + type: boolean + secrets: + registry-token: + description: "Token used to authenticate to the container registry when pushing" + required: false + +jobs: + build: + runs-on: ${{ inputs.runs-on }} + defaults: + run: + working-directory: ${{ inputs.working-directory }} + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: ${{ inputs.go-version != '' && inputs.go-version || null }} + go-version-file: ${{ inputs.go-version == '' && format('{0}/go.mod', inputs.working-directory) || null }} + cache: true + + - name: Build + run: make ${{ inputs.build-target }} + + - name: Set up QEMU + if: inputs.docker-build == true + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff9c25c0e60b9eba63c # v3 + + - name: Set up Docker Buildx + if: inputs.docker-build == true + uses: docker/setup-buildx-action@b5730b4fe97e6f9f14b9d7bb5f0f0b9f75a3b6ca # v3 + + - name: Log in to container registry + if: inputs.docker-build == true && inputs.push == true + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.registry-token != '' && secrets.registry-token || secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + if: inputs.docker-build == true + id: meta + uses: docker/metadata-action@902fa8ec7d6ecbea8a63d9c1064e4b9e02685b72 # v5 + with: + images: ${{ inputs.image-name }} + + - name: Build and push Docker image + if: inputs.docker-build == true + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 + with: + context: ${{ inputs.working-directory }} + platforms: ${{ inputs.platforms }} + push: ${{ inputs.push }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml new file mode 100644 index 0000000..c265dc7 --- /dev/null +++ b/.github/workflows/shared-go-lint.yaml @@ -0,0 +1,57 @@ +name: Shared Go Lint + +on: + workflow_call: + inputs: + runs-on: + description: "The runner to use for the job" + required: false + default: "ubuntu-latest" + type: string + go-version: + description: "Go version to use. If not set, version is read from go.mod" + required: false + default: "" + type: string + golangci-lint-version: + description: "golangci-lint version to use" + required: false + default: "latest" + type: string + working-directory: + description: "Working directory for the job" + required: false + default: "." + type: string + enable-govulncheck: + description: "Run govulncheck in addition to golangci-lint" + required: false + default: false + type: boolean + +jobs: + lint: + runs-on: ${{ inputs.runs-on }} + defaults: + run: + working-directory: ${{ inputs.working-directory }} + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: ${{ inputs.go-version != '' && inputs.go-version || null }} + go-version-file: ${{ inputs.go-version == '' && format('{0}/go.mod', inputs.working-directory) || null }} + cache: true + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 + with: + version: ${{ inputs.golangci-lint-version }} + working-directory: ${{ inputs.working-directory }} + + - name: Run govulncheck + if: inputs.enable-govulncheck == true + run: go run golang.org/x/vuln/cmd/govulncheck@latest ./... diff --git a/.github/workflows/shared-go-test.yaml b/.github/workflows/shared-go-test.yaml new file mode 100644 index 0000000..70d55f9 --- /dev/null +++ b/.github/workflows/shared-go-test.yaml @@ -0,0 +1,81 @@ +name: Shared Go Test + +on: + workflow_call: + inputs: + runs-on: + description: "The runner to use for the job" + required: false + default: "ubuntu-latest" + type: string + go-version: + description: "Go version to use. If not set, version is read from go.mod" + required: false + default: "" + type: string + working-directory: + description: "Working directory for the job" + required: false + default: "." + type: string + test-target: + description: "The make target to run for tests" + required: false + default: "test" + type: string + upload-coverage: + description: "Upload coverage report as an artifact" + required: false + default: false + type: boolean + coverage-artifact-name: + description: "Name of the coverage artifact" + required: false + default: "code-coverage" + type: string + coverage-path: + description: "Path to the coverage output file" + required: false + default: "build/cover.out" + type: string + extra-env: + description: "Extra environment variables as newline-separated KEY=VALUE pairs" + required: false + default: "" + type: string + +jobs: + test: + runs-on: ${{ inputs.runs-on }} + defaults: + run: + working-directory: ${{ inputs.working-directory }} + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: ${{ inputs.go-version != '' && inputs.go-version || null }} + go-version-file: ${{ inputs.go-version == '' && format('{0}/go.mod', inputs.working-directory) || null }} + cache: true + + - name: Download Go modules + run: go mod download + + - name: Set extra environment variables + if: inputs.extra-env != '' + run: | + echo "${{ inputs.extra-env }}" >> "$GITHUB_ENV" + + - name: Run tests + run: make ${{ inputs.test-target }} + + - name: Upload coverage report + if: inputs.upload-coverage == true + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: ${{ inputs.coverage-artifact-name }} + path: ${{ inputs.working-directory != '.' && format('{0}/{1}', inputs.working-directory, inputs.coverage-path) || inputs.coverage-path }} + if-no-files-found: warn diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml new file mode 100644 index 0000000..4c7d005 --- /dev/null +++ b/.github/workflows/shared-release.yaml @@ -0,0 +1,143 @@ +name: Shared Release + +on: + workflow_call: + inputs: + runs-on: + description: "The runner to use for the job" + required: false + default: "ubuntu-latest" + type: string + bump: + description: "Version bump type: patch, minor, or major" + required: true + type: string + chart-path: + description: "Relative path to Chart.yaml to update (e.g. charts/my-app/Chart.yaml). Leave empty to skip." + required: false + default: "" + type: string + makefile-path: + description: "Relative path to the Makefile containing the VERSION variable" + required: false + default: "Makefile" + type: string + bump-make-version: + description: "Update the IMG tag in the Makefile to the new version" + required: false + default: false + type: boolean + dispatch-greenhouse-extensions: + description: "Trigger a repository_dispatch event on cloudoperators/greenhouse-extensions after release" + required: false + default: false + type: boolean + plugin-name: + description: "Plugin name passed as payload when dispatching to greenhouse-extensions" + required: false + default: "" + type: string + secrets: + release-token: + description: "PAT with contents:write and pull-requests:write permissions" + required: true + dispatch-token: + description: "Token used to trigger repository_dispatch on greenhouse-extensions" + required: false + +jobs: + release: + runs-on: ${{ inputs.runs-on }} + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + token: ${{ secrets.release-token }} + fetch-depth: 0 + + - name: Compute new version + id: version + run: | + CURRENT_VERSION=$(grep -E '^VERSION\s*[:?]?=' ${{ inputs.makefile-path }} | head -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + echo "Current version: $CURRENT_VERSION" + + MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1) + MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) + PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3) + + case "${{ inputs.bump }}" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + *) + echo "Invalid bump type: ${{ inputs.bump }}" + exit 1 + ;; + esac + + NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" + echo "New version: $NEW_VERSION" + echo "current=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" + echo "new=$NEW_VERSION" >> "$GITHUB_OUTPUT" + + - name: Update Makefile VERSION + run: | + sed -i "s/^VERSION\s*[:?]\?=.*/VERSION = ${{ steps.version.outputs.new }}/" ${{ inputs.makefile-path }} + + - name: Update Makefile IMG tag + if: inputs.bump-make-version == true + run: | + sed -i "s|\(IMG\s*[:?]\?=.*:\).*|\1${{ steps.version.outputs.new }}|" ${{ inputs.makefile-path }} + + - name: Update Chart.yaml + if: inputs.chart-path != '' + run: | + sed -i "s/^version:.*/version: ${{ steps.version.outputs.new }}/" ${{ inputs.chart-path }} + sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.new }}\"/" ${{ inputs.chart-path }} + + - name: Create release branch and PR + env: + GH_TOKEN: ${{ secrets.release-token }} + run: | + BRANCH="release-${{ steps.version.outputs.new }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add ${{ inputs.makefile-path }} ${{ inputs.chart-path != '' && inputs.chart-path || '' }} + git commit -m "chore: bump version to ${{ steps.version.outputs.new }}" + git push origin "$BRANCH" + gh pr create \ + --title "chore: release v${{ steps.version.outputs.new }}" \ + --body "Automated version bump from \`${{ steps.version.outputs.current }}\` to \`${{ steps.version.outputs.new }}\`." \ + --base main \ + --head "$BRANCH" + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.release-token }} + run: | + gh release create "v${{ steps.version.outputs.new }}" \ + --generate-notes \ + --title "v${{ steps.version.outputs.new }}" + + - name: Dispatch to greenhouse-extensions + if: inputs.dispatch-greenhouse-extensions == true + env: + GH_TOKEN: ${{ secrets.dispatch-token }} + run: | + gh api repos/cloudoperators/greenhouse-extensions/dispatches \ + --method POST \ + --field event_type=plugin-release \ + --field client_payload='{"plugin":"${{ inputs.plugin-name }}","version":"${{ steps.version.outputs.new }}"}' From 346f2b9a53cca9f511de5a9b5819c8605c671738 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 17:35:45 +0200 Subject: [PATCH 02/31] fix(workflows): address Copilot review comments shared-release.yaml: - Replace non-POSIX \s with [[:space:]] in grep and sed expressions - Add -E flag to sed for portable extended regex - Add fail-fast VERSION parse validation (must match X.Y.Z) - Add environment input so callers can gate on protection rules - Fall back to release-token when dispatch-token is not provided shared-e2e.yaml: - Checkout calling repo into path: caller to avoid workspace collision with the greenhouse checkout in the e2e composite action - Run e2e make target in caller/ - Remove unused with-remote-cluster input - Remove unused kubeconfig secret shared-go-build.yaml: - Add packages: write permission when push is true - Add early validation step when docker-build is true and image-name is empty Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 18 ++++-------------- .github/workflows/shared-go-build.yaml | 11 +++++++++++ .github/workflows/shared-release.yaml | 19 +++++++++++++++---- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index a2206e9..0bb90e8 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -25,11 +25,6 @@ on: description: "E2E scenario name passed to the composite action and make target" required: true type: string - with-remote-cluster: - description: "Spin up a second KinD cluster as the remote cluster" - required: false - default: false - type: boolean test-target: description: "The make target in the calling repo used to run e2e tests" required: false @@ -45,26 +40,20 @@ on: required: false default: "" type: string - secrets: - kubeconfig: - description: "Kubeconfig for an external cluster (optional, used instead of spinning up KinD)" - required: false - jobs: e2e: runs-on: ${{ inputs.runs-on }} environment: ${{ inputs.environment != '' && inputs.environment || null }} - defaults: - run: - working-directory: ${{ inputs.working-directory }} steps: - name: Checkout calling repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + path: caller - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: - go-version-file: ${{ inputs.working-directory != '.' && format('{0}/go.mod', inputs.working-directory) || 'go.mod' }} + go-version-file: caller/${{ inputs.working-directory != '.' && format('{0}/go.mod', inputs.working-directory) || 'go.mod' }} cache: true - name: Run Greenhouse E2E composite action @@ -76,4 +65,5 @@ jobs: ref: ${{ inputs.greenhouse-ref }} - name: Run e2e tests + working-directory: caller/${{ inputs.working-directory }} run: make ${{ inputs.test-target }} diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index 5f07f91..183e3e6 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -51,6 +51,9 @@ on: jobs: build: runs-on: ${{ inputs.runs-on }} + permissions: + contents: read + packages: ${{ inputs.push == true && 'write' || 'read' }} defaults: run: working-directory: ${{ inputs.working-directory }} @@ -68,6 +71,14 @@ jobs: - name: Build run: make ${{ inputs.build-target }} + - name: Validate docker-build inputs + if: inputs.docker-build == true + run: | + if [ -z "${{ inputs.image-name }}" ]; then + echo "ERROR: image-name is required when docker-build is true" + exit 1 + fi + - name: Set up QEMU if: inputs.docker-build == true uses: docker/setup-qemu-action@29109295f81e9208d7d86ff9c25c0e60b9eba63c # v3 diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 4c7d005..7331687 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -37,6 +37,11 @@ on: required: false default: "" type: string + environment: + description: "GitHub environment name to use for this job (e.g. release), for environment protection rules and scoped secrets" + required: false + default: "" + type: string secrets: release-token: description: "PAT with contents:write and pull-requests:write permissions" @@ -48,6 +53,7 @@ on: jobs: release: runs-on: ${{ inputs.runs-on }} + environment: ${{ inputs.environment != '' && inputs.environment || null }} permissions: contents: write pull-requests: write @@ -61,9 +67,14 @@ jobs: - name: Compute new version id: version run: | - CURRENT_VERSION=$(grep -E '^VERSION\s*[:?]?=' ${{ inputs.makefile-path }} | head -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' ${{ inputs.makefile-path }} | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]') echo "Current version: $CURRENT_VERSION" + if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "ERROR: could not parse VERSION from ${{ inputs.makefile-path }} (got: '$CURRENT_VERSION')" + exit 1 + fi + MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1) MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3) @@ -94,12 +105,12 @@ jobs: - name: Update Makefile VERSION run: | - sed -i "s/^VERSION\s*[:?]\?=.*/VERSION = ${{ steps.version.outputs.new }}/" ${{ inputs.makefile-path }} + sed -i -E "s/^VERSION[[:space:]]*[:?]?=.*/VERSION = ${{ steps.version.outputs.new }}/" "${{ inputs.makefile-path }}" - name: Update Makefile IMG tag if: inputs.bump-make-version == true run: | - sed -i "s|\(IMG\s*[:?]\?=.*:\).*|\1${{ steps.version.outputs.new }}|" ${{ inputs.makefile-path }} + sed -i -E "s|(IMG[[:space:]]*[:?]?=.*:).*|\1${{ steps.version.outputs.new }}|" "${{ inputs.makefile-path }}" - name: Update Chart.yaml if: inputs.chart-path != '' @@ -135,7 +146,7 @@ jobs: - name: Dispatch to greenhouse-extensions if: inputs.dispatch-greenhouse-extensions == true env: - GH_TOKEN: ${{ secrets.dispatch-token }} + GH_TOKEN: ${{ secrets.dispatch-token != '' && secrets.dispatch-token || secrets.release-token }} run: | gh api repos/cloudoperators/greenhouse-extensions/dispatches \ --method POST \ From 7147bd9476edc294c2c76bbef0535ba9e18c9097 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 17:59:31 +0200 Subject: [PATCH 03/31] fix(workflows): add explicit permissions: contents: read to shared-go-test job Signed-off-by: I313226 --- .github/workflows/shared-go-test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/shared-go-test.yaml b/.github/workflows/shared-go-test.yaml index 70d55f9..b3efbb1 100644 --- a/.github/workflows/shared-go-test.yaml +++ b/.github/workflows/shared-go-test.yaml @@ -47,6 +47,8 @@ on: jobs: test: runs-on: ${{ inputs.runs-on }} + permissions: + contents: read defaults: run: working-directory: ${{ inputs.working-directory }} From 1e884b198e89c2bd2d2691ebd16fb835dbc449d4 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 19:12:10 +0200 Subject: [PATCH 04/31] fix(workflows): harden shared-release.yaml - Checkout default branch explicitly (ref: default_branch) so releases are never cut from a feature branch - Use github.event.repository.default_branch instead of hard-coded 'main' for PR base and release --target, supporting non-main default branches - Quote ${{ inputs.makefile-path }} in grep invocation - Quote ${{ inputs.chart-path }} in both sed commands - Create GitHub release targeting the default branch after fetching latest, so the tag points at the correct merged commit Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 7331687..38adb9c 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -62,12 +62,13 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: token: ${{ secrets.release-token }} + ref: ${{ github.event.repository.default_branch }} fetch-depth: 0 - name: Compute new version id: version run: | - CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' ${{ inputs.makefile-path }} | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]') + CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "${{ inputs.makefile-path }}" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]') echo "Current version: $CURRENT_VERSION" if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then @@ -115,8 +116,8 @@ jobs: - name: Update Chart.yaml if: inputs.chart-path != '' run: | - sed -i "s/^version:.*/version: ${{ steps.version.outputs.new }}/" ${{ inputs.chart-path }} - sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.new }}\"/" ${{ inputs.chart-path }} + sed -i "s/^version:.*/version: ${{ steps.version.outputs.new }}/" "${{ inputs.chart-path }}" + sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.new }}\"/" "${{ inputs.chart-path }}" - name: Create release branch and PR env: @@ -129,19 +130,26 @@ jobs: git add ${{ inputs.makefile-path }} ${{ inputs.chart-path != '' && inputs.chart-path || '' }} git commit -m "chore: bump version to ${{ steps.version.outputs.new }}" git push origin "$BRANCH" + DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" gh pr create \ --title "chore: release v${{ steps.version.outputs.new }}" \ --body "Automated version bump from \`${{ steps.version.outputs.current }}\` to \`${{ steps.version.outputs.new }}\`." \ - --base main \ + --base "$DEFAULT_BRANCH" \ --head "$BRANCH" - name: Create GitHub release env: GH_TOKEN: ${{ secrets.release-token }} run: | + # Fetch the default branch so the release tag points at the latest merged commit + DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" + git fetch origin "$DEFAULT_BRANCH" + git checkout "$DEFAULT_BRANCH" + git pull origin "$DEFAULT_BRANCH" gh release create "v${{ steps.version.outputs.new }}" \ --generate-notes \ - --title "v${{ steps.version.outputs.new }}" + --title "v${{ steps.version.outputs.new }}" \ + --target "$DEFAULT_BRANCH" - name: Dispatch to greenhouse-extensions if: inputs.dispatch-greenhouse-extensions == true From 845b28e97a28b94e2a4ae676579815b781d5cb78 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 20:29:41 +0200 Subject: [PATCH 05/31] fix(workflows): address abhijith-darshan review comments - shared-go-test: drop go-version input, always use go-version-file from go.mod - shared-release: add if condition to Update Makefile VERSION step (skip when makefile-path is empty) - shared-release: split Chart.yaml update into two steps with separate bump-chart-app-version input - shared-release: replace gh CLI release creation with actions/github-script@v7 - shared-release: replace dispatch-token with GitHub App token via actions/create-github-app-token@v3 and peter-evans/repository-dispatch@v4 Signed-off-by: I313226 --- .github/workflows/shared-go-test.yaml | 8 +--- .github/workflows/shared-release.yaml | 68 ++++++++++++++++++--------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/.github/workflows/shared-go-test.yaml b/.github/workflows/shared-go-test.yaml index b3efbb1..d792dd9 100644 --- a/.github/workflows/shared-go-test.yaml +++ b/.github/workflows/shared-go-test.yaml @@ -8,11 +8,6 @@ on: required: false default: "ubuntu-latest" type: string - go-version: - description: "Go version to use. If not set, version is read from go.mod" - required: false - default: "" - type: string working-directory: description: "Working directory for the job" required: false @@ -59,8 +54,7 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: - go-version: ${{ inputs.go-version != '' && inputs.go-version || null }} - go-version-file: ${{ inputs.go-version == '' && format('{0}/go.mod', inputs.working-directory) || null }} + go-version-file: ${{ format('{0}/go.mod', inputs.working-directory) }} cache: true - name: Download Go modules diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 38adb9c..900f050 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -17,6 +17,11 @@ on: required: false default: "" type: string + bump-chart-app-version: + description: "Update the appVersion field in Chart.yaml to the new version" + required: false + default: true + type: boolean makefile-path: description: "Relative path to the Makefile containing the VERSION variable" required: false @@ -46,8 +51,11 @@ on: release-token: description: "PAT with contents:write and pull-requests:write permissions" required: true - dispatch-token: - description: "Token used to trigger repository_dispatch on greenhouse-extensions" + dispatch-app-id: + description: "GitHub App ID used to generate a token for dispatching to greenhouse-extensions" + required: false + dispatch-app-private-key: + description: "GitHub App private key used to generate a token for dispatching to greenhouse-extensions" required: false jobs: @@ -105,6 +113,7 @@ jobs: echo "new=$NEW_VERSION" >> "$GITHUB_OUTPUT" - name: Update Makefile VERSION + if: inputs.makefile-path != '' run: | sed -i -E "s/^VERSION[[:space:]]*[:?]?=.*/VERSION = ${{ steps.version.outputs.new }}/" "${{ inputs.makefile-path }}" @@ -113,10 +122,14 @@ jobs: run: | sed -i -E "s|(IMG[[:space:]]*[:?]?=.*:).*|\1${{ steps.version.outputs.new }}|" "${{ inputs.makefile-path }}" - - name: Update Chart.yaml + - name: Update Chart.yaml version if: inputs.chart-path != '' run: | sed -i "s/^version:.*/version: ${{ steps.version.outputs.new }}/" "${{ inputs.chart-path }}" + + - name: Update Chart.yaml appVersion + if: inputs.chart-path != '' && inputs.bump-chart-app-version == true + run: | sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.new }}\"/" "${{ inputs.chart-path }}" - name: Create release branch and PR @@ -138,25 +151,36 @@ jobs: --head "$BRANCH" - name: Create GitHub release - env: - GH_TOKEN: ${{ secrets.release-token }} - run: | - # Fetch the default branch so the release tag points at the latest merged commit - DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" - git fetch origin "$DEFAULT_BRANCH" - git checkout "$DEFAULT_BRANCH" - git pull origin "$DEFAULT_BRANCH" - gh release create "v${{ steps.version.outputs.new }}" \ - --generate-notes \ - --title "v${{ steps.version.outputs.new }}" \ - --target "$DEFAULT_BRANCH" + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + github-token: ${{ secrets.release-token }} + script: | + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: 'v${{ steps.version.outputs.new }}', + name: 'v${{ steps.version.outputs.new }}', + generate_release_notes: true, + draft: false, + prerelease: false, + target_commitish: context.payload.repository.default_branch + }); + + - name: Generate GitHub App token for dispatch + if: inputs.dispatch-greenhouse-extensions == true + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + app-id: ${{ secrets.dispatch-app-id }} + private-key: ${{ secrets.dispatch-app-private-key }} + repositories: greenhouse-extensions + permission-contents: write - name: Dispatch to greenhouse-extensions if: inputs.dispatch-greenhouse-extensions == true - env: - GH_TOKEN: ${{ secrets.dispatch-token != '' && secrets.dispatch-token || secrets.release-token }} - run: | - gh api repos/cloudoperators/greenhouse-extensions/dispatches \ - --method POST \ - --field event_type=plugin-release \ - --field client_payload='{"plugin":"${{ inputs.plugin-name }}","version":"${{ steps.version.outputs.new }}"}' + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 + with: + token: ${{ steps.app-token.outputs.token }} + repository: cloudoperators/greenhouse-extensions + event-type: deployment-params + client-payload: '{"plugin":"${{ inputs.plugin-name }}","version":"${{ steps.version.outputs.new }}"}' From 74c09c76a1c646fab795c096f477d2c9cdc8d353 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 20:35:58 +0200 Subject: [PATCH 06/31] fix(workflows): drop go-version input, always use go-version-file from go.mod Remove the go-version input and the conditional go-version/go-version-file logic from shared-go-lint, shared-go-test, and shared-go-build. All three workflows now unconditionally read the Go version from go.mod. Also rename shared-release workflow to "Shared Release Bump" to clarify that this workflow performs a version bump and creates a PR, not a direct release deployment. Signed-off-by: I313226 --- .github/workflows/shared-go-build.yaml | 8 +------- .github/workflows/shared-go-lint.yaml | 8 +------- .github/workflows/shared-release.yaml | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index 183e3e6..9a08797 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -8,11 +8,6 @@ on: required: false default: "ubuntu-latest" type: string - go-version: - description: "Go version to use. If not set, version is read from go.mod" - required: false - default: "" - type: string working-directory: description: "Working directory for the job" required: false @@ -64,8 +59,7 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: - go-version: ${{ inputs.go-version != '' && inputs.go-version || null }} - go-version-file: ${{ inputs.go-version == '' && format('{0}/go.mod', inputs.working-directory) || null }} + go-version-file: ${{ format('{0}/go.mod', inputs.working-directory) }} cache: true - name: Build diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index c265dc7..b8d625c 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -8,11 +8,6 @@ on: required: false default: "ubuntu-latest" type: string - go-version: - description: "Go version to use. If not set, version is read from go.mod" - required: false - default: "" - type: string golangci-lint-version: description: "golangci-lint version to use" required: false @@ -42,8 +37,7 @@ jobs: - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: - go-version: ${{ inputs.go-version != '' && inputs.go-version || null }} - go-version-file: ${{ inputs.go-version == '' && format('{0}/go.mod', inputs.working-directory) || null }} + go-version-file: ${{ format('{0}/go.mod', inputs.working-directory) }} cache: true - name: Run golangci-lint diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 900f050..b4bc194 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -1,4 +1,4 @@ -name: Shared Release +name: Shared Release Bump on: workflow_call: From bf067fdb825da9e60921b1fc4781a27458bc057b Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 20:38:43 +0200 Subject: [PATCH 07/31] fix(workflows): fix git add quoting and add missing permissions to lint job - shared-release: pass makefile-path and chart-path as env vars in the git add step to prevent word-splitting/glob expansion on paths with spaces - shared-go-lint: add explicit permissions: contents: read so the job works in repos with restricted default GITHUB_TOKEN permissions Signed-off-by: I313226 --- .github/workflows/shared-go-lint.yaml | 2 ++ .github/workflows/shared-release.yaml | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index b8d625c..ba7801d 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -27,6 +27,8 @@ on: jobs: lint: runs-on: ${{ inputs.runs-on }} + permissions: + contents: read defaults: run: working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index b4bc194..5dbc707 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -135,12 +135,15 @@ jobs: - name: Create release branch and PR env: GH_TOKEN: ${{ secrets.release-token }} + MAKEFILE_PATH: ${{ inputs.makefile-path }} + CHART_PATH: ${{ inputs.chart-path }} run: | BRANCH="release-${{ steps.version.outputs.new }}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -b "$BRANCH" - git add ${{ inputs.makefile-path }} ${{ inputs.chart-path != '' && inputs.chart-path || '' }} + git add "$MAKEFILE_PATH" + if [ -n "$CHART_PATH" ]; then git add "$CHART_PATH"; fi git commit -m "chore: bump version to ${{ steps.version.outputs.new }}" git push origin "$BRANCH" DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" From 42b719bc91992057982396f3b6fb9a13882d5798 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 21:35:27 +0200 Subject: [PATCH 08/31] fix(workflows): add guards for makefile-path and dispatch inputs - Add makefile-path != '' guard to bump-make-version step to prevent sed running on an empty path - Add fail-fast validation step before dispatch: error early when dispatch-greenhouse-extensions is true but dispatch-app-id, dispatch-app-private-key, or plugin-name are missing Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 5dbc707..9b80cca 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -118,7 +118,7 @@ jobs: sed -i -E "s/^VERSION[[:space:]]*[:?]?=.*/VERSION = ${{ steps.version.outputs.new }}/" "${{ inputs.makefile-path }}" - name: Update Makefile IMG tag - if: inputs.bump-make-version == true + if: inputs.bump-make-version == true && inputs.makefile-path != '' run: | sed -i -E "s|(IMG[[:space:]]*[:?]?=.*:).*|\1${{ steps.version.outputs.new }}|" "${{ inputs.makefile-path }}" @@ -169,6 +169,18 @@ jobs: target_commitish: context.payload.repository.default_branch }); + - name: Validate dispatch inputs + if: inputs.dispatch-greenhouse-extensions == true + run: | + if [ -z "${{ secrets.dispatch-app-id }}" ] || [ -z "${{ secrets.dispatch-app-private-key }}" ]; then + echo "ERROR: dispatch-app-id and dispatch-app-private-key secrets are required when dispatch-greenhouse-extensions is true" + exit 1 + fi + if [ -z "${{ inputs.plugin-name }}" ]; then + echo "ERROR: plugin-name input is required when dispatch-greenhouse-extensions is true" + exit 1 + fi + - name: Generate GitHub App token for dispatch if: inputs.dispatch-greenhouse-extensions == true id: app-token From c413099837dad635d79e268f08a0e9f41a4b85d0 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 21:40:47 +0200 Subject: [PATCH 09/31] chore: update status file with review round 4 changes Signed-off-by: I313226 --- GHA-STREAMLINING-STATUS.md | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 GHA-STREAMLINING-STATUS.md diff --git a/GHA-STREAMLINING-STATUS.md b/GHA-STREAMLINING-STATUS.md new file mode 100644 index 0000000..d56e37b --- /dev/null +++ b/GHA-STREAMLINING-STATUS.md @@ -0,0 +1,127 @@ +# GHA Streamlining Status (cloudoperators/common#2086) + +**Tracking issue:** [cloudoperators/greenhouse#2086](https://github.com/cloudoperators/greenhouse/issues/2086) +**Last updated:** 2026-06-24 (Copilot + abhijith-darshan review round 4) + +--- + +## PRs — merge order matters + +> **common#65 must merge first.** All consumer PRs depend on it resolving `@main`. + +| # | Repo | PR | Status | Notes | +|---|---|---|---|---| +| 1 | `cloudoperators/common` | [#65](https://github.com/cloudoperators/common/pull/65) | Open — awaiting Copilot review round 5 | Adds 5 new shared reusable workflows. **Merge this first.** | +| 2 | `cloudoperators/repo-guard` | [#167](https://github.com/cloudoperators/repo-guard/pull/167) | Open — clean pass | Depends on common#65 | +| 3 | `cloudoperators/cloudctl` | [#57](https://github.com/cloudoperators/cloudctl/pull/57) | Open — clean pass | Depends on common#65 | +| 4 | `cloudoperators/shoot-grafter` | [#66](https://github.com/cloudoperators/shoot-grafter/pull/66) | Open — clean pass | Depends on common#65 | +| 5 | `cloudoperators/permission-manager` | [#39](https://github.com/cloudoperators/permission-manager/pull/39) | Open — clean pass | Depends on common#65 | + +--- + +## What each PR does + +### common#65 — new shared workflows + +Five new files in `.github/workflows/`: + +| File | Purpose | +|---|---| +| `shared-go-lint.yaml` | golangci-lint + optional govulncheck via `enable-govulncheck: true`. Go version always read from `go.mod`. | +| `shared-go-test.yaml` | Parameterized `make` test target; optional coverage artifact upload. Go version always read from `go.mod`. | +| `shared-go-build.yaml` | Go binary build + optional Docker multi-arch build/push to GHCR. Go version always read from `go.mod`. | +| `shared-release.yaml` (renamed to "Shared Release Bump") | Semver bump → Chart.yaml (version + optional appVersion) + Makefile update → release PR → GitHub release via `actions/github-script@v7` → optional `repository_dispatch` to greenhouse-extensions via GitHub App token | +| `shared-e2e.yaml` | Wraps existing `workflows/e2e` composite action for KinD-based e2e | + +**Key design choices:** +- `go-version` input removed from all three Go workflows — always reads from `go.mod` +- `dispatch-token` secret replaced by `dispatch-app-id` + `dispatch-app-private-key` (GitHub App token via `actions/create-github-app-token@v3`) +- Chart.yaml update split into two steps: `version` always bumped when `chart-path` set; `appVersion` controlled by `bump-chart-app-version` input (default `true`) +- GitHub release created via `actions/github-script@v7` (not `gh` CLI) +- `peter-evans/repository-dispatch@v4` for greenhouse-extensions dispatch + +**Latest commits on `feat/shared-go-workflows`:** +- `42b719b` — guards for makefile-path and dispatch inputs +- `bf067fd` — git add quoting, permissions on lint job +- `74c09c7` — drop go-version input from lint/build/build +- `845b28e` — abhijith-darshan review round + +### repo-guard#167 + +- `ci.yaml`: `lint` + `test` jobs delegate to `shared-go-lint` and `shared-go-test` respectively. + The `e2e` job stays **inline** (k3d + mock-GitHub, not compatible with greenhouse KinD composite). +- `release-cut.yaml`: all inline semver/PR/release logic replaced by `shared-release.yaml`. + Includes `dispatch-greenhouse-extensions: true`; passes `CLOUDOPERATOR_APP_ID` and `CLOUDOPERATOR_APP_PRIVATE_KEY` as App secrets for the dispatch step. + +### cloudctl#57 + +- `test.yaml`: `unit` job delegates to `shared-go-test.yaml` (`test-target: test`). + The `e2e` job stays **inline** (k3d setup, not compatible with greenhouse KinD composite). + +### shoot-grafter#66 + +- `ci.yaml`: `build` → `shared-go-build` (`build-target: build-all`); `test` → `shared-go-test` (`test-target: test-with-envtest`, coverage upload). `code_coverage` job stays inline. +- `checks.yaml`: new `lint` job calls `shared-go-lint` with `enable-govulncheck: true`. + The `checks` job retains shellcheck, typos, addlicense, reuse, dependency-licenses inline. + +### permission-manager#39 + +- New `ci.yaml` added (repo had no CI before). Calls `shared-go-lint`, `shared-go-test`, and `shared-go-build` (no `working-directory` override — Go module is at repo root). +- Existing `release.yaml` (multi-arch Docker + cosign + SBOM) is **unchanged**. + +--- + +## Local branches + +| Repo | Branch | Local path | +|---|---|---| +| `common` | `feat/shared-go-workflows` | `/Users/I313226/CODE/common` | +| `repo-guard` | `feat/shared-workflows` | `/Users/I313226/CODE/repo-guard` | +| `cloudctl` | `feat/shared-workflows` | `/Users/I313226/CODE/cloudctl` | +| `shoot-grafter` | `feat/shared-workflows` | `/Users/I313226/CODE/shoot-grafter` | +| `permission-manager` | `feat/shared-workflows` | `/Users/I313226/CODE/permission-manager` | + +--- + +## Known gaps / follow-ups + +| Item | Detail | +|---|---| +| **e2e standardization** | `repo-guard` and `cloudctl` e2e jobs use k3d with repo-specific mock setups. To use `shared-e2e.yaml`, those repos would need to migrate to KinD and the greenhouse composite action. | +| **shoot-grafter release** | No `charts/` dir or `VERSION` in Makefile. Release process needs to be defined before `shared-release.yaml` can be wired up. | +| **permission-manager release-cut** | Same as above — no charts, no `VERSION`. Existing `release.yaml` handles image publishing; version bump workflow TBD. | +| **shoot-grafter remaining checks** | `shellcheck`, `typos`, `check-addlicense`, `reuse`, `check-dependency-licenses` remain inline. Could be extracted into a shared `shared-go-checks.yaml` if other repos need the same. | +| **greenhouse-extensions** | Helm-based CI/release left unchanged (different paradigm). `repository_dispatch` receiver for `plugin-release` events should be verified/added. | + +--- + +## Workflow call graph (after common#65 merges) + +``` +repo-guard/ci.yaml + ├── lint → common/shared-go-lint.yaml@main + ├── test → common/shared-go-test.yaml@main + └── e2e → inline (k3d) + +repo-guard/release-cut.yaml + └── cut-release → common/shared-release.yaml@main + └── dispatches → greenhouse-extensions (plugin-release) + +cloudctl/test.yaml + ├── unit → common/shared-go-test.yaml@main + └── e2e → inline (k3d) + +shoot-grafter/ci.yaml + ├── build → common/shared-go-build.yaml@main + ├── test → common/shared-go-test.yaml@main + └── code_coverage → inline + +shoot-grafter/checks.yaml + ├── lint → common/shared-go-lint.yaml@main + └── checks → inline (shellcheck, typos, addlicense, reuse, dep-licenses) + +permission-manager/ci.yaml + ├── lint → common/shared-go-lint.yaml@main + ├── test → common/shared-go-test.yaml@main + └── build → common/shared-go-build.yaml@main +``` From f4bdca774e511fe29b89153c307a2a45981006fd Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 21:42:05 +0200 Subject: [PATCH 10/31] Revert "chore: update status file with review round 4 changes" This reverts commit c413099837dad635d79e268f08a0e9f41a4b85d0. Signed-off-by: I313226 --- GHA-STREAMLINING-STATUS.md | 127 ------------------------------------- 1 file changed, 127 deletions(-) delete mode 100644 GHA-STREAMLINING-STATUS.md diff --git a/GHA-STREAMLINING-STATUS.md b/GHA-STREAMLINING-STATUS.md deleted file mode 100644 index d56e37b..0000000 --- a/GHA-STREAMLINING-STATUS.md +++ /dev/null @@ -1,127 +0,0 @@ -# GHA Streamlining Status (cloudoperators/common#2086) - -**Tracking issue:** [cloudoperators/greenhouse#2086](https://github.com/cloudoperators/greenhouse/issues/2086) -**Last updated:** 2026-06-24 (Copilot + abhijith-darshan review round 4) - ---- - -## PRs — merge order matters - -> **common#65 must merge first.** All consumer PRs depend on it resolving `@main`. - -| # | Repo | PR | Status | Notes | -|---|---|---|---|---| -| 1 | `cloudoperators/common` | [#65](https://github.com/cloudoperators/common/pull/65) | Open — awaiting Copilot review round 5 | Adds 5 new shared reusable workflows. **Merge this first.** | -| 2 | `cloudoperators/repo-guard` | [#167](https://github.com/cloudoperators/repo-guard/pull/167) | Open — clean pass | Depends on common#65 | -| 3 | `cloudoperators/cloudctl` | [#57](https://github.com/cloudoperators/cloudctl/pull/57) | Open — clean pass | Depends on common#65 | -| 4 | `cloudoperators/shoot-grafter` | [#66](https://github.com/cloudoperators/shoot-grafter/pull/66) | Open — clean pass | Depends on common#65 | -| 5 | `cloudoperators/permission-manager` | [#39](https://github.com/cloudoperators/permission-manager/pull/39) | Open — clean pass | Depends on common#65 | - ---- - -## What each PR does - -### common#65 — new shared workflows - -Five new files in `.github/workflows/`: - -| File | Purpose | -|---|---| -| `shared-go-lint.yaml` | golangci-lint + optional govulncheck via `enable-govulncheck: true`. Go version always read from `go.mod`. | -| `shared-go-test.yaml` | Parameterized `make` test target; optional coverage artifact upload. Go version always read from `go.mod`. | -| `shared-go-build.yaml` | Go binary build + optional Docker multi-arch build/push to GHCR. Go version always read from `go.mod`. | -| `shared-release.yaml` (renamed to "Shared Release Bump") | Semver bump → Chart.yaml (version + optional appVersion) + Makefile update → release PR → GitHub release via `actions/github-script@v7` → optional `repository_dispatch` to greenhouse-extensions via GitHub App token | -| `shared-e2e.yaml` | Wraps existing `workflows/e2e` composite action for KinD-based e2e | - -**Key design choices:** -- `go-version` input removed from all three Go workflows — always reads from `go.mod` -- `dispatch-token` secret replaced by `dispatch-app-id` + `dispatch-app-private-key` (GitHub App token via `actions/create-github-app-token@v3`) -- Chart.yaml update split into two steps: `version` always bumped when `chart-path` set; `appVersion` controlled by `bump-chart-app-version` input (default `true`) -- GitHub release created via `actions/github-script@v7` (not `gh` CLI) -- `peter-evans/repository-dispatch@v4` for greenhouse-extensions dispatch - -**Latest commits on `feat/shared-go-workflows`:** -- `42b719b` — guards for makefile-path and dispatch inputs -- `bf067fd` — git add quoting, permissions on lint job -- `74c09c7` — drop go-version input from lint/build/build -- `845b28e` — abhijith-darshan review round - -### repo-guard#167 - -- `ci.yaml`: `lint` + `test` jobs delegate to `shared-go-lint` and `shared-go-test` respectively. - The `e2e` job stays **inline** (k3d + mock-GitHub, not compatible with greenhouse KinD composite). -- `release-cut.yaml`: all inline semver/PR/release logic replaced by `shared-release.yaml`. - Includes `dispatch-greenhouse-extensions: true`; passes `CLOUDOPERATOR_APP_ID` and `CLOUDOPERATOR_APP_PRIVATE_KEY` as App secrets for the dispatch step. - -### cloudctl#57 - -- `test.yaml`: `unit` job delegates to `shared-go-test.yaml` (`test-target: test`). - The `e2e` job stays **inline** (k3d setup, not compatible with greenhouse KinD composite). - -### shoot-grafter#66 - -- `ci.yaml`: `build` → `shared-go-build` (`build-target: build-all`); `test` → `shared-go-test` (`test-target: test-with-envtest`, coverage upload). `code_coverage` job stays inline. -- `checks.yaml`: new `lint` job calls `shared-go-lint` with `enable-govulncheck: true`. - The `checks` job retains shellcheck, typos, addlicense, reuse, dependency-licenses inline. - -### permission-manager#39 - -- New `ci.yaml` added (repo had no CI before). Calls `shared-go-lint`, `shared-go-test`, and `shared-go-build` (no `working-directory` override — Go module is at repo root). -- Existing `release.yaml` (multi-arch Docker + cosign + SBOM) is **unchanged**. - ---- - -## Local branches - -| Repo | Branch | Local path | -|---|---|---| -| `common` | `feat/shared-go-workflows` | `/Users/I313226/CODE/common` | -| `repo-guard` | `feat/shared-workflows` | `/Users/I313226/CODE/repo-guard` | -| `cloudctl` | `feat/shared-workflows` | `/Users/I313226/CODE/cloudctl` | -| `shoot-grafter` | `feat/shared-workflows` | `/Users/I313226/CODE/shoot-grafter` | -| `permission-manager` | `feat/shared-workflows` | `/Users/I313226/CODE/permission-manager` | - ---- - -## Known gaps / follow-ups - -| Item | Detail | -|---|---| -| **e2e standardization** | `repo-guard` and `cloudctl` e2e jobs use k3d with repo-specific mock setups. To use `shared-e2e.yaml`, those repos would need to migrate to KinD and the greenhouse composite action. | -| **shoot-grafter release** | No `charts/` dir or `VERSION` in Makefile. Release process needs to be defined before `shared-release.yaml` can be wired up. | -| **permission-manager release-cut** | Same as above — no charts, no `VERSION`. Existing `release.yaml` handles image publishing; version bump workflow TBD. | -| **shoot-grafter remaining checks** | `shellcheck`, `typos`, `check-addlicense`, `reuse`, `check-dependency-licenses` remain inline. Could be extracted into a shared `shared-go-checks.yaml` if other repos need the same. | -| **greenhouse-extensions** | Helm-based CI/release left unchanged (different paradigm). `repository_dispatch` receiver for `plugin-release` events should be verified/added. | - ---- - -## Workflow call graph (after common#65 merges) - -``` -repo-guard/ci.yaml - ├── lint → common/shared-go-lint.yaml@main - ├── test → common/shared-go-test.yaml@main - └── e2e → inline (k3d) - -repo-guard/release-cut.yaml - └── cut-release → common/shared-release.yaml@main - └── dispatches → greenhouse-extensions (plugin-release) - -cloudctl/test.yaml - ├── unit → common/shared-go-test.yaml@main - └── e2e → inline (k3d) - -shoot-grafter/ci.yaml - ├── build → common/shared-go-build.yaml@main - ├── test → common/shared-go-test.yaml@main - └── code_coverage → inline - -shoot-grafter/checks.yaml - ├── lint → common/shared-go-lint.yaml@main - └── checks → inline (shellcheck, typos, addlicense, reuse, dep-licenses) - -permission-manager/ci.yaml - ├── lint → common/shared-go-lint.yaml@main - ├── test → common/shared-go-test.yaml@main - └── build → common/shared-go-build.yaml@main -``` From c39583929251d56592634607701f55b16b865087 Mon Sep 17 00:00:00 2001 From: I313226 Date: Wed, 24 Jun 2026 23:25:57 +0200 Subject: [PATCH 11/31] refactor(workflows): commit version bump directly to default branch; fix e2e Go setup ordering shared-release: replace release-branch + PR approach with a direct commit to the default branch followed by an annotated tag and GitHub release. This eliminates the dangling release branch, the need for pull-requests:write permissions, and the race where the release tag pointed at a pre-bump commit. The workflow now commits the Makefile/Chart changes straight to main, tags that exact commit, and creates the GitHub release against it. shared-e2e: move setup-go to after the Greenhouse composite action. The composite action runs its own actions/setup-go based on the Greenhouse go.mod, which was overwriting the caller's Go version. Running setup-go after the composite ensures the final make test step uses the caller's intended Go toolchain. Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 12 ++++++------ .github/workflows/shared-release.yaml | 25 ++++++++----------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index 0bb90e8..a924389 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -50,12 +50,6 @@ jobs: with: path: caller - - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 - with: - go-version-file: caller/${{ inputs.working-directory != '.' && format('{0}/go.mod', inputs.working-directory) || 'go.mod' }} - cache: true - - name: Run Greenhouse E2E composite action uses: cloudoperators/common/workflows/e2e@main with: @@ -64,6 +58,12 @@ jobs: scenario: ${{ inputs.scenario }} ref: ${{ inputs.greenhouse-ref }} + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version-file: caller/${{ inputs.working-directory != '.' && format('{0}/go.mod', inputs.working-directory) || 'go.mod' }} + cache: true + - name: Run e2e tests working-directory: caller/${{ inputs.working-directory }} run: make ${{ inputs.test-target }} diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 9b80cca..c81d630 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -1,4 +1,4 @@ -name: Shared Release Bump +name: Shared Release on: workflow_call: @@ -23,7 +23,7 @@ on: default: true type: boolean makefile-path: - description: "Relative path to the Makefile containing the VERSION variable" + description: "Relative path to the Makefile containing the VERSION variable. Leave empty to skip." required: false default: "Makefile" type: string @@ -49,7 +49,7 @@ on: type: string secrets: release-token: - description: "PAT with contents:write and pull-requests:write permissions" + description: "PAT with contents:write permissions" required: true dispatch-app-id: description: "GitHub App ID used to generate a token for dispatching to greenhouse-extensions" @@ -64,7 +64,6 @@ jobs: environment: ${{ inputs.environment != '' && inputs.environment || null }} permissions: contents: write - pull-requests: write steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -132,26 +131,19 @@ jobs: run: | sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.new }}\"/" "${{ inputs.chart-path }}" - - name: Create release branch and PR + - name: Commit version bump and tag env: GH_TOKEN: ${{ secrets.release-token }} MAKEFILE_PATH: ${{ inputs.makefile-path }} CHART_PATH: ${{ inputs.chart-path }} run: | - BRANCH="release-${{ steps.version.outputs.new }}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add "$MAKEFILE_PATH" + if [ -n "$MAKEFILE_PATH" ]; then git add "$MAKEFILE_PATH"; fi if [ -n "$CHART_PATH" ]; then git add "$CHART_PATH"; fi git commit -m "chore: bump version to ${{ steps.version.outputs.new }}" - git push origin "$BRANCH" - DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" - gh pr create \ - --title "chore: release v${{ steps.version.outputs.new }}" \ - --body "Automated version bump from \`${{ steps.version.outputs.current }}\` to \`${{ steps.version.outputs.new }}\`." \ - --base "$DEFAULT_BRANCH" \ - --head "$BRANCH" + git tag "v${{ steps.version.outputs.new }}" + git push origin "${{ github.event.repository.default_branch }}" --follow-tags - name: Create GitHub release uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 @@ -165,8 +157,7 @@ jobs: name: 'v${{ steps.version.outputs.new }}', generate_release_notes: true, draft: false, - prerelease: false, - target_commitish: context.payload.repository.default_branch + prerelease: false }); - name: Validate dispatch inputs From 843a3a88a2186e53396c391ccb0b2df9dd956a1a Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 00:19:46 +0200 Subject: [PATCH 12/31] fix(workflows): address Copilot review round 5 comments - shared-release: add early validation that makefile-path is non-empty (version computation always requires it); clarify input description - shared-release: switch to annotated tag (git tag -a) so --follow-tags pushes it correctly to the remote - shared-e2e: add explicit permissions: contents: read - shared-go-build: constrain image-name to ghcr.io/ only; add validation that rejects non-GHCR image names when push is true Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 2 ++ .github/workflows/shared-go-build.yaml | 6 +++++- .github/workflows/shared-release.yaml | 11 +++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index a924389..213efe2 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -44,6 +44,8 @@ jobs: e2e: runs-on: ${{ inputs.runs-on }} environment: ${{ inputs.environment != '' && inputs.environment || null }} + permissions: + contents: read steps: - name: Checkout calling repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index 9a08797..ebb421d 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -24,7 +24,7 @@ on: default: false type: boolean image-name: - description: "Full image name including registry, e.g. ghcr.io/cloudoperators/myapp" + description: "Full GHCR image name, e.g. ghcr.io/cloudoperators/myapp (must start with ghcr.io/ when push is true)" required: false default: "" type: string @@ -72,6 +72,10 @@ jobs: echo "ERROR: image-name is required when docker-build is true" exit 1 fi + if [ "${{ inputs.push }}" = "true" ] && [[ "${{ inputs.image-name }}" != ghcr.io/* ]]; then + echo "ERROR: image-name must start with 'ghcr.io/' when push is true (got: '${{ inputs.image-name }}')" + exit 1 + fi - name: Set up QEMU if: inputs.docker-build == true diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index c81d630..1924a86 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -23,7 +23,7 @@ on: default: true type: boolean makefile-path: - description: "Relative path to the Makefile containing the VERSION variable. Leave empty to skip." + description: "Relative path to the Makefile containing the VERSION variable (required for version computation)." required: false default: "Makefile" type: string @@ -72,6 +72,13 @@ jobs: ref: ${{ github.event.repository.default_branch }} fetch-depth: 0 + - name: Validate inputs + run: | + if [ -z "${{ inputs.makefile-path }}" ]; then + echo "ERROR: makefile-path is required for version computation" + exit 1 + fi + - name: Compute new version id: version run: | @@ -142,7 +149,7 @@ jobs: if [ -n "$MAKEFILE_PATH" ]; then git add "$MAKEFILE_PATH"; fi if [ -n "$CHART_PATH" ]; then git add "$CHART_PATH"; fi git commit -m "chore: bump version to ${{ steps.version.outputs.new }}" - git tag "v${{ steps.version.outputs.new }}" + git tag -a "v${{ steps.version.outputs.new }}" -m "Release v${{ steps.version.outputs.new }}" git push origin "${{ github.event.repository.default_branch }}" --follow-tags - name: Create GitHub release From b752974336176eb03a4a5bbe2e887cb06fcb661d Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 00:37:37 +0200 Subject: [PATCH 13/31] fix(workflows): run Greenhouse composite action before checking out caller repo The composite action checks out cloudoperators/greenhouse into the workspace root with clean:true (the default), which deletes any pre-existing caller/ subdirectory. Reorder steps: composite action runs first (greenhouse fills workspace root), then the calling repo is checked out into caller/, then setup-go reads from caller/go.mod, and the test runs from caller/. Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index 213efe2..5e80dde 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -47,11 +47,6 @@ jobs: permissions: contents: read steps: - - name: Checkout calling repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - path: caller - - name: Run Greenhouse E2E composite action uses: cloudoperators/common/workflows/e2e@main with: @@ -60,6 +55,11 @@ jobs: scenario: ${{ inputs.scenario }} ref: ${{ inputs.greenhouse-ref }} + - name: Checkout calling repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + path: caller + - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: From b75e569c0fa30c23c931aff0ff96c320b875463b Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 00:45:05 +0200 Subject: [PATCH 14/31] fix(workflows): address Copilot review round 6 comments - shared-go-test/build/lint: add cache-dependency-path to setup-go so the Go module cache key is correct for subdirectory modules - shared-go-test: pass extra-env via env var + printf to prevent shell expansion of command substitutions in user-supplied env values - shared-go-test/build/e2e: pass make target via env var and quote it to prevent shell metacharacter injection - shared-go-lint: add govulncheck-version input (default: latest) so callers can pin a specific govulncheck version for reproducibility Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 4 +++- .github/workflows/shared-go-build.yaml | 5 ++++- .github/workflows/shared-go-lint.yaml | 8 +++++++- .github/workflows/shared-go-test.yaml | 9 +++++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index 5e80dde..7f569ce 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -68,4 +68,6 @@ jobs: - name: Run e2e tests working-directory: caller/${{ inputs.working-directory }} - run: make ${{ inputs.test-target }} + env: + TEST_TARGET: ${{ inputs.test-target }} + run: make "$TEST_TARGET" diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index ebb421d..0ad4a90 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -61,9 +61,12 @@ jobs: with: go-version-file: ${{ format('{0}/go.mod', inputs.working-directory) }} cache: true + cache-dependency-path: ${{ format('{0}/go.sum', inputs.working-directory) }} - name: Build - run: make ${{ inputs.build-target }} + env: + BUILD_TARGET: ${{ inputs.build-target }} + run: make "$BUILD_TARGET" - name: Validate docker-build inputs if: inputs.docker-build == true diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index ba7801d..cfe60a6 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -23,6 +23,11 @@ on: required: false default: false type: boolean + govulncheck-version: + description: "Version of govulncheck to use (e.g. latest or v1.1.3)" + required: false + default: "latest" + type: string jobs: lint: @@ -41,6 +46,7 @@ jobs: with: go-version-file: ${{ format('{0}/go.mod', inputs.working-directory) }} cache: true + cache-dependency-path: ${{ format('{0}/go.sum', inputs.working-directory) }} - name: Run golangci-lint uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 @@ -50,4 +56,4 @@ jobs: - name: Run govulncheck if: inputs.enable-govulncheck == true - run: go run golang.org/x/vuln/cmd/govulncheck@latest ./... + run: go run golang.org/x/vuln/cmd/govulncheck@${{ inputs.govulncheck-version }} ./... diff --git a/.github/workflows/shared-go-test.yaml b/.github/workflows/shared-go-test.yaml index d792dd9..d86ff96 100644 --- a/.github/workflows/shared-go-test.yaml +++ b/.github/workflows/shared-go-test.yaml @@ -56,17 +56,22 @@ jobs: with: go-version-file: ${{ format('{0}/go.mod', inputs.working-directory) }} cache: true + cache-dependency-path: ${{ format('{0}/go.sum', inputs.working-directory) }} - name: Download Go modules run: go mod download - name: Set extra environment variables if: inputs.extra-env != '' + env: + EXTRA_ENV: ${{ inputs.extra-env }} run: | - echo "${{ inputs.extra-env }}" >> "$GITHUB_ENV" + printf '%s\n' "$EXTRA_ENV" >> "$GITHUB_ENV" - name: Run tests - run: make ${{ inputs.test-target }} + env: + TEST_TARGET: ${{ inputs.test-target }} + run: make "$TEST_TARGET" - name: Upload coverage report if: inputs.upload-coverage == true From 3ee740747b71fc07d2c19c9c4805a84626331a34 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 00:58:32 +0200 Subject: [PATCH 15/31] fix(workflows): address Copilot review round 7 comments - shared-release: validate plugin-name to safe char set (alphanumeric, hyphens, underscores) before use in client-payload JSON to prevent injection via string interpolation - shared-release: add Linux runner guard ($RUNNER_OS check) and update runs-on description to make GNU sed requirement explicit Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 1924a86..d82e4ae 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -4,7 +4,7 @@ on: workflow_call: inputs: runs-on: - description: "The runner to use for the job" + description: "The runner to use for the job (must be a Linux runner; this workflow uses GNU sed)" required: false default: "ubuntu-latest" type: string @@ -74,6 +74,10 @@ jobs: - name: Validate inputs run: | + if [ "$RUNNER_OS" != "Linux" ]; then + echo "ERROR: this workflow requires a Linux runner (uses GNU sed); got: $RUNNER_OS" + exit 1 + fi if [ -z "${{ inputs.makefile-path }}" ]; then echo "ERROR: makefile-path is required for version computation" exit 1 @@ -178,6 +182,10 @@ jobs: echo "ERROR: plugin-name input is required when dispatch-greenhouse-extensions is true" exit 1 fi + if ! echo "${{ inputs.plugin-name }}" | grep -qE '^[a-zA-Z0-9_-]+$'; then + echo "ERROR: plugin-name must contain only alphanumeric characters, hyphens, or underscores (got: '${{ inputs.plugin-name }}')" + exit 1 + fi - name: Generate GitHub App token for dispatch if: inputs.dispatch-greenhouse-extensions == true From cc53b8922525505cb235f55384f5fdec2a523190 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 05:39:25 +0200 Subject: [PATCH 16/31] fix(workflows): address Copilot review round 8 comments - shared-go-build: replace bash-specific [[ ]] glob match with POSIX case statement for ghcr.io/ prefix check; add Linux runner guard when docker-build is enabled Signed-off-by: I313226 --- .github/workflows/shared-go-build.yaml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index 0ad4a90..d3bc9d7 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -71,14 +71,23 @@ jobs: - name: Validate docker-build inputs if: inputs.docker-build == true run: | - if [ -z "${{ inputs.image-name }}" ]; then - echo "ERROR: image-name is required when docker-build is true" + if [ "$RUNNER_OS" != "Linux" ]; then + echo "ERROR: docker-build requires a Linux runner; got: $RUNNER_OS" exit 1 fi - if [ "${{ inputs.push }}" = "true" ] && [[ "${{ inputs.image-name }}" != ghcr.io/* ]]; then - echo "ERROR: image-name must start with 'ghcr.io/' when push is true (got: '${{ inputs.image-name }}')" + if [ -z "${{ inputs.image-name }}" ]; then + echo "ERROR: image-name is required when docker-build is true" exit 1 fi + case "${{ inputs.image-name }}" in + ghcr.io/*) ;; + *) + if [ "${{ inputs.push }}" = "true" ]; then + echo "ERROR: image-name must start with 'ghcr.io/' when push is true (got: '${{ inputs.image-name }}')" + exit 1 + fi + ;; + esac - name: Set up QEMU if: inputs.docker-build == true From 84169adffc11abbec808c6857b53be88e5bc2334 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 07:47:02 +0200 Subject: [PATCH 17/31] fix(workflows): address Copilot review round 9 comments - shared-release: append '|| true' to grep pipeline in command substitution so pipefail does not swallow the explicit parse-error message when VERSION is not found - shared-go-build: add early Validate inputs step that rejects push=true with docker-build=false to prevent unnecessary packages:write token scope being granted without Docker steps running Signed-off-by: I313226 --- .github/workflows/shared-go-build.yaml | 7 +++++++ .github/workflows/shared-release.yaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index d3bc9d7..7f088b2 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -56,6 +56,13 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Validate inputs + run: | + if [ "${{ inputs.push }}" = "true" ] && [ "${{ inputs.docker-build }}" != "true" ]; then + echo "ERROR: push=true requires docker-build=true" + exit 1 + fi + - name: Set up Go uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 with: diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index d82e4ae..44aad9a 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -86,7 +86,7 @@ jobs: - name: Compute new version id: version run: | - CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "${{ inputs.makefile-path }}" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]') + CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "${{ inputs.makefile-path }}" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]' || true) echo "Current version: $CURRENT_VERSION" if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then From 4533eba2944f8540ca3d9544bc062012edcefced Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 08:20:09 +0200 Subject: [PATCH 18/31] fix(workflows): address Copilot review round 10 comments - shared-release: add file existence check for makefile-path in Validate inputs step to fail fast with a clear message instead of a confusing grep error when the path is wrong Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 44aad9a..c26698e 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -82,6 +82,10 @@ jobs: echo "ERROR: makefile-path is required for version computation" exit 1 fi + if [ ! -f "${{ inputs.makefile-path }}" ]; then + echo "ERROR: makefile-path '${{ inputs.makefile-path }}' does not exist" + exit 1 + fi - name: Compute new version id: version From afe7634a9bf74ec80efd9c675f0802b26307e534 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 08:36:55 +0200 Subject: [PATCH 19/31] fix(workflows): address Copilot review round 11 comments - shared-release: add chart-path file existence check in Validate inputs step for consistent early-failure behavior with makefile-path - shared-e2e: set cache-dependency-path to caller go.sum to prevent greenhouse go.sum in workspace root from polluting the cache key Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 1 + .github/workflows/shared-release.yaml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index 7f569ce..4640c2f 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -65,6 +65,7 @@ jobs: with: go-version-file: caller/${{ inputs.working-directory != '.' && format('{0}/go.mod', inputs.working-directory) || 'go.mod' }} cache: true + cache-dependency-path: caller/${{ inputs.working-directory != '.' && format('{0}/go.sum', inputs.working-directory) || 'go.sum' }} - name: Run e2e tests working-directory: caller/${{ inputs.working-directory }} diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index c26698e..f343139 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -86,6 +86,10 @@ jobs: echo "ERROR: makefile-path '${{ inputs.makefile-path }}' does not exist" exit 1 fi + if [ -n "${{ inputs.chart-path }}" ] && [ ! -f "${{ inputs.chart-path }}" ]; then + echo "ERROR: chart-path '${{ inputs.chart-path }}' does not exist" + exit 1 + fi - name: Compute new version id: version From a1bbe7f326f39e5c25dca4ce9225ca819f53942b Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 12:02:09 +0200 Subject: [PATCH 20/31] fix(workflows): add concurrency group to shared-release to prevent races Prevent parallel release runs from racing on VERSION reads, causing non-fast-forward push failures or duplicate tag errors. Uses cancel-in-progress: false so a running release is never aborted. Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index f343139..b58b929 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -62,6 +62,9 @@ jobs: release: runs-on: ${{ inputs.runs-on }} environment: ${{ inputs.environment != '' && inputs.environment || null }} + concurrency: + group: release-${{ github.repository }} + cancel-in-progress: false permissions: contents: write steps: From 0f014795da0e02821b1a3b46df7a8ff9141d91e2 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 12:28:47 +0200 Subject: [PATCH 21/31] fix(workflows): eliminate all inline template expressions in run: steps Move all ${{ inputs.* }}, ${{ steps.*.outputs.* }}, and ${{ secrets.* }} references out of run: shell scripts into env: blocks to prevent potential shell injection and improve security posture across all five workflows: - shared-go-test: validate extra-env lines match KEY=VALUE format before appending to GITHUB_ENV to give callers a clear error on malformed input - shared-go-build: pass push/docker-build and image-name via env vars in both validate steps instead of inline template expressions - shared-go-lint: pass govulncheck-version via env var in run: step - shared-release: pass makefile-path, chart-path, bump, new-version, and plugin-name via env vars in all run: steps; remove all inline expressions from grep/sed/git commands Signed-off-by: I313226 --- .github/workflows/shared-go-build.yaml | 16 +++++-- .github/workflows/shared-go-lint.yaml | 4 +- .github/workflows/shared-go-test.yaml | 7 +++ .github/workflows/shared-release.yaml | 64 ++++++++++++++++++-------- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index 7f088b2..5a255dd 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -57,8 +57,11 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Validate inputs + env: + PUSH: ${{ inputs.push }} + DOCKER_BUILD: ${{ inputs.docker-build }} run: | - if [ "${{ inputs.push }}" = "true" ] && [ "${{ inputs.docker-build }}" != "true" ]; then + if [ "$PUSH" = "true" ] && [ "$DOCKER_BUILD" != "true" ]; then echo "ERROR: push=true requires docker-build=true" exit 1 fi @@ -77,20 +80,23 @@ jobs: - name: Validate docker-build inputs if: inputs.docker-build == true + env: + IMAGE_NAME: ${{ inputs.image-name }} + PUSH: ${{ inputs.push }} run: | if [ "$RUNNER_OS" != "Linux" ]; then echo "ERROR: docker-build requires a Linux runner; got: $RUNNER_OS" exit 1 fi - if [ -z "${{ inputs.image-name }}" ]; then + if [ -z "$IMAGE_NAME" ]; then echo "ERROR: image-name is required when docker-build is true" exit 1 fi - case "${{ inputs.image-name }}" in + case "$IMAGE_NAME" in ghcr.io/*) ;; *) - if [ "${{ inputs.push }}" = "true" ]; then - echo "ERROR: image-name must start with 'ghcr.io/' when push is true (got: '${{ inputs.image-name }}')" + if [ "$PUSH" = "true" ]; then + echo "ERROR: image-name must start with 'ghcr.io/' when push is true (got: '$IMAGE_NAME')" exit 1 fi ;; diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index cfe60a6..b016292 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -56,4 +56,6 @@ jobs: - name: Run govulncheck if: inputs.enable-govulncheck == true - run: go run golang.org/x/vuln/cmd/govulncheck@${{ inputs.govulncheck-version }} ./... + env: + GOVULNCHECK_VERSION: ${{ inputs.govulncheck-version }} + run: go run golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION} ./... diff --git a/.github/workflows/shared-go-test.yaml b/.github/workflows/shared-go-test.yaml index d86ff96..29797d4 100644 --- a/.github/workflows/shared-go-test.yaml +++ b/.github/workflows/shared-go-test.yaml @@ -66,6 +66,13 @@ jobs: env: EXTRA_ENV: ${{ inputs.extra-env }} run: | + while IFS= read -r line; do + [ -z "$line" ] && continue + if ! echo "$line" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*='; then + echo "ERROR: invalid extra-env line (must be KEY=VALUE with a valid identifier key): '$line'" + exit 1 + fi + done <<< "$EXTRA_ENV" printf '%s\n' "$EXTRA_ENV" >> "$GITHUB_ENV" - name: Run tests diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index b58b929..89df9e7 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -76,32 +76,38 @@ jobs: fetch-depth: 0 - name: Validate inputs + env: + MAKEFILE_PATH: ${{ inputs.makefile-path }} + CHART_PATH: ${{ inputs.chart-path }} run: | if [ "$RUNNER_OS" != "Linux" ]; then echo "ERROR: this workflow requires a Linux runner (uses GNU sed); got: $RUNNER_OS" exit 1 fi - if [ -z "${{ inputs.makefile-path }}" ]; then + if [ -z "$MAKEFILE_PATH" ]; then echo "ERROR: makefile-path is required for version computation" exit 1 fi - if [ ! -f "${{ inputs.makefile-path }}" ]; then - echo "ERROR: makefile-path '${{ inputs.makefile-path }}' does not exist" + if [ ! -f "$MAKEFILE_PATH" ]; then + echo "ERROR: makefile-path '$MAKEFILE_PATH' does not exist" exit 1 fi - if [ -n "${{ inputs.chart-path }}" ] && [ ! -f "${{ inputs.chart-path }}" ]; then - echo "ERROR: chart-path '${{ inputs.chart-path }}' does not exist" + if [ -n "$CHART_PATH" ] && [ ! -f "$CHART_PATH" ]; then + echo "ERROR: chart-path '$CHART_PATH' does not exist" exit 1 fi - name: Compute new version id: version + env: + MAKEFILE_PATH: ${{ inputs.makefile-path }} + BUMP: ${{ inputs.bump }} run: | - CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "${{ inputs.makefile-path }}" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]' || true) + CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "$MAKEFILE_PATH" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]' || true) echo "Current version: $CURRENT_VERSION" if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "ERROR: could not parse VERSION from ${{ inputs.makefile-path }} (got: '$CURRENT_VERSION')" + echo "ERROR: could not parse VERSION from $MAKEFILE_PATH (got: '$CURRENT_VERSION')" exit 1 fi @@ -109,7 +115,7 @@ jobs: MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3) - case "${{ inputs.bump }}" in + case "$BUMP" in major) MAJOR=$((MAJOR + 1)) MINOR=0 @@ -123,7 +129,7 @@ jobs: PATCH=$((PATCH + 1)) ;; *) - echo "Invalid bump type: ${{ inputs.bump }}" + echo "Invalid bump type: $BUMP" exit 1 ;; esac @@ -135,37 +141,51 @@ jobs: - name: Update Makefile VERSION if: inputs.makefile-path != '' + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + MAKEFILE_PATH: ${{ inputs.makefile-path }} run: | - sed -i -E "s/^VERSION[[:space:]]*[:?]?=.*/VERSION = ${{ steps.version.outputs.new }}/" "${{ inputs.makefile-path }}" + sed -i -E "s/^VERSION[[:space:]]*[:?]?=.*/VERSION = ${NEW_VERSION}/" "$MAKEFILE_PATH" - name: Update Makefile IMG tag if: inputs.bump-make-version == true && inputs.makefile-path != '' + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + MAKEFILE_PATH: ${{ inputs.makefile-path }} run: | - sed -i -E "s|(IMG[[:space:]]*[:?]?=.*:).*|\1${{ steps.version.outputs.new }}|" "${{ inputs.makefile-path }}" + sed -i -E "s|(IMG[[:space:]]*[:?]?=.*:).*|\1${NEW_VERSION}|" "$MAKEFILE_PATH" - name: Update Chart.yaml version if: inputs.chart-path != '' + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + CHART_PATH: ${{ inputs.chart-path }} run: | - sed -i "s/^version:.*/version: ${{ steps.version.outputs.new }}/" "${{ inputs.chart-path }}" + sed -i "s/^version:.*/version: ${NEW_VERSION}/" "$CHART_PATH" - name: Update Chart.yaml appVersion if: inputs.chart-path != '' && inputs.bump-chart-app-version == true + env: + NEW_VERSION: ${{ steps.version.outputs.new }} + CHART_PATH: ${{ inputs.chart-path }} run: | - sed -i "s/^appVersion:.*/appVersion: \"${{ steps.version.outputs.new }}\"/" "${{ inputs.chart-path }}" + sed -i "s/^appVersion:.*/appVersion: \"${NEW_VERSION}\"/" "$CHART_PATH" - name: Commit version bump and tag env: GH_TOKEN: ${{ secrets.release-token }} MAKEFILE_PATH: ${{ inputs.makefile-path }} CHART_PATH: ${{ inputs.chart-path }} + NEW_VERSION: ${{ steps.version.outputs.new }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" if [ -n "$MAKEFILE_PATH" ]; then git add "$MAKEFILE_PATH"; fi if [ -n "$CHART_PATH" ]; then git add "$CHART_PATH"; fi - git commit -m "chore: bump version to ${{ steps.version.outputs.new }}" - git tag -a "v${{ steps.version.outputs.new }}" -m "Release v${{ steps.version.outputs.new }}" - git push origin "${{ github.event.repository.default_branch }}" --follow-tags + git commit -m "chore: bump version to ${NEW_VERSION}" + git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}" + git push origin "$DEFAULT_BRANCH" --follow-tags - name: Create GitHub release uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 @@ -184,17 +204,21 @@ jobs: - name: Validate dispatch inputs if: inputs.dispatch-greenhouse-extensions == true + env: + PLUGIN_NAME: ${{ inputs.plugin-name }} + DISPATCH_APP_ID: ${{ secrets.dispatch-app-id }} + DISPATCH_APP_KEY: ${{ secrets.dispatch-app-private-key }} run: | - if [ -z "${{ secrets.dispatch-app-id }}" ] || [ -z "${{ secrets.dispatch-app-private-key }}" ]; then + if [ -z "$DISPATCH_APP_ID" ] || [ -z "$DISPATCH_APP_KEY" ]; then echo "ERROR: dispatch-app-id and dispatch-app-private-key secrets are required when dispatch-greenhouse-extensions is true" exit 1 fi - if [ -z "${{ inputs.plugin-name }}" ]; then + if [ -z "$PLUGIN_NAME" ]; then echo "ERROR: plugin-name input is required when dispatch-greenhouse-extensions is true" exit 1 fi - if ! echo "${{ inputs.plugin-name }}" | grep -qE '^[a-zA-Z0-9_-]+$'; then - echo "ERROR: plugin-name must contain only alphanumeric characters, hyphens, or underscores (got: '${{ inputs.plugin-name }}')" + if ! echo "$PLUGIN_NAME" | grep -qE '^[a-zA-Z0-9_-]+$'; then + echo "ERROR: plugin-name must contain only alphanumeric characters, hyphens, or underscores (got: '$PLUGIN_NAME')" exit 1 fi From c5223f6e7e244788772021bb97d095d74e269b53 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 12:41:25 +0200 Subject: [PATCH 22/31] fix(workflows): quote govulncheck module@version argument Wrap the module@version argument in double quotes to prevent word-splitting or globbing if GOVULNCHECK_VERSION contains unexpected characters. Signed-off-by: I313226 --- .github/workflows/shared-go-lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index b016292..1df93ec 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -58,4 +58,4 @@ jobs: if: inputs.enable-govulncheck == true env: GOVULNCHECK_VERSION: ${{ inputs.govulncheck-version }} - run: go run golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION} ./... + run: go run "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" ./... From 776800c64ae91c82f6151768ce86ab7fbe7052e3 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 13:18:57 +0200 Subject: [PATCH 23/31] fix(workflows): add runner OS guard to test and build workflows Add a preflight check that fails fast with a clear error if a Windows runner is used, since both workflows require make which is not available by default on Windows GitHub-hosted runners. Also update runs-on descriptions to document the Linux/macOS requirement. Signed-off-by: I313226 --- .github/workflows/shared-go-build.yaml | 6 +++++- .github/workflows/shared-go-test.yaml | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shared-go-build.yaml b/.github/workflows/shared-go-build.yaml index 5a255dd..dfe6002 100644 --- a/.github/workflows/shared-go-build.yaml +++ b/.github/workflows/shared-go-build.yaml @@ -4,7 +4,7 @@ on: workflow_call: inputs: runs-on: - description: "The runner to use for the job" + description: "The runner to use for the job (must be a Linux or macOS runner; this workflow requires make)" required: false default: "ubuntu-latest" type: string @@ -61,6 +61,10 @@ jobs: PUSH: ${{ inputs.push }} DOCKER_BUILD: ${{ inputs.docker-build }} run: | + case "$RUNNER_OS" in + Linux|macOS) ;; + *) echo "ERROR: this workflow requires a Linux or macOS runner (needs make); got: $RUNNER_OS"; exit 1 ;; + esac if [ "$PUSH" = "true" ] && [ "$DOCKER_BUILD" != "true" ]; then echo "ERROR: push=true requires docker-build=true" exit 1 diff --git a/.github/workflows/shared-go-test.yaml b/.github/workflows/shared-go-test.yaml index 29797d4..72b7854 100644 --- a/.github/workflows/shared-go-test.yaml +++ b/.github/workflows/shared-go-test.yaml @@ -4,7 +4,7 @@ on: workflow_call: inputs: runs-on: - description: "The runner to use for the job" + description: "The runner to use for the job (must be a Linux or macOS runner; this workflow requires make)" required: false default: "ubuntu-latest" type: string @@ -48,6 +48,13 @@ jobs: run: working-directory: ${{ inputs.working-directory }} steps: + - name: Validate runner + run: | + case "$RUNNER_OS" in + Linux|macOS) ;; + *) echo "ERROR: this workflow requires a Linux or macOS runner (needs make); got: $RUNNER_OS"; exit 1 ;; + esac + - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 From fb71994de897c735dedd3384390d28b30da74819 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 13:27:32 +0200 Subject: [PATCH 24/31] fix(workflows): remove unused GH_TOKEN from Commit version bump step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GH_TOKEN was set in the env block of the git commit/tag/push step but never used there — git authenticates via the release-token from checkout and the GitHub release is created in a separate step. Remove it to avoid implying gh CLI is needed and to reduce confusion. Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 89df9e7..60e9882 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -173,7 +173,6 @@ jobs: - name: Commit version bump and tag env: - GH_TOKEN: ${{ secrets.release-token }} MAKEFILE_PATH: ${{ inputs.makefile-path }} CHART_PATH: ${{ inputs.chart-path }} NEW_VERSION: ${{ steps.version.outputs.new }} From 3ead49e222f775a7d07d102a3541cefcd115f1c4 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 14:24:13 +0200 Subject: [PATCH 25/31] fix(workflows): anchor IMG sed to line start; add Linux runner guard to shared-e2e Signed-off-by: I313226 --- .github/workflows/shared-e2e.yaml | 9 ++++++++- .github/workflows/shared-release.yaml | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shared-e2e.yaml b/.github/workflows/shared-e2e.yaml index 4640c2f..27be8bf 100644 --- a/.github/workflows/shared-e2e.yaml +++ b/.github/workflows/shared-e2e.yaml @@ -4,7 +4,7 @@ on: workflow_call: inputs: runs-on: - description: "The runner to use for the job" + description: "The runner to use for the job (must be a Linux runner; this workflow uses KinD and make)" required: false default: "ubuntu-latest" type: string @@ -47,6 +47,13 @@ jobs: permissions: contents: read steps: + - name: Validate runner + run: | + if [ "$RUNNER_OS" != "Linux" ]; then + echo "ERROR: this workflow requires a Linux runner (uses KinD and make); got: $RUNNER_OS" + exit 1 + fi + - name: Run Greenhouse E2E composite action uses: cloudoperators/common/workflows/e2e@main with: diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 60e9882..e1f9b30 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -153,7 +153,7 @@ jobs: NEW_VERSION: ${{ steps.version.outputs.new }} MAKEFILE_PATH: ${{ inputs.makefile-path }} run: | - sed -i -E "s|(IMG[[:space:]]*[:?]?=.*:).*|\1${NEW_VERSION}|" "$MAKEFILE_PATH" + sed -i -E "s|^(IMG[[:space:]]*[:?]?=.*:).*|\1${NEW_VERSION}|" "$MAKEFILE_PATH" - name: Update Chart.yaml version if: inputs.chart-path != '' From 3b5289ea8601c1ee1ae7998b09c554422afb96d6 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 14:40:45 +0200 Subject: [PATCH 26/31] fix(workflows): clarify makefile-path and bump-make-version input descriptions Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index e1f9b30..7b515f9 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -23,12 +23,12 @@ on: default: true type: boolean makefile-path: - description: "Relative path to the Makefile containing the VERSION variable (required for version computation)." + description: "Relative path to the Makefile containing the VERSION variable. Defaults to 'Makefile' in the repository root." required: false default: "Makefile" type: string bump-make-version: - description: "Update the IMG tag in the Makefile to the new version" + description: "Also update the IMG tag in the Makefile to the new version (the VERSION variable is always updated; this controls whether the IMG tag line is also rewritten)" required: false default: false type: boolean From c8b144f7e2699995820e036469cb031a69935213 Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 15:10:22 +0200 Subject: [PATCH 27/31] fix(e2e): replace undefined k8s-version and matrix context with valid inputs.remote-k8s-version Signed-off-by: I313226 --- workflows/e2e/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflows/e2e/action.yaml b/workflows/e2e/action.yaml index f94e08f..9656fcc 100644 --- a/workflows/e2e/action.yaml +++ b/workflows/e2e/action.yaml @@ -104,8 +104,8 @@ runs: GREENHOUSE_ADMIN_KUBECONFIG: ${{ steps.config.outputs.admin_config }} GREENHOUSE_REMOTE_KUBECONFIG: ${{ steps.config.outputs.remote_config }} GREENHOUSE_REMOTE_INT_KUBECONFIG: ${{ steps.config.outputs.remote_int_config }} - CONTROLLER_LOGS_PATH: ${{github.workspace}}/bin/${{inputs.scenario}}-${{inputs.k8s-version}}.txt - E2E_REPORT_PATH: ${{github.workspace}}/bin/${{inputs.scenario}}-${{matrix.k8s-version}}.json + CONTROLLER_LOGS_PATH: ${{github.workspace}}/bin/${{inputs.scenario}}-${{inputs.remote-k8s-version}}.txt + E2E_REPORT_PATH: ${{github.workspace}}/bin/${{inputs.scenario}}-${{inputs.remote-k8s-version}}.json run: | echo "result=$CONTROLLER_LOGS_PATH" >> $GITHUB_OUTPUT make e2e From 09410c1258588c20fb9ed231867158f11d58c4fc Mon Sep 17 00:00:00 2001 From: I313226 Date: Thu, 25 Jun 2026 15:53:43 +0200 Subject: [PATCH 28/31] fix(workflows): support chart-only version computation; makefile-path is now truly optional Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 7b515f9..a46c710 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -23,7 +23,7 @@ on: default: true type: boolean makefile-path: - description: "Relative path to the Makefile containing the VERSION variable. Defaults to 'Makefile' in the repository root." + description: "Relative path to the Makefile containing the VERSION variable. Defaults to 'Makefile'. Leave empty ('') to skip Makefile version parsing and fall back to chart-path version." required: false default: "Makefile" type: string @@ -84,11 +84,11 @@ jobs: echo "ERROR: this workflow requires a Linux runner (uses GNU sed); got: $RUNNER_OS" exit 1 fi - if [ -z "$MAKEFILE_PATH" ]; then - echo "ERROR: makefile-path is required for version computation" + if [ -z "$MAKEFILE_PATH" ] && [ -z "$CHART_PATH" ]; then + echo "ERROR: at least one of makefile-path or chart-path is required for version computation" exit 1 fi - if [ ! -f "$MAKEFILE_PATH" ]; then + if [ -n "$MAKEFILE_PATH" ] && [ ! -f "$MAKEFILE_PATH" ]; then echo "ERROR: makefile-path '$MAKEFILE_PATH' does not exist" exit 1 fi @@ -101,13 +101,23 @@ jobs: id: version env: MAKEFILE_PATH: ${{ inputs.makefile-path }} + CHART_PATH: ${{ inputs.chart-path }} BUMP: ${{ inputs.bump }} run: | - CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "$MAKEFILE_PATH" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]' || true) + CURRENT_VERSION="" + if [ -n "$MAKEFILE_PATH" ]; then + CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "$MAKEFILE_PATH" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]' || true) + fi + if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + if [ -n "$CHART_PATH" ]; then + CURRENT_VERSION=$(grep -E '^version:[[:space:]]*' "$CHART_PATH" | head -1 | sed 's/^version:[[:space:]]*//' | tr -d '[:space:]"' || true) + echo "Falling back to Chart.yaml version: $CURRENT_VERSION" + fi + fi echo "Current version: $CURRENT_VERSION" if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "ERROR: could not parse VERSION from $MAKEFILE_PATH (got: '$CURRENT_VERSION')" + echo "ERROR: could not parse version from makefile-path or chart-path (got: '$CURRENT_VERSION')" exit 1 fi From 2a12e275df165f157ca32667e1a9427567e9d012 Mon Sep 17 00:00:00 2001 From: I313226 Date: Fri, 26 Jun 2026 08:27:50 +0200 Subject: [PATCH 29/31] fix(workflows): validate govulncheck-version format; prefix bump error with ERROR: - shared-go-lint: validate GOVULNCHECK_VERSION against semver/latest pattern before interpolating into go run command to prevent injection - shared-release: prefix invalid bump type message with ERROR: for consistency with other validation failures in the workflow Signed-off-by: I313226 --- .github/workflows/shared-go-lint.yaml | 7 ++++++- .github/workflows/shared-release.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index 1df93ec..6549942 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -58,4 +58,9 @@ jobs: if: inputs.enable-govulncheck == true env: GOVULNCHECK_VERSION: ${{ inputs.govulncheck-version }} - run: go run "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" ./... + run: | + if ! echo "$GOVULNCHECK_VERSION" | grep -qE '^(latest|v[0-9]+\.[0-9]+\.[0-9]+)$'; then + echo "ERROR: govulncheck-version must be 'latest' or a semver tag like v1.2.3 (got: '$GOVULNCHECK_VERSION')" + exit 1 + fi + go run "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" ./... diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index a46c710..d840879 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -139,7 +139,7 @@ jobs: PATCH=$((PATCH + 1)) ;; *) - echo "Invalid bump type: $BUMP" + echo "ERROR: Invalid bump type: $BUMP" exit 1 ;; esac From f42b82c20da52d6f440e147707119d7cfcead065 Mon Sep 17 00:00:00 2001 From: I313226 Date: Fri, 26 Jun 2026 10:43:38 +0200 Subject: [PATCH 30/31] fix(workflows): default makefile-path to empty; add shell: bash to govulncheck step - shared-release: change makefile-path default from "Makefile" to "" so chart-only callers don't need to override it to avoid the file-existence check failing on a non-existent Makefile - shared-go-lint: add shell: bash to govulncheck step to ensure bash builtins and grep work correctly regardless of runner OS default shell Signed-off-by: I313226 --- .github/workflows/shared-go-lint.yaml | 1 + .github/workflows/shared-release.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/shared-go-lint.yaml b/.github/workflows/shared-go-lint.yaml index 6549942..5038bec 100644 --- a/.github/workflows/shared-go-lint.yaml +++ b/.github/workflows/shared-go-lint.yaml @@ -56,6 +56,7 @@ jobs: - name: Run govulncheck if: inputs.enable-govulncheck == true + shell: bash env: GOVULNCHECK_VERSION: ${{ inputs.govulncheck-version }} run: | diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index d840879..9019bd2 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -23,9 +23,9 @@ on: default: true type: boolean makefile-path: - description: "Relative path to the Makefile containing the VERSION variable. Defaults to 'Makefile'. Leave empty ('') to skip Makefile version parsing and fall back to chart-path version." + description: "Relative path to the Makefile containing the VERSION variable (e.g. Makefile). Leave empty ('') to skip Makefile version parsing and fall back to chart-path version." required: false - default: "Makefile" + default: "" type: string bump-make-version: description: "Also update the IMG tag in the Makefile to the new version (the VERSION variable is always updated; this controls whether the IMG tag line is also rewritten)" From dce6e69c3d0053ff3ee45ae2aa610c8c3d0aaf70 Mon Sep 17 00:00:00 2001 From: I313226 Date: Fri, 26 Jun 2026 10:52:03 +0200 Subject: [PATCH 31/31] fix(workflows): strip inline comments from VERSION; preserve operator; fix e2e output path - shared-release: strip inline Makefile comments (e.g. '# comment') from the VERSION value before semver validation to avoid false parse failures - shared-release: preserve the original assignment operator (?=, :=, =) when rewriting VERSION in the Makefile instead of normalising to '=' - e2e/action: output E2E_REPORT_PATH (.json report) instead of CONTROLLER_LOGS_PATH (.txt logs) to match the declared output description Signed-off-by: I313226 --- .github/workflows/shared-release.yaml | 4 ++-- workflows/e2e/action.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/shared-release.yaml b/.github/workflows/shared-release.yaml index 9019bd2..ee8be7a 100644 --- a/.github/workflows/shared-release.yaml +++ b/.github/workflows/shared-release.yaml @@ -106,7 +106,7 @@ jobs: run: | CURRENT_VERSION="" if [ -n "$MAKEFILE_PATH" ]; then - CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "$MAKEFILE_PATH" | head -1 | sed 's/.*=[[:space:]]*//' | tr -d '[:space:]' || true) + CURRENT_VERSION=$(grep -E '^VERSION[[:space:]]*[:?]?=' "$MAKEFILE_PATH" | head -1 | sed 's/.*=[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d '[:space:]' || true) fi if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then if [ -n "$CHART_PATH" ]; then @@ -155,7 +155,7 @@ jobs: NEW_VERSION: ${{ steps.version.outputs.new }} MAKEFILE_PATH: ${{ inputs.makefile-path }} run: | - sed -i -E "s/^VERSION[[:space:]]*[:?]?=.*/VERSION = ${NEW_VERSION}/" "$MAKEFILE_PATH" + sed -i -E "s/^(VERSION[[:space:]]*[:?]?=)[[:space:]]*.*/\1 ${NEW_VERSION}/" "$MAKEFILE_PATH" - name: Update Makefile IMG tag if: inputs.bump-make-version == true && inputs.makefile-path != '' diff --git a/workflows/e2e/action.yaml b/workflows/e2e/action.yaml index 9656fcc..b3e1da4 100644 --- a/workflows/e2e/action.yaml +++ b/workflows/e2e/action.yaml @@ -107,5 +107,5 @@ runs: CONTROLLER_LOGS_PATH: ${{github.workspace}}/bin/${{inputs.scenario}}-${{inputs.remote-k8s-version}}.txt E2E_REPORT_PATH: ${{github.workspace}}/bin/${{inputs.scenario}}-${{inputs.remote-k8s-version}}.json run: | - echo "result=$CONTROLLER_LOGS_PATH" >> $GITHUB_OUTPUT + echo "result=$E2E_REPORT_PATH" >> $GITHUB_OUTPUT make e2e