Skip to content
83 changes: 83 additions & 0 deletions .github/workflows/manual-patch-release.yaml
Original file line number Diff line number Diff line change
@@ -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/<name>/v* semver tag
# are skipped with a notice.
# - Resolves the latest functions/go/<name>/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
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion functions/go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
165 changes: 165 additions & 0 deletions scripts/manual-patch-release.sh
Original file line number Diff line number Diff line change
@@ -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).
Comment thread
mozesl-nokia marked this conversation as resolved.
#
# 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/<name>/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
Loading