diff --git a/.github/workflows/manual-patch-release.yaml b/.github/workflows/manual-patch-release.yaml new file mode 100644 index 000000000..ede24d8ab --- /dev/null +++ b/.github/workflows/manual-patch-release.yaml @@ -0,0 +1,83 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Manual patch release for functions/go KRM functions. +# +# - Input: comma-separated function names, or "all" (case-insensitive) for every name from +# `make list-functions`. With "all", functions without a prior functions/go//v* semver tag +# are skipped with a notice. +# - Resolves the latest functions/go//vMAJOR.MINOR.PATCH tag, bumps patch only. +# - Creates a GitHub Release (and tag at github.sha) via `gh` using a GitHub App installation +# access token (not GITHUB_TOKEN), so tag push events start other workflows. +# - Image build/push, short tag, and appending the container image line to release notes are handled +# by workflows triggered on the new tag (e.g. after-tag-with-version.yaml, release.yaml). +# +# GitHub App setup (org or user app, installed on this repository): +# 1. Create a GitHub App with Repository permissions: Contents (Read and write), Metadata (Read). +# 2. Install the app on this repository (Only select repositories). +# 3. Repository secret: CI_BOT_APP_ID = App ID (numeric, from app settings). +# 4. Repository secret: CI_BOT_PRIVATE_KEY = App private key (.pem contents). + +name: Manual patch release (functions/go) + +on: + workflow_dispatch: + inputs: + functions: + description: >- + Comma-separated function names (e.g. kubeconform,apply-setters), or "all" to patch-release + every function from `make list-functions`. Functions with no prior v* semver tag + are skipped with a notice when using "all". + required: true + type: string + dry_run: + description: "If true, only print planned versions (no releases)" + required: false + type: boolean + default: false + +concurrency: + group: manual-patch-release-catalog + cancel-in-progress: false + +permissions: + contents: read + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Generate GitHub App token + if: ${{ !inputs.dry_run }} + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.CI_BOT_APP_ID }} + private-key: ${{ secrets.CI_BOT_PRIVATE_KEY }} + permission-contents: write + + - name: Patch release (GitHub release only) + env: + MANUAL_PATCH_FUNCTIONS: ${{ inputs.functions }} + MANUAL_PATCH_DRY_RUN: ${{ inputs.dry_run }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SHA: ${{ github.sha }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: bash scripts/manual-patch-release.sh diff --git a/Makefile b/Makefile index 6d4af1b96..c3b1aed8a 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,10 @@ NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) help: ## Print this help @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -.PHONY: test unit-test e2e-test push +.PHONY: test unit-test e2e-test push list-functions + +list-functions: ## Print Go KRM function names, one per line + $(MAKE) -C functions/go list-functions unit-test: ## Run unit tests for Go functions cd functions/go && $(MAKE) test -j$(NPROC) @@ -35,7 +38,7 @@ test: unit-test e2e-test ## Run all unit tests and e2e tests # find all subdirectories with a go.mod file in them GO_MOD_DIRS = $(shell find . -name 'go.mod' -not -path './documentation/*' -exec sh -c 'echo "$$(dirname "{}")"' \; ) # NOTE: the above line is complicated for Mac and busybox compatibilty reasons. -# It is meant to be equivalent with this: find . -name 'go.mod' -printf "'%h' " +# It is meant to be equivalent with this: find . -name 'go.mod' -printf "'%h' " .PHONY: tidy tidy: diff --git a/functions/go/Makefile b/functions/go/Makefile index 4fbfedcd4..e38854181 100644 --- a/functions/go/Makefile +++ b/functions/go/Makefile @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. SHELL=/bin/bash +GOPATH ?= $(shell go env GOPATH || echo $(HOME)/go) TAG ?= latest DEFAULT_CR ?= ghcr.io/kptdev/krm-functions-catalog -GOBIN := $(shell go env GOPATH)/bin +GOBIN ?= $(GOPATH)/bin GOLANGCI_LINT_VERSION ?= 2.12.2 @@ -48,6 +49,10 @@ FUNCTIONS := \ upsert-resource \ sleep +.PHONY: list-functions +list-functions: ## Print Go KRM function names (FUNCTIONS), one per line + @printf '%s\n' $(FUNCTIONS) + # Targets for running all function tests FUNCTION_TESTS := $(patsubst %,%-TEST,$(FUNCTIONS)) # Targets for generating all functions docs diff --git a/scripts/manual-patch-release.sh b/scripts/manual-patch-release.sh new file mode 100644 index 000000000..99c91bbcc --- /dev/null +++ b/scripts/manual-patch-release.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash + +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Manual patch release for functions/go KRM functions (used by GitHub Actions and runnable locally). +# +# Creates a GitHub Release (and the semver tag on the server at GITHUB_SHA) via `gh`. Image build, +# short tag creation, and appending the container image to release notes are left to workflows +# that run on tag push (e.g. after-tag-with-version.yaml, release.yaml). Use a GitHub App +# installation access token or PAT for GH_TOKEN in CI so those workflows are triggered; +# GITHUB_TOKEN-created tag/release events do not start them. +# +# Environment (required unless noted): +# MANUAL_PATCH_FUNCTIONS Comma-separated function names (must match output of: make list-functions), +# or the single word "all" (case-insensitive) to release every name from +# `make list-functions`. With "all", functions with no prior +# functions/go//vMAJOR.MINOR.PATCH tag are skipped with a notice. +# GITHUB_REPOSITORY owner/repo (e.g. kptdev/krm-functions-catalog). +# GITHUB_SHA Commit SHA for the new tag and release target. +# GH_TOKEN GitHub App installation token or PAT for gh release create — required +# when not in dry-run; must allow creating releases and tags on the repo. +# +# Optional: +# MANUAL_PATCH_DRY_RUN If "true", only print planned versions (no gh calls). +# +# Prerequisites: gh, git; remote `origin` must exist for non dry-run. + +set -euo pipefail + +scripts_dir="$(cd "$(dirname "$0")" && pwd)" +repo_root="$(cd "${scripts_dir}/.." && pwd)" +cd "${repo_root}" + +functions_input="${MANUAL_PATCH_FUNCTIONS:-}" +repo="${GITHUB_REPOSITORY:-}" +sha="${GITHUB_SHA:-}" +dry_run="${MANUAL_PATCH_DRY_RUN:-false}" + +if [[ -z "$functions_input" ]]; then + echo "::error::MANUAL_PATCH_FUNCTIONS is not set" >&2 + exit 1 +fi +if [[ -z "$repo" ]]; then + echo "::error::GITHUB_REPOSITORY is not set" >&2 + exit 1 +fi +if [[ -z "$sha" ]]; then + echo "::error::GITHUB_SHA is not set" >&2 + exit 1 +fi + +if [[ "$dry_run" != "true" ]] && [[ -z "${GH_TOKEN:-}" ]]; then + echo "::error::GH_TOKEN is required when not in dry-run (GitHub App installation token or PAT with permission to create releases and tags)" >&2 + exit 1 +fi + +# Same names as FUNCTIONS in functions/go/Makefile (see target list-functions). +mapfile -t allowed < <(make -s list-functions) + +if [[ ${#allowed[@]} -eq 0 ]]; then + echo "::error::make list-functions produced no output" >&2 + exit 1 +fi + +_trim_space() { + local s="$1" + s="${s#"${s%%[![:space:]]*}"}" + s="${s%"${s##*[![:space:]]}"}" + printf '%s' "$s" +} + +is_allowed() { + local n="$1" + for a in "${allowed[@]}"; do + [[ "$n" == "$a" ]] && return 0 + done + return 1 +} + +outer_trim="$(_trim_space "${functions_input}")" +declare -a functions=() +expand_all=0 + +# Bulk mode: every function from `make list-functions`. +if [[ "${outer_trim,,}" == "all" ]]; then + expand_all=1 + functions=("${allowed[@]}") +else + # Explicit list: comma-separated names (strict if missing tags). + IFS=',' read -ra raw_parts <<< "${functions_input}" + for part in "${raw_parts[@]}"; do + fn="$(_trim_space "$part")" + [[ -z "$fn" ]] && continue + if ! is_allowed "$fn"; then + echo "::error::Function '$fn' is not in output of: make list-functions" >&2 + exit 1 + fi + functions+=("$fn") + done +fi + +mapfile -t functions < <(printf '%s\n' "${functions[@]}" | sort -u) + +if [[ ${#functions[@]} -eq 0 ]]; then + echo "::error::No function names after parsing MANUAL_PATCH_FUNCTIONS" >&2 + exit 1 +fi + +for fn in "${functions[@]}"; do + echo "===== ${fn} =====" + + # Latest strict SemVer long tag for this function (sort -V orders versions correctly). + prev_long="$( + git tag -l "functions/go/${fn}/v*" --sort=v:refname | + grep -E -- '/v[0-9]+\.[0-9]+\.[0-9]+$' | + tail -n 1 || true + )" + + if [[ -z "${prev_long}" ]]; then + if [[ "${expand_all}" -eq 1 ]]; then + echo "::notice::Skipping ${fn}: no prior semver tag functions/go/${fn}/vMAJOR.MINOR.PATCH" + continue + fi + echo "::error::No prior semver tag functions/go/${fn}/vMAJOR.MINOR.PATCH; cannot infer next patch." >&2 + exit 1 + fi + + ver="${prev_long##*/}" + v="${ver#v}" + IFS=. read -r major minor patch <<< "${v}" + next_ver="v${major}.${minor}.$((patch + 1))" + long_tag="functions/go/${fn}/${next_ver}" + release_title="${fn} ${next_ver}" + + echo "Previous: ${prev_long} -> Next: ${long_tag}" + + if [[ "$dry_run" == "true" ]]; then + echo "(dry_run) would run: gh release create \"${long_tag}\" --repo \"${repo}\" --target \"${sha}\" --title \"${release_title}\" --generate-notes --notes-start-tag \"${prev_long}\"" + continue + fi + + if [[ -n "$(git ls-remote origin "refs/tags/${long_tag}")" ]]; then + echo "::error::Tag ${long_tag} already exists on origin" >&2 + exit 1 + fi + + gh release create "${long_tag}" \ + --repo "${repo}" \ + --target "${sha}" \ + --title "${release_title}" \ + --generate-notes \ + --notes-start-tag "${prev_long}" +done