From bcbfd8ed92415df8a35a1bd088fbc3416ff57d21 Mon Sep 17 00:00:00 2001 From: Torsten Ueberschar Date: Fri, 30 Oct 2026 16:44:50 +0000 Subject: [PATCH] feat: Woodpecker-Plugin bump (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Was - `woodpecker/bump/`: Plugin, das aus Branch und Commit den nächsten Tag ableitet und setzt (dev, beta, hotfix→beta, main mit und ohne Marker, `release_line`), siehe D8 in oumta-deploy. - `.woodpecker/plugins.yaml` baut jetzt beide Plugins mit Pfadfilter je Plugin; Release über Tag `bump-vX.Y.Z` bzw. `deploy-vX.Y.Z`. - Nachträge zum deploy-Plugin aus #1: `plugin-docker-buildx:6` exakt wie in `WOODPECKER_PLUGINS_PRIVILEGED` (war nach dem Merge noch `6.1.2`), Registry-Login mit `oumta-ci` und Secret `oumta_ci_token` statt der globalen Secrets, README-Beispiele entsprechend (zugaenge.md in oumta-deploy). ## Voraussetzungen vor dem Tag - Secret `oumta_ci_token` in Woodpecker für `actions/ci-cd`, freigegeben für `woodpeckerci/plugin-docker-buildx:6`, Events `push`, `tag`, `manual`. - `oumta-ci` mit Paket-Schreibrecht in der Org `actions`. ## Getestet Lokal gegen ein Bare-Repo: dev, beta, hotfix→beta, main mit und ohne Marker, `release_line`, weitergelaufener Branch, fehlendes Token. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://git.uesome.de/actions/ci-cd/pulls/2 --- .woodpecker/plugins.yaml | 41 +++++-- README.md | 52 +++++++-- woodpecker/bump/Dockerfile | 5 + woodpecker/bump/bump.sh | 212 +++++++++++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+), 17 deletions(-) create mode 100644 woodpecker/bump/Dockerfile create mode 100755 woodpecker/bump/bump.sh diff --git a/.woodpecker/plugins.yaml b/.woodpecker/plugins.yaml index 9466d84..0d4d22c 100644 --- a/.woodpecker/plugins.yaml +++ b/.woodpecker/plugins.yaml @@ -2,13 +2,13 @@ # git.uesome.de/actions/ci-cd/:vX.Y.Z; pushes and PRs only test the build. when: - event: [push, pull_request] - path: 'woodpecker/deploy/**' + path: 'woodpecker/**' - event: tag - ref: refs/tags/deploy-v* + ref: [refs/tags/deploy-v*, refs/tags/bump-v*] steps: - name: deploy-plugin-check - image: woodpeckerci/plugin-docker-buildx:6.1.2 + image: woodpeckerci/plugin-docker-buildx:6 settings: repo: git.uesome.de/actions/ci-cd/deploy context: woodpecker/deploy @@ -16,18 +16,45 @@ steps: dry_run: true when: event: [push, pull_request] + path: 'woodpecker/deploy/**' - name: deploy-plugin - image: woodpeckerci/plugin-docker-buildx:6.1.2 + image: woodpeckerci/plugin-docker-buildx:6 settings: registry: git.uesome.de repo: git.uesome.de/actions/ci-cd/deploy context: woodpecker/deploy dockerfile: woodpecker/deploy/Dockerfile tags: ${CI_COMMIT_TAG##deploy-} - username: - from_secret: registry_username + username: oumta-ci password: - from_secret: registry_password + from_secret: oumta_ci_token when: event: tag + ref: refs/tags/deploy-v* + + - name: bump-plugin-check + image: woodpeckerci/plugin-docker-buildx:6 + settings: + repo: git.uesome.de/actions/ci-cd/bump + context: woodpecker/bump + dockerfile: woodpecker/bump/Dockerfile + dry_run: true + when: + event: [push, pull_request] + path: 'woodpecker/bump/**' + + - name: bump-plugin + image: woodpeckerci/plugin-docker-buildx:6 + settings: + registry: git.uesome.de + repo: git.uesome.de/actions/ci-cd/bump + context: woodpecker/bump + dockerfile: woodpecker/bump/Dockerfile + tags: ${CI_COMMIT_TAG##bump-} + username: oumta-ci + password: + from_secret: oumta_ci_token + when: + event: tag + ref: refs/tags/bump-v* diff --git a/README.md b/README.md index 1196c64..4a0c13b 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ Writes a pushed dev or beta image with its digest to `tanztee/oumta-deploy` (D16 Settings: - `var` (required): variable in `envs//images.env`, e.g. `AUTH_IMAGE`. `{major}` is replaced by the major of the tag (`APP_V{major}_IMAGE` -> `APP_V2_IMAGE`). -- `deploy_token` (required): token of `oumta-ci`, write access to oumta-deploy only. Limit the secret to this image and the events `tag` and `manual`. -- `registry_username`, `registry_password`: read access to the registry for the digest +- `deploy_token` (required): token of `oumta-ci` (`oumta_ci_token`), write access to oumta-deploy +- `registry_username`, `registry_password`: read access to the registry for the digest (`oumta-ci` with the same token) - `envs`: fixed environments instead of the tag rule, e.g. `dev beta` (oumta-keyklo) - `tag`: default `CI_COMMIT_TAG` - `registry`: default `git.uesome.de` @@ -147,24 +147,56 @@ when: steps: - name: build - image: woodpeckerci/plugin-docker-buildx:6.1.2 + image: woodpeckerci/plugin-docker-buildx:6 settings: registry: git.uesome.de repo: git.uesome.de/tanztee/oumta-auth tags: ${CI_COMMIT_TAG} - username: - from_secret: registry_username + username: oumta-ci password: - from_secret: registry_password + from_secret: oumta_ci_token - name: deploy image: git.uesome.de/actions/ci-cd/deploy:v1.0.0 settings: var: AUTH_IMAGE - registry_username: - from_secret: registry_username + registry_username: oumta-ci registry_password: - from_secret: registry_password + from_secret: oumta_ci_token deploy_token: - from_secret: oumta_deploy_token + from_secret: oumta_ci_token +``` + +### bump + +Sets the version for the pushed branch in `package.json`, commits it as `update version: vX [skip ci]`, tags it and pushes branch and tag. The tag event then starts the build; `[skip ci]` only suppresses push and pull_request events. + +- `main`: release from the commit message (`release/x.y[.z]`, `hotfix/x.y.z`), otherwise a patch bump +- `release/*`, `hotfix/*`: next `vX.Y.Z-beta.N` +- any other branch: `vX.Y.Z-dev.`; the release line comes from `release_line`, the newest ancestor `release/*` branch, or `package.json` + +Only the commit that triggered the pipeline is bumped; if the branch moved on, the step exits without a bump and the newer pipeline takes over. + +Settings: + +- `git_token` (required): token with write access to the repo (`oumta_ci_token`) +- `git_user`: default `oumta-ci` +- `release_line`: optional, `.[.]` +- `author_name`, `author_email`: default `oumta-ci`, `oumta-ci@noreply.git.uesome.de` +- `remote`: default `CI_REPO_CLONE_URL` + +Example: + +```yaml +when: + - event: push + branch: [develop, 'feature/**', 'release/**', 'hotfix/**'] + - event: manual + +steps: + - name: bump + image: git.uesome.de/actions/ci-cd/bump:v1.0.0 + settings: + git_token: + from_secret: oumta_ci_token ``` diff --git a/woodpecker/bump/Dockerfile b/woodpecker/bump/Dockerfile new file mode 100644 index 0000000..3a0714a --- /dev/null +++ b/woodpecker/bump/Dockerfile @@ -0,0 +1,5 @@ +# Woodpecker plugin: bumps the version in package.json, tags and pushes +FROM node:24-alpine +RUN apk add --no-cache bash git ca-certificates +COPY bump.sh /usr/local/bin/bump.sh +ENTRYPOINT ["/usr/local/bin/bump.sh"] diff --git a/woodpecker/bump/bump.sh b/woodpecker/bump/bump.sh new file mode 100755 index 0000000..0063d9c --- /dev/null +++ b/woodpecker/bump/bump.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# Woodpecker plugin "bump": sets the version for the pushed branch, commits it +# with [skip ci], tags it and pushes both. The tag event then starts the build; +# [skip ci] only suppresses push and pull_request events, not tag events. +# +# main release from the commit message (release/x.y[.z], +# hotfix/x.y.z), otherwise a patch bump +# release/*, hotfix/* next vX.Y.Z-beta.N +# any other branch vX.Y.Z-dev. +set -euo pipefail + +die() { echo "bump: $*" >&2; exit 1; } + +BRANCH=${CI_COMMIT_BRANCH:?CI_COMMIT_BRANCH is missing (push event only)} +SHA=${CI_COMMIT_SHA:?CI_COMMIT_SHA is missing} +REMOTE=${PLUGIN_REMOTE:-${CI_REPO_CLONE_URL:?CI_REPO_CLONE_URL is missing}} +GIT_USER=${PLUGIN_GIT_USER:-oumta-ci} +[[ -n ${PLUGIN_GIT_TOKEN:-} ]] || die "settings.git_token is missing" +INPUT_RELEASE_LINE=${PLUGIN_RELEASE_LINE:-} + +export GIT_AUTHOR_NAME=${PLUGIN_AUTHOR_NAME:-oumta-ci} GIT_AUTHOR_EMAIL=${PLUGIN_AUTHOR_EMAIL:-oumta-ci@noreply.git.uesome.de} +export GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL + +# Token only via credential helper, never in a URL or config, so no log shows it +export GIT_USER PLUGIN_GIT_TOKEN +git config --global --add safe.directory "$PWD" +git config credential.helper '!f() { echo "username=$GIT_USER"; echo "password=$PLUGIN_GIT_TOKEN"; }; f' +git remote set-url origin "$REMOTE" + +# The Woodpecker clone is shallow and has no tags; the version rules need both. +if [[ $(git rev-parse --is-shallow-repository) == true ]]; then + git fetch --quiet --unshallow origin +fi +git fetch --quiet --tags --force origin "+refs/heads/*:refs/remotes/origin/*" + +# Bump exactly the commit that triggered the pipeline. A newer push on the +# branch starts its own pipeline, so this one steps aside. +if [[ $(git rev-parse "origin/$BRANCH") != "$SHA" ]]; then + echo "bump: $BRANCH moved past ${SHA:0:8}, the newer pipeline bumps" + exit 0 +fi +git checkout --quiet -B "$BRANCH" "$SHA" + +set_version() { + npm version "$1" --no-git-tag-version --allow-same-version >/dev/null +} + +read_package_version() { + node -p "require('./package.json').version" +} + +normalize_release_base() { + local value="$1" + if [[ "${value}" =~ ^v?([0-9]+)\.([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0" + return 0 + fi + if [[ "${value}" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" + return 0 + fi + return 1 +} + +extract_release_base() { + local ref_name="$1" + local normalized + if [[ "${ref_name}" =~ ^(release|hotfix)/(.+)$ ]]; then + if normalized="$(normalize_release_base "${BASH_REMATCH[2]}")"; then + echo "${normalized}" + return 0 + fi + fi + return 1 +} + +# Only release branches: a fresh hotfix branch is an ancestor of everything. +infer_release_base_from_ancestor_release_branch() { + local best_release_base="" + local best_commit_time=0 + local remote_ref + local release_base + local commit_time + + while IFS= read -r remote_ref; do + if ! release_base="$(extract_release_base "${remote_ref#origin/}")"; then + continue + fi + if ! git merge-base --is-ancestor "$(git rev-parse "${remote_ref}")" HEAD; then + continue + fi + commit_time="$(git show -s --format=%ct "${remote_ref}")" + if [[ "${commit_time}" -gt "${best_commit_time}" ]]; then + best_commit_time="${commit_time}" + best_release_base="${release_base}" + fi + done < <(git for-each-ref --format='%(refname:short)' refs/remotes/origin/release/) + + [[ -n "${best_release_base}" ]] && echo "${best_release_base}" +} + +resolve_release_base() { + local package_version + local normalized + + if [[ -n "${INPUT_RELEASE_LINE}" ]]; then + normalized="$(normalize_release_base "${INPUT_RELEASE_LINE}")" || + die "invalid settings.release_line '${INPUT_RELEASE_LINE}', expected .[.]" + echo "${normalized}" + return 0 + fi + if extract_release_base "${BRANCH}"; then + return 0 + fi + if infer_release_base_from_ancestor_release_branch; then + return 0 + fi + package_version="$(read_package_version)" + if [[ "${package_version}" =~ ^([0-9]+)\.([0-9]+)\.[0-9]+([-.].*)?$ ]]; then + echo "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0" + return 0 + fi + die "no release base for '${BRANCH}': use release/.[.], settings.release_line or a semantic version in package.json" +} + +next_beta_version() { + local release_base="$1" + local escaped_base + local highest_beta + escaped_base="$(printf '%s' "${release_base}" | sed 's/\./\\./g')" + highest_beta="$( + git tag --list "v${release_base}-beta.*" \ + | sed -nE "s/^v${escaped_base}-beta\.([0-9]+)$/\\1/p" \ + | sort -n \ + | tail -n 1 + )" + if [[ -z "${highest_beta}" ]]; then + echo "${release_base}-beta.0" + else + echo "${release_base}-beta.$((highest_beta + 1))" + fi +} + +next_dev_version() { + local release_base="$1" + local timestamp + while true; do + timestamp="$(date -u +%Y%m%d%H%M%S)" + if ! git rev-parse -q --verify "refs/tags/v${release_base}-dev.${timestamp}" >/dev/null; then + echo "${release_base}-dev.${timestamp}" + return 0 + fi + sleep 1 + done +} + +next_version() { + case "${BRANCH}" in + main) npm version patch --no-git-tag-version >/dev/null ;; + release/* | hotfix/*) set_version "$(next_beta_version "${RELEASE_BASE}")" ;; + *) set_version "$(next_dev_version "${RELEASE_BASE}")" ;; + esac +} + +case "${BRANCH}" in + main) + COMMIT_MSG="$(git log -1 --pretty=%B)" + TARGET_VERSION="" + if [[ "${COMMIT_MSG}" =~ release/v?([0-9]+\.[0-9]+\.[0-9]+) ]]; then + TARGET_VERSION="${BASH_REMATCH[1]}" + elif [[ "${COMMIT_MSG}" =~ release/([0-9]+)\.([0-9]+) ]]; then + TARGET_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0" + elif [[ "${COMMIT_MSG}" =~ hotfix/([0-9]+\.[0-9]+\.[0-9]+) ]]; then + TARGET_VERSION="${BASH_REMATCH[1]}" + fi + if [[ -n "${TARGET_VERSION}" ]]; then + set_version "${TARGET_VERSION}" + else + next_version + fi + ;; + *) + RELEASE_BASE="$(resolve_release_base)" + next_version + ;; +esac + +VERSION="$(read_package_version)" +while git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; do + echo "bump: tag v${VERSION} already exists, trying the next version" + next_version + VERSION="$(read_package_version)" +done + +TAG="v${VERSION}" +if git diff --quiet; then + echo "bump: no version file changes, tagging the current commit" +else + git add package.json + [[ -f package-lock.json ]] && git add package-lock.json + git commit --quiet -m "update version: ${TAG} [skip ci]" +fi +git tag "${TAG}" + +# Branch first, then the tag, so the tag never points at an unpushed commit. +# If the branch moved meanwhile, the push is rejected and nothing is tagged. +if ! git push --quiet origin "HEAD:refs/heads/${BRANCH}"; then + echo "bump: push to ${BRANCH} rejected, the newer pipeline bumps" + exit 0 +fi +git push --quiet origin "refs/tags/${TAG}" +echo "bump: ${TAG} on ${BRANCH}"