From fa391c59262ae5b8f6d6b575d28efb5e74af7450 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:04 +0200 Subject: [PATCH 01/22] docs: fill in community and governance documents Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- CODE_OF_CONDUCT.md | 2 +- GOVERNANCE.md | 5 ++++- MAINTAINERS.md | 4 ++-- SECURITY.md | 12 ++++++------ 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6a887e0..9c10fe9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,11 +1,11 @@ name: Bug Report -description: Report a bug in {{PROJECT_NAME}} +description: Report a bug in updater-python labels: ["type: bug", "needs triage"] body: - type: markdown attributes: value: | - Thank you for reporting a bug. For security vulnerabilities, use private reporting via {{SECURITY_ADVISORY_URL}}. + Thank you for reporting a bug. For security vulnerabilities, use private reporting via https://github.com/SemRels/updater-python/security/advisories/new. - type: input id: version attributes: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e833251..9a09b70 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,6 +1,6 @@ # Code of Conduct -updater-python is committed to providing a welcoming and harassment-free community. +`updater-python` is committed to providing a welcoming and harassment-free community. ## Our Pledge diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e11f666..89aa3cb 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -2,17 +2,20 @@ ## Overview -{{PROJECT_NAME}} is an open source project governed by its maintainers and community contributors. This document describes how decisions are made and how to participate. +`updater-python` is an open source project governed by its maintainers and community contributors. This document describes how decisions are made and how to participate. ## Project Roles ### Contributor + Anyone who opens an issue, submits a PR, improves documentation, or participates in discussions. No formal process is required beyond following [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). ### Reviewer + An experienced contributor trusted to review changes in a specific area. Reviewers can approve changes but should not merge without maintainer approval unless the repo explicitly grants that permission. ### Maintainer + Maintainers have merge rights and are responsible for the overall health of the repository. The current list of maintainers is in [MAINTAINERS.md](MAINTAINERS.md). **To become a Maintainer:** diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 94dc0c4..b0ace2b 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -4,8 +4,8 @@ | Name | GitHub | Affiliation | Areas | | --- | --- | --- | --- | -| {{MAINTAINER_NAME_1}} | [@{{MAINTAINER_GITHUB_1}}](https://github.com/{{MAINTAINER_GITHUB_1}}) | {{AFFILIATION_1}} | {{AREAS_1}} | -| {{MAINTAINER_NAME_2}} | [@{{MAINTAINER_GITHUB_2}}](https://github.com/{{MAINTAINER_GITHUB_2}}) | {{AFFILIATION_2}} | {{AREAS_2}} | +| Markus Waldheim | [@mwaldheim](https://github.com/mwaldheim) | Independent | Core, Plugin Architecture, CI | +| Thomas Boerger | [@tboerger](https://github.com/tboerger) | Independent | Core, Plugin Architecture, Gitea Integration | ## Responsibilities diff --git a/SECURITY.md b/SECURITY.md index 4632a3c..b4e0ee5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,17 +4,17 @@ | Version | Supported | | --- | --- | -| `main` / `{{DEFAULT_BRANCH}}` | ? | +| `main` (pre-release) | ✅ | -Update this table once stable releases exist. +Once stable releases are published this table will list supported version ranges. ## Reporting a Vulnerability -**Do not open a public issue for security vulnerabilities.** +**Please do not open a public GitHub Issue for security vulnerabilities.** -Please report vulnerabilities through **[GitHub Security Advisories]({{SECURITY_ADVISORY_URL}})**. +Report security issues privately via **[GitHub Security Advisories](https://github.com/SemRels/updater-python/security/advisories/new)**. -Alternative contact: {{SECURITY_CONTACT}} +You can also reach the maintainers at the addresses listed in [MAINTAINERS.md](MAINTAINERS.md). ## What To Include @@ -23,7 +23,7 @@ Alternative contact: {{SECURITY_CONTACT}} - Affected versions, tags, or commits - Proposed mitigations or workarounds, if known -## Response Targets +## Response SLA | Action | Target | | --- | --- | From 03e8ae5f632289be8457eaea04b41956fe5244e8 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:04 +0200 Subject: [PATCH 02/22] feat: replace Makefile with Taskfile Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- Makefile | 35 ------------ Taskfile.yaml | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 35 deletions(-) delete mode 100644 Makefile create mode 100644 Taskfile.yaml diff --git a/Makefile b/Makefile deleted file mode 100644 index 5b36335..0000000 --- a/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2026 The plugin-template Authors - -.PHONY: build test lint coverage release build-all-platforms clean - -PLUGIN_NAME ?= plugin -DIST_DIR ?= dist - -build: - mkdir -p bin - go build -o bin/$(PLUGIN_NAME) ./cmd/plugin - -test: - go test -v ./... - -lint: - golangci-lint run - -coverage: - go test -cover ./... - -build-all-platforms: - mkdir -p $(DIST_DIR) - GOOS=linux GOARCH=amd64 go build -o $(DIST_DIR)/$(PLUGIN_NAME)-linux-amd64 ./cmd/plugin - GOOS=linux GOARCH=arm64 go build -o $(DIST_DIR)/$(PLUGIN_NAME)-linux-arm64 ./cmd/plugin - GOOS=darwin GOARCH=amd64 go build -o $(DIST_DIR)/$(PLUGIN_NAME)-darwin-amd64 ./cmd/plugin - GOOS=darwin GOARCH=arm64 go build -o $(DIST_DIR)/$(PLUGIN_NAME)-darwin-arm64 ./cmd/plugin - GOOS=windows GOARCH=amd64 go build -o $(DIST_DIR)/$(PLUGIN_NAME)-windows-amd64.exe ./cmd/plugin - GOOS=windows GOARCH=arm64 go build -o $(DIST_DIR)/$(PLUGIN_NAME)-windows-arm64.exe ./cmd/plugin - -release: - @echo "Build all release artifacts locally with 'make build-all-platforms' and push a v*.*.* tag to trigger .github/workflows/release.yml" - -clean: - go clean diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..c38e2ac --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,143 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://taskfile.dev/schema.json +version: "3.40" + +tasks: + clean: + desc: Remove all temporary build artifacts + cmds: + - go clean -i ./... + - rm -rf dist/ bin/ coverage.out + + fmt: + desc: Run standard formatter + vars: + SOURCES: + sh: find . -name "*.go" -type f | xargs echo + cmds: + - gofmt -s -w {{ .SOURCES }} + + vet: + desc: Run vet linting + cmds: + - go vet ./... + + lint: + desc: Run golangci linter + cmds: + - golangci-lint run + + test: + desc: Run unit tests + vars: + PACKAGES: + sh: go list ./... | xargs echo + cmds: + - go test -v -coverprofile=coverage.out ./... + + e2e: + desc: Run end-to-end tests + cmds: + - go test -tags e2e -v ./cmd/plugin/... + + build: + desc: Build the plugin for the current platform + cmds: + - go build -v -trimpath -ldflags="-s -w" -o bin/plugin{{if eq .GOOS "windows"}}.exe{{end}} ./cmd/plugin + env: + CGO_ENABLED: "0" + GOOS: "{{ .GOOS }}" + GOARCH: "{{ .GOARCH }}" + vars: + GOOS: + sh: go env GOOS + GOARCH: + sh: go env GOARCH + + build:dist: + desc: Build the plugin for the current platform + cmds: + - go build -v -trimpath -ldflags="-s -w" -o dist/plugin{{if eq .GOOS "windows"}}.exe{{end}} ./cmd/plugin + - sha256sum "dist/plugin{{if eq .GOOS "windows"}}.exe{{end}}" > "dist/plugin{{if eq .GOOS "windows"}}.exe{{end}}.sha256" + env: + CGO_ENABLED: "0" + GOOS: "{{ .GOOS }}" + GOARCH: "{{ .GOARCH }}" + vars: + GOOS: + sh: go env GOOS + GOARCH: + sh: go env GOARCH + + build:all: + desc: Build the plugin for all release platforms + cmds: + - for: + - linux/amd64 + - linux/arm64 + - darwin/amd64 + - darwin/arm64 + - windows/amd64 + - windows/arm64 + cmd: >- + GOOS={{ index (splitList "/" .ITEM) 0 }} + GOARCH={{ index (splitList "/" .ITEM) 1 }} + go build -o dist/plugin-{{ index (splitList "/" .ITEM) 0 }}-{{ index (splitList "/" .ITEM) 1 }}{{if eq (index (splitList "/" .ITEM) 0) "windows"}}.exe{{end}} + ./cmd/plugin + - for: + - linux/amd64 + - linux/arm64 + - darwin/amd64 + - darwin/arm64 + - windows/amd64 + - windows/arm64 + cmd: >- + cd dist && + sha256sum plugin-{{ index (splitList "/" .ITEM) 0 }}-{{ index (splitList "/" .ITEM) 1 }}{{if eq (index (splitList "/" .ITEM) 0) "windows"}}.exe{{end}} + > plugin-{{ index (splitList "/" .ITEM) 0 }}-{{ index (splitList "/" .ITEM) 1 }}{{if eq (index (splitList "/" .ITEM) 0) "windows"}}.exe{{end}}.sha256 + env: + CGO_ENABLED: "0" + + checksums: + desc: Generate checksums for all release artifacts + cmds: + - | + cd dist && find . -maxdepth 1 -type f ! -name '*.sha256' ! -name checksums.txt | sed 's|^\./||' | sort | while read -r file; do + sha256sum "${file}" + done > checksums.txt + + sync:registry: + desc: Trigger a plugin sync in the semrel registry for a release tag + silent: true + vars: + PLUGIN_OWNER: SemRels + PLUGIN_REPO: SemRels/updater-python + PLUGIN_TAG: + sh: if [ -n "{{ .PLUGIN_TAG }}" ]; then echo "{{ .PLUGIN_TAG }}"; else git describe --tags --abbrev=0; fi + env: + PLUGIN_TAG: "{{ .PLUGIN_TAG }}" + cmds: + - | + if [ -n "${REGISTRY_SYNC_TOKEN:-}" ]; then + payload=$(printf '{"event_type":"sync-plugins","client_payload":{"source_owner":"{{ .PLUGIN_OWNER }}","source_repo":"{{ .PLUGIN_REPO }}","tag":"%s"}}' "${PLUGIN_TAG}") + curl --fail --silent --show-error \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${REGISTRY_SYNC_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/SemRels/semrel-registry/dispatches \ + -d "${payload}" + elif [ -n "${REGISTRY_SYNC_WEBHOOK:-}" ]; then + payload=$(printf '{"repository":"{{ .PLUGIN_REPO }}","owner":"{{ .PLUGIN_OWNER }}","tag":"%s"}' "${PLUGIN_TAG}") + curl --fail --silent --show-error \ + -X POST \ + -H "Content-Type: application/json" \ + "${REGISTRY_SYNC_WEBHOOK}" \ + -d "${payload}" + else + echo "No registry sync secret configured; skipping dispatch." + fi + +... From 00ae474116aafd585c98d2b47d453456ebafb388 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:04 +0200 Subject: [PATCH 03/22] chore: add editorconfig, pre-commit, commitlint and mise tooling Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- .editorconfig | 21 +++++++++++++++++++++ .gitignore | 2 +- .golangci.yml | 7 +++++-- .pre-commit-config.yaml | 18 ++++++++++++++++++ commitlint.config.js | 34 ++++++++++++++++++++++++++++++++++ mise.toml | 14 ++++++++++++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 .editorconfig create mode 100644 .pre-commit-config.yaml create mode 100644 commitlint.config.js create mode 100644 mise.toml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c744f0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors + +root = true + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{html,tmpl,py,go}] +indent_style = tab +indent_size = 4 + +[*.{yml,yaml,toml,json,css,less,js,vue,rb,nix}] +indent_style = space +indent_size = 2 + +[*.sh] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore index 5e7bb87..7dd05e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2026 The plugin-template Authors +# SPDX-FileCopyrightText: 2026 The semrel Authors # Operating system files .DS_Store diff --git a/.golangci.yml b/.golangci.yml index 0ced420..b2321fc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,9 @@ +--- # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2026 The plugin-template Authors +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json version: "2" - run: timeout: 5m @@ -13,3 +14,5 @@ linters: - ineffassign - staticcheck - unused + +... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a7547cc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://www.schemastore.org/pre-commit-config.json +default_stages: + - pre-commit + +repos: + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.26.0 + hooks: + - id: commitlint + stages: + - commit-msg + additional_dependencies: + - "@commitlint/config-conventional" + +... diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..afd2f32 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The semrel Authors +// yaml-language-server: $schema=https://json.schemastore.org/commitlintrc.json +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "signed-off-by": [ + 2, + "always", + "Signed-off-by:", + ], + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "docs", + "style", + "refactor", + "perf", + "test", + "build", + "ci", + "chore", + "revert", + "major", + "minor", + "patch", + "deps", + ], + ], + }, +}; diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..3e6ea77 --- /dev/null +++ b/mise.toml @@ -0,0 +1,14 @@ + +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +#:schema https://mise.jdx.dev/schema/mise.json + +[tools] +cosign = "3.1.3" +go = "1.27.2" +node = "24" +prek = "0.5.7" +task = "3.54.0" + +"github:SemRels/semrel" = "0.28.2" +"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = "2.14.0" From 62d64e677aa6706cfd793e90a90f8d5ba05ed074 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:04 +0200 Subject: [PATCH 04/22] feat: bump go to 1.27.2 and refresh Dockerfile Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- Dockerfile | 50 ++++++++++++++++++++++++++++++++------------------ go.mod | 4 +--- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8f568e0..8afac5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,45 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2026 The updater-python Authors +# SPDX-FileCopyrightText: 2026 The semrel Authors # ── build stage ──────────────────────────────────────────────────────────────── -FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build +FROM --platform=$BUILDPLATFORM debian:13-slim AS build + +RUN apt-get update && \ + apt-get -y --no-install-recommends install ca-certificates curl git build-essential && \ + rm -rf /var/lib/apt/lists/* + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ENV PATH="/root/.local/bin:/mise/shims:$PATH" \ + MISE_DATA_DIR="/mise" \ + MISE_CONFIG_DIR="/mise" \ + MISE_CACHE_DIR="/mise/cache" \ + MISE_INSTALL_PATH="/usr/local/bin/mise" + +RUN curl --proto '=https' --proto-redir '=https' \ + --fail --show-error --silent --location https://mise.run | sh + +COPY mise.toml ./ +RUN --mount=type=cache,target=/mise/cache \ + mise trust && mise install + +WORKDIR /src + +COPY go.mod go.sum* ./ +RUN --mount=type=cache,target=/root/go/pkg \ + go mod download -x + +COPY . . ARG TARGETOS ARG TARGETARCH ARG VERSION=dev -WORKDIR /src -RUN apk add --no-cache ca-certificates git -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ - go build -trimpath -ldflags="-s -w" -o /out/plugin ./cmd/plugin +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + task build # ── distroless release image ─────────────────────────────────────────────────── -FROM gcr.io/distroless/static-debian12:nonroot - -# NOTE: intentionally no repo-specific `org.opencontainers.image.*` LABEL block -# here. sync-template.yml only substitutes the "updater-python Authors" line -# in this file when propagating it to plugin repos (see `sed` step) — it does -# NOT rewrite LABEL title/description/source values. Adding those here would -# get copied verbatim (and wrongly) into every plugin's Dockerfile. Add labels -# in each plugin's own Dockerfile after copying this template, not here. +FROM gcr.io/distroless/static-debian13:nonroot -COPY --from=build /out/plugin /usr/local/bin/plugin +COPY --from=build /src/bin/plugin /usr/local/bin/plugin USER nonroot ENTRYPOINT ["/usr/local/bin/plugin"] diff --git a/go.mod b/go.mod index 40ecd96..2ee0a6d 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/SemRels/updater-python -go 1.25 - -toolchain go1.25.0 +go 1.27.2 From 091b1e83eefa795b9c5f742727031ea1e24ad6ca Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:05 +0200 Subject: [PATCH 05/22] chore: switch from dependabot to renovate and add repository settings Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- .github/dependabot.yml | 13 ---- .github/renovate.json | 6 ++ .github/settings.yml | 137 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 13 deletions(-) delete mode 100644 .github/dependabot.yml create mode 100644 .github/renovate.json create mode 100644 .github/settings.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 21be3c6..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2026 The plugin-template Authors - -version: 2 -updates: - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: weekly - - package-ecosystem: gomod - directory: "/" - schedule: - interval: weekly diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..fa68310 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>SemRels/.github//renovate/preset" + ] +} diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..7a54134 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,137 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=http://json-schema.org/draft-07/schema# +repository: + name: updater-python + description: Semrel updater plugin that updates the Python package version for the selected packaging backend + topics: semrel, semrel-plugin, updater, python + + private: false + has_issues: true + has_wiki: false + has_downloads: false + + default_branch: main + + allow_merge_commit: false + allow_squash_merge: true + allow_rebase_merge: true + + allow_update_branch: true + allow_auto_merge: true + delete_branch_on_merge: true + enable_automated_security_fixes: true + enable_vulnerability_alerts: true + +rulesets: + - name: prevent destruction + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + exclude: [] + rules: + - type: required_linear_history + - type: deletion + - type: non_fast_forward + + - name: check verification + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + exclude: [] + rules: + - type: required_status_checks + parameters: + strict_required_status_checks_policy: true + required_status_checks: + - context: "Build, test, lint, and scan" + integration_id: 15368 + - context: "Build and Release Containers" + integration_id: 15368 + bypass_actors: + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: always + + - name: require reviewing + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + exclude: [] + rules: + - type: pull_request + parameters: + allowed_merge_methods: + - squash + - rebase + dismiss_stale_reviews_on_push: false + require_code_owner_review: false + require_last_push_approval: false + required_approving_review_count: 0 + required_review_thread_resolution: false + bypass_actors: + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: always + +labels: + - name: bug + color: "fc2929" + description: Something isn't working + - name: dependencies + color: "0366d6" + description: Pull requests that update a dependency file + - name: documentation + color: "0075ca" + description: Improvements or additions to documentation + - name: duplicate + color: "cfd3d7" + description: This issue or pull request already exists + - name: enhancement + color: "a2eeef" + description: New feature or request + - name: github_actions + color: "000000" + description: Pull requests that update GitHub Actions code + - name: go + color: "16e2e2" + description: Pull requests that update go code + - name: good first issue + color: "7057ff" + description: Good for newcomers + - name: help wanted + color: "008672" + description: Extra attention is needed + - name: implementation + color: "5319e7" + description: Implementation task + - name: invalid + color: "e4e669" + description: This doesn't seem right + - name: mvp + color: "fbca04" + description: Minimum viable plugin delivery + - name: plugin + color: "0e8a16" + description: Plugin-related work + - name: question + color: "d876e3" + description: Further information is requested + - name: wontfix + color: "ffffff" + description: This will not be worked on + - name: renovate + color: "1d76db" + description: Automated action from Renovate + +... From ce17125e1539703eb532cf3bbf312e90b5ebdaaa Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:05 +0200 Subject: [PATCH 06/22] chore: update copyright holder and reuse metadata Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- .reuse/dep5 | 21 ++++++++++++--------- LICENSE | 2 +- LICENSES/Apache-2.0.txt | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.reuse/dep5 b/.reuse/dep5 index 17e7657..4cd2f07 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -1,21 +1,21 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: {{PROJECT_NAME}} -Upstream-Contact: The {{PROJECT_NAME}} Authors -Source: {{REPO_URL}} +Upstream-Name: updater-python +Upstream-Contact: The semrel Authors +Source: https://github.com/SemRels/updater-python # ---------------------------------------------------------------------------- # Documentation and community health files # ---------------------------------------------------------------------------- Files: *.md .github/**/*.md -Copyright: {{COPYRIGHT_YEAR}} The {{PROJECT_NAME}} Authors +Copyright: 2026 The semrel Authors License: Apache-2.0 # ---------------------------------------------------------------------------- # Build and tooling configuration # ---------------------------------------------------------------------------- -Files: Makefile - Makefile.template +Files: Taskfile.yml + Taskfile.yaml Dockerfile go.mod go.sum @@ -23,15 +23,18 @@ Files: Makefile *.template .gitignore .gitignore.template + .pre-commit-config.yaml + .github/renovate.json .github/**/*.yml .github/**/*.yaml .github/**/*.json .github/**/*.template .reuse/dep5 + commitlint.config.js cmd/**/*.go internal/**/*.go proto/** -Copyright: {{COPYRIGHT_YEAR}} The {{PROJECT_NAME}} Authors +Copyright: 2026 The semrel Authors License: Apache-2.0 # ---------------------------------------------------------------------------- @@ -40,10 +43,10 @@ License: Apache-2.0 Files: LICENSE LICENSES/Apache-2.0.txt NOTICE -Copyright: {{COPYRIGHT_YEAR}} The {{PROJECT_NAME}} Authors +Copyright: 2026 The semrel Authors License: Apache-2.0 # JSON Schema files Files: schema/*.json -Copyright: The updater-python Authors +Copyright: The semrel Authors License: Apache-2.0 diff --git a/LICENSE b/LICENSE index aaaa23a..cf3fa9b 100644 --- a/LICENSE +++ b/LICENSE @@ -165,7 +165,7 @@ END OF TERMS AND CONDITIONS - Copyright 2026 The go-semrel Authors + Copyright 2026 The semrel Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt index aaaa23a..cf3fa9b 100644 --- a/LICENSES/Apache-2.0.txt +++ b/LICENSES/Apache-2.0.txt @@ -165,7 +165,7 @@ END OF TERMS AND CONDITIONS - Copyright 2026 The go-semrel Authors + Copyright 2026 The semrel Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 8f0ac6d6f71c156b1b2b4c409797268761790066 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:05 +0200 Subject: [PATCH 07/22] ci: update ci, security, scorecard and auto-label workflows Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- .github/workflows/auto-label.yml | 18 ++++++++---- .github/workflows/ci.yml | 48 ++++++++++++++------------------ .github/workflows/scorecard.yml | 27 +++++++++--------- .github/workflows/security.yml | 32 +++++++++++++-------- 4 files changed, 68 insertions(+), 57 deletions(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index d677f9f..8d8a2d3 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -1,18 +1,24 @@ -# This code has been fully generated by an AI. +--- # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: 2026 The semrel Authors +# This code has been fully generated by an AI. +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json name: Automatically label pull requests on: pull_request_target: - types: [opened, reopened, synchronize, edited] + types: + - opened + - reopened + - synchronize + - edited permissions: issues: write pull-requests: write concurrency: - group: auto-label-${{ github.event.pull_request.number }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -20,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Apply labels based on the title and changed files - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const pullRequest = context.payload.pull_request; @@ -56,4 +62,6 @@ jobs: issue_number: pullRequest.number, labels: missing, }); - } \ No newline at end of file + } + +... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7016de5..eb80eb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,16 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json name: CI on: push: - branches: [main] + branches: + - main pull_request: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + branches: + - main permissions: contents: read @@ -17,48 +19,38 @@ jobs: ci: name: Build, test, lint, and scan runs-on: ubuntu-latest - continue-on-error: ${{ matrix.go == '1.27rc1' }} - strategy: - fail-fast: false - matrix: - go: ["1.25", "1.26", "1.27"] steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version: ${{ matrix.go == '1.27rc1' && '1.27.0-rc.1' || format('{0}.x', matrix.go) }} - allow-prereleases: true - cache: true + - name: Install tools with Mise + uses: jdx/mise-action@v5 - name: Download dependencies run: go mod download - name: Run golangci-lint - if: matrix.go != '1.27' - uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1 - with: - version: v2.12.2 + run: task lint - name: Run tests - run: go test -v -coverprofile=coverage.txt ./... + run: task test - name: Show coverage summary - run: go tool cover -func=coverage.txt | tail -1 + run: go tool cover -func=coverage.out | tail -1 - name: Upload coverage report - if: matrix.go == '1.26' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: coverage-report - path: coverage.txt + path: coverage.out retention-days: 30 - name: Build plugin binary - run: go build -v ./cmd/plugin + run: task build + + - name: Run end-to-end tests + run: task e2e - name: Run Trivy filesystem scan uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 @@ -69,3 +61,5 @@ jobs: exit-code: '1' ignore-unfixed: true severity: CRITICAL,HIGH + +... diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 96d7c08..b6501fb 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,31 +1,28 @@ +--- # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: 2026 The semrel Authors - +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json name: Scorecard on: + workflow_dispatch: push: - branches: [main] + branches: + - main schedule: - cron: "30 1 * * 1" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true permissions: + security-events: write + id-token: write contents: read + actions: read jobs: analysis: - name: Scorecard analysis + name: Scorecard Analysis runs-on: ubuntu-latest - permissions: - security-events: write - id-token: write - contents: read - actions: read + steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -43,6 +40,8 @@ jobs: path: results.sarif retention-days: 5 - - uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 + - uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4 with: sarif_file: results.sarif + +... diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b91b439..a2be0d4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,36 +1,46 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json name: Security on: - pull_request: - branches: [main] push: - branches: [main] + branches: + - main + pull_request: + branches: + - main schedule: - cron: '0 4 * * 1' -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - permissions: contents: read jobs: dependency-review: if: ${{ github.event_name == 'pull_request' }} - name: Dependency review + name: Dependency Review runs-on: ubuntu-latest + steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Review dependency changes - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5 + with: + warn-only: true reuse: - name: REUSE compliance + name: REUSE Compliance runs-on: ubuntu-latest + steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Run REUSE check - uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 + uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6 + +... From fe8fccae029a11d93a653d39720d8f78d0c8264b Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Wed, 21 Oct 2026 14:29:06 +0200 Subject: [PATCH 08/22] ci: rework release and semrel-release workflows Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Thomas Boerger --- .github/workflows/binaries.yml | 55 ++ .github/workflows/container.yml | 126 +++++ .github/workflows/release.yml | 736 -------------------------- .github/workflows/semrel-release.yaml | 150 ------ .github/workflows/semrel-release.yml | 83 +++ 5 files changed, 264 insertions(+), 886 deletions(-) create mode 100644 .github/workflows/binaries.yml create mode 100644 .github/workflows/container.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/semrel-release.yaml create mode 100644 .github/workflows/semrel-release.yml diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml new file mode 100644 index 0000000..14571c2 --- /dev/null +++ b/.github/workflows/binaries.yml @@ -0,0 +1,55 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +name: Binaries + +on: + workflow_dispatch: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + binaries: + name: Build and Release Binaries + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install tools with Mise + uses: jdx/mise-action@v5 + + - name: Build release binary + run: task build:all + + - name: Generate checksum manifest + run: task checksums + + - name: Upload GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 + with: + name: ${{ github.ref_name }} + files: dist/* + generate_release_notes: false + + - name: Trigger registry sync + if: startsWith(github.ref, 'refs/tags/') + env: + REGISTRY_SYNC_TOKEN: ${{ secrets.REGISTRY_SYNC_TOKEN }} + REGISTRY_SYNC_WEBHOOK: ${{ secrets.REGISTRY_SYNC_WEBHOOK }} + PLUGIN_TAG: ${{ github.ref_name }} + run: task sync:registry + +... diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 0000000..7ca58eb --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,126 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 The semrel Authors +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +name: Container + +on: + workflow_dispatch: + push: + branches: + - main + tags: + - 'v*.*.*' + pull_request: + branches: + - main + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + container: + name: Build and Release Containers + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install tools with Mise + uses: jdx/mise-action@v5 + + - name: Set version metadata + id: version + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "push" ]; then + ver="${GITHUB_REF_NAME}" + else + ver="${{ github.event.inputs.version || 'dev' }}" + fi + echo "version=${ver}" >> "$GITHUB_OUTPUT" + echo "version_short=${ver#v}" >> "$GITHUB_OUTPUT" + + - name: Set lowercase repository name + id: repo + run: | + echo "lower=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + images: ghcr.io/${{ steps.repo.outputs.lower }} + labels: | + org.opencontainers.image.licenses=Apache-2.0 + org.opencontainers.image.title=semrel-plugin-updater-python + org.opencontainers.image.url=https://semrel.io + org.opencontainers.image.vendor=SemRels + org.opencontainers.image.version=${{ steps.version.outputs.version_short }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=${{ steps.version.outputs.version_short }},enable=true + + - name: Set up Docker QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multiarch image + id: build + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v6 + with: + builder: ${{ steps.buildx.outputs.name }} + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true + build-args: | + VERSION=${{ steps.version.outputs.version }} + + - name: Sign images with cosign + if: github.event_name != 'pull_request' + env: + COSIGN_EXPERIMENTAL: "1" + run: | + echo "${{ steps.meta.outputs.tags }}" | while read -r TAG; do + cosign sign --yes ghcr.io/${{ steps.repo.outputs.lower }}@${{ steps.build.outputs.digest }} + done + + - name: Run Trivy vulnerability scanner + if: github.event_name != 'pull_request' + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.30.0 + with: + image-ref: ghcr.io/${{ steps.repo.outputs.lower }}:${{ steps.version.outputs.version_short }} + format: table + exit-code: '1' + ignore-unfixed: true + severity: CRITICAL,HIGH + +... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a169b24..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,736 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*.*.*' - workflow_dispatch: - inputs: - version: - description: Release version (SemVer 2.0, optional v; may be omitted on a tag ref) - required: false - type: string - -permissions: - contents: read - -jobs: - version: - name: Validate release version - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - version_short: ${{ steps.version.outputs.version_short }} - image_version: ${{ steps.version.outputs.image_version }} - exact_key: ${{ steps.version.outputs.exact_key }} - tag: ${{ steps.version.outputs.tag }} - stable: ${{ steps.version.outputs.stable }} - image: ${{ steps.version.outputs.image }} - build_date: ${{ steps.source.outputs.build_date }} - source_sha: ${{ steps.source.outputs.source_sha }} - steps: - - name: Resolve and validate version - id: version - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - INPUT_VERSION: ${{ inputs.version }} - REF_NAME: ${{ github.ref_name }} - REF_TYPE: ${{ github.ref_type }} - run: | - set -euo pipefail - if [[ "${EVENT_NAME}" == "push" ]]; then - candidate="${REF_NAME}" - elif [[ -n "${INPUT_VERSION}" ]]; then - candidate="${INPUT_VERSION}" - elif [[ "${REF_TYPE}" == "tag" ]]; then - candidate="${REF_NAME}" - else - printf 'A version is required unless workflow_dispatch uses a tag ref.\n' >&2 - exit 1 - fi - - if [[ "${candidate}" == *$'\r'* || "${candidate}" == *$'\n'* ]]; then - printf 'Version must not contain CR or LF characters.\n' >&2 - exit 1 - fi - - semver_re='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' - if [[ ! "${candidate}" =~ ${semver_re} ]]; then - printf 'Version must be valid SemVer 2.0 with an optional leading v.\n' >&2 - exit 1 - fi - - version_short="${candidate#v}" - without_build="${version_short%%+*}" - if [[ "${without_build}" == *-* ]]; then - prerelease="${without_build#*-}" - IFS='.' read -r -a identifiers <<< "${prerelease}" - for identifier in "${identifiers[@]}"; do - if [[ "${identifier}" =~ ^0[0-9]+$ ]]; then - printf 'Numeric prerelease identifiers must not contain leading zeros.\n' >&2 - exit 1 - fi - done - fi - - stable=true - if [[ "${without_build}" == *-* ]]; then - stable=false - fi - { - printf 'version=v%s\n' "${version_short}" - printf 'version_short=%s\n' "${version_short}" - # SemVer forbids underscores, so replacing '+' is collision-free and Docker-safe. - printf 'image_version=%s\n' "${version_short//+/_}" - # Actions concurrency groups are case-insensitive; hash the case-sensitive exact SemVer. - printf 'exact_key=%s\n' "$(printf '%s' "${version_short}" | sha256sum | cut -d' ' -f1)" - printf 'tag=v%s\n' "${version_short}" - printf 'stable=%s\n' "${stable}" - printf 'image=ghcr.io/%s\n' "${GITHUB_REPOSITORY,,}" - } >> "${GITHUB_OUTPUT}" - - - name: Check out exact release source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ steps.version.outputs.tag }} - - - name: Resolve reproducible source metadata - id: source - shell: bash - run: | - set -euo pipefail - source_sha=$(git rev-parse HEAD) - source_epoch=$(git show -s --format=%ct HEAD) - [[ "${source_sha}" =~ ^[0-9a-f]{40}$ ]] - [[ "${source_epoch}" =~ ^[0-9]+$ ]] - { - printf 'source_sha=%s\n' "${source_sha}" - printf 'build_date=%s\n' "$(date -u -d "@${source_epoch}" +%Y-%m-%dT%H:%M:%SZ)" - } >> "${GITHUB_OUTPUT}" - - build: - name: Build ${{ matrix.goos }}-${{ matrix.goarch }} - needs: version - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - goos: linux - goarch: amd64 - ext: '' - - goos: linux - goarch: arm64 - ext: '' - - goos: darwin - goarch: amd64 - ext: '' - - goos: darwin - goarch: arm64 - ext: '' - - goos: windows - goarch: amd64 - ext: '.exe' - - goos: windows - goarch: arm64 - ext: '.exe' - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.version.outputs.source_sha }} - - - name: Set up Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - cache: true - - - name: Build release binary - shell: bash - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: '0' - run: | - set -euo pipefail - mkdir -p dist - binary="plugin-${GOOS}-${GOARCH}${{ matrix.ext }}" - go build -trimpath -ldflags='-s -w' -o "dist/${binary}" ./cmd/plugin - (cd dist && sha256sum "${binary}" > "${binary}.sha256") - - - name: Upload release artifacts - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: release-${{ matrix.goos }}-${{ matrix.goarch }} - path: dist/* - if-no-files-found: error - - docker-validate: - name: Validate image (${{ matrix.platform.arch }}) - needs: version - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - name: linux/amd64 - arch: amd64 - - name: linux/arm64 - arch: arm64 - steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.version.outputs.source_sha }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - with: - platforms: arm64 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - - name: Build immutable OCI validation archive - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - with: - context: . - platforms: ${{ matrix.platform.name }} - outputs: type=oci,dest=${{ runner.temp }}/plugin-image-${{ matrix.platform.arch }}.tar - tags: validation - labels: | - org.opencontainers.image.created=${{ needs.version.outputs.build_date }} - org.opencontainers.image.revision=${{ needs.version.outputs.source_sha }} - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.version=${{ needs.version.outputs.version }} - build-args: | - VERSION=${{ needs.version.outputs.version }} - BUILD_DATE=${{ needs.version.outputs.build_date }} - VCS_REF=${{ needs.version.outputs.source_sha }} - cache-from: type=gha,scope=validation-${{ matrix.platform.arch }} - cache-to: type=gha,mode=max,scope=validation-${{ matrix.platform.arch }} - provenance: true - sbom: true - - - name: Record validation archive checksum - shell: bash - env: - ARCH: ${{ matrix.platform.arch }} - run: | - set -euo pipefail - cd "${RUNNER_TEMP}" - sha256sum "plugin-image-${ARCH}.tar" > "plugin-image-${ARCH}.tar.sha256" - - - name: Extract immutable OCI layout for scanning - id: extract - shell: bash - env: - ARCH: ${{ matrix.platform.arch }} - run: | - set -euo pipefail - scan_dir=$(mktemp -d "${RUNNER_TEMP}/plugin-image-${ARCH}-layout.XXXXXX") - tar -xf "${RUNNER_TEMP}/plugin-image-${ARCH}.tar" -C "${scan_dir}" - test -f "${scan_dir}/index.json" - test -f "${scan_dir}/oci-layout" - printf 'path=%s\n' "${scan_dir}" >> "${GITHUB_OUTPUT}" - - - name: Scan immutable extracted OCI layout - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - env: - TRIVY_PLATFORM: ${{ matrix.platform.name }} - with: - input: ${{ steps.extract.outputs.path }} - format: table - exit-code: '1' - ignore-unfixed: true - severity: CRITICAL,HIGH - - - name: Upload scanned OCI validation archive - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: plugin-image-${{ matrix.platform.arch }} - path: | - ${{ runner.temp }}/plugin-image-${{ matrix.platform.arch }}.tar - ${{ runner.temp }}/plugin-image-${{ matrix.platform.arch }}.tar.sha256 - if-no-files-found: error - compression-level: 0 - retention-days: 1 - - docker: - name: Publish validated multiarch image - needs: [version, build, docker-validate] - runs-on: ubuntu-latest - concurrency: - group: exact-image-${{ needs.version.outputs.exact_key }} - cancel-in-progress: false - permissions: - contents: read - packages: write - id-token: write - attestations: write - steps: - - name: Download scanned OCI validation archives - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - pattern: plugin-image-* - path: ${{ runner.temp }}/docker-images - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - - name: Install ORAS - uses: oras-project/setup-oras@005458ad77f1c8facd38a094e4af2e69e5607ff4 # v2.0.2 - with: - version: 1.3.1 - - - name: Install cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Calculate final image tags - id: meta - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 - with: - images: ${{ needs.version.outputs.image }} - flavor: latest=false - tags: | - type=raw,value=${{ needs.version.outputs.image_version }} - - - name: Promote scanned platform images - id: promote - shell: bash - env: - ARCHIVE_DIR: ${{ runner.temp }}/docker-images - IMAGE: ${{ needs.version.outputs.image }} - run: | - set -euo pipefail - cd "${ARCHIVE_DIR}" - test "$(find . -maxdepth 1 -type f -name 'plugin-image-*.tar' | wc -l)" -eq 2 - test "$(find . -maxdepth 1 -type f -name 'plugin-image-*.tar.sha256' | wc -l)" -eq 2 - sha256sum --check -- plugin-image-*.tar.sha256 - for arch in amd64 arm64; do - archive="plugin-image-${arch}.tar" - layout_dir=$(mktemp -d) - tar -xf "${archive}" -C "${layout_dir}" - # Read the layout's sole top-level entry directly from index.json - # instead of resolving it by tag through oras. Docker silently - # rewrites a bare --tag like "validation" to "validation:latest" - # and only records the tag component ("latest") as the OCI ref - # annotation, which made oras' ":" ref parsing unable - # to find a match — jq sidesteps that ambiguity entirely. - source_digest=$(jq -r '.manifests[0].digest' "${layout_dir}/index.json") - [[ "${source_digest}" =~ ^sha256:[0-9a-f]{64}$ ]] - platform_digest=$(jq -r --arg arch "${arch}" \ - '.manifests[] | select(.platform.architecture == $arch and .platform.os == "linux") | .digest' \ - "${layout_dir}/blobs/sha256/${source_digest#sha256:}") - [[ "${platform_digest}" =~ ^sha256:[0-9a-f]{64}$ ]] - # Registry deletion is manifest-scoped, so retain this run-scoped tag to protect the final index children. - staging_tag="scan-${GITHUB_SHA:0:12}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${arch}" - oras cp --from-oci-layout "${layout_dir}@${source_digest}" "${IMAGE}:${staging_tag}" - pushed_digest=$(oras resolve "${IMAGE}:${staging_tag}") - pushed_platform_digest=$(oras resolve --platform "linux/${arch}" "${IMAGE}:${staging_tag}") - [[ "${pushed_digest}" == "${source_digest}" ]] - [[ "${pushed_platform_digest}" == "${platform_digest}" ]] - printf '%s=%s\n' "${arch}" "${pushed_digest}" >> "${GITHUB_OUTPUT}" - printf '%s_image=%s\n' "${arch}" "${platform_digest}" >> "${GITHUB_OUTPUT}" - done - - - name: Create final multiarch manifest from scanned digests - id: manifest - shell: bash - env: - AMD64_DIGEST: ${{ steps.promote.outputs.amd64 }} - AMD64_IMAGE_DIGEST: ${{ steps.promote.outputs.amd64_image }} - ARM64_DIGEST: ${{ steps.promote.outputs.arm64 }} - ARM64_IMAGE_DIGEST: ${{ steps.promote.outputs.arm64_image }} - IMAGE: ${{ needs.version.outputs.image }} - SOURCE_SHA: ${{ needs.version.outputs.source_sha }} - TAGS: ${{ steps.meta.outputs.tags }} - VERSION: ${{ needs.version.outputs.version }} - run: | - set -euo pipefail - resolve_registry_ref() { - local ref=$1 - local attempts=4 - local attempt output status - for ((attempt = 1; attempt <= attempts; attempt++)); do - if output=$(oras resolve "${ref}" 2>&1); then - if [[ ! "${output}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - printf 'Registry returned an invalid digest for %s: %s\n' "${ref}" "${output}" >&2 - return 1 - fi - printf '%s\n' "${output}" - return 0 - else - status=$? - fi - if ((attempt < attempts)); then - sleep "$((attempt * 2))" - fi - done - if grep -Eiq '(^|[^0-9])(401|403|408|409|425|429|5[0-9][0-9])([^0-9]|$)|unauthorized|denied|forbidden|too many requests|timed? out|connection|tls|eof|server error|service unavailable|temporar' <<< "${output}"; then - printf 'Registry authorization or transient failure resolving %s after %d attempts (exit %d): %s\n' \ - "${ref}" "${attempts}" "${status}" "${output}" >&2 - return 1 - fi - if grep -Eiq '(^|[^0-9])404([^0-9]|$)|manifest[ _]unknown|name[ _]unknown|(^|[^[:alpha:]])not found([^[:alpha:]]|$)' <<< "${output}"; then - return 4 - fi - printf 'Non-definitive registry failure resolving %s after %d attempts (exit %d): %s\n' \ - "${ref}" "${attempts}" "${status}" "${output}" >&2 - return 1 - } - verify_exact_image() { - local ref=$1 - local manifest image_config platform_digest - manifest=$(docker buildx imagetools inspect --raw "${ref}") - jq -e \ - --arg amd64 "${AMD64_IMAGE_DIGEST}" \ - --arg arm64 "${ARM64_IMAGE_DIGEST}" \ - '([.manifests[] | - select(.platform.os == "linux" and - (.platform.architecture == "amd64" or .platform.architecture == "arm64"))] | - length == 2) and - any(.manifests[]; .digest == $amd64 and .platform.os == "linux" and .platform.architecture == "amd64") and - any(.manifests[]; .digest == $arm64 and .platform.os == "linux" and .platform.architecture == "arm64")' \ - <<< "${manifest}" >/dev/null - for platform_digest in "${AMD64_IMAGE_DIGEST}" "${ARM64_IMAGE_DIGEST}"; do - image_config=$(docker buildx imagetools inspect \ - "${IMAGE}@${platform_digest}" --format '{{json .Image}}') - jq -e \ - --arg revision "${SOURCE_SHA}" \ - --arg version "${VERSION}" \ - '.config.Labels["org.opencontainers.image.revision"] == $revision and - .config.Labels["org.opencontainers.image.version"] == $version' \ - <<< "${image_config}" >/dev/null - done - } - [[ "${AMD64_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]] - [[ "${AMD64_IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]] - [[ "${ARM64_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]] - [[ "${ARM64_IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]] - [[ "${SOURCE_SHA}" =~ ^[0-9a-f]{40}$ ]] - mapfile -t final_tags < <(printf '%s\n' "${TAGS}" | sed '/^[[:space:]]*$/d') - ((${#final_tags[@]} == 1)) - exact_tag="${final_tags[0]}" - [[ "${exact_tag}" == "${IMAGE}:"* ]] - if existing_digest=$(resolve_registry_ref "${exact_tag}"); then - verify_exact_image "${exact_tag}" - digest="${existing_digest}" - else - resolve_status=$? - if ((resolve_status != 4)); then - exit "${resolve_status}" - fi - candidate_tag="${IMAGE}:manifest-${GITHUB_SHA:0:12}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - docker buildx imagetools create \ - --tag "${candidate_tag}" \ - "${IMAGE}@${AMD64_DIGEST}" \ - "${IMAGE}@${ARM64_DIGEST}" - if ! digest=$(resolve_registry_ref "${candidate_tag}"); then - printf 'Candidate manifest %s could not be resolved; refusing publication.\n' "${candidate_tag}" >&2 - exit 1 - fi - oras tag "${IMAGE}@${digest}" "${exact_tag#"${IMAGE}:"}" - fi - if ! published_digest=$(resolve_registry_ref "${exact_tag}"); then - printf 'Exact tag %s could not be verified after publication.\n' "${exact_tag}" >&2 - exit 1 - fi - if [[ "${published_digest}" != "${digest}" ]]; then - printf 'Refusing exact-tag conflict for %s (%s != %s).\n' \ - "${exact_tag}" "${published_digest}" "${digest}" >&2 - exit 1 - fi - verify_exact_image "${exact_tag}" - printf 'digest=%s\n' "${digest}" >> "${GITHUB_OUTPUT}" - - - name: Sign published image digest - shell: bash - env: - COSIGN_EXPERIMENTAL: '1' - DIGEST: ${{ steps.manifest.outputs.digest }} - IMAGE: ${{ needs.version.outputs.image }} - run: cosign sign --yes "${IMAGE}@${DIGEST}" - - publish: - name: Publish GitHub release and notify registry - needs: [version, build, docker] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Download release artifacts - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - pattern: release-* - path: dist - merge-multiple: true - - - name: Validate artifacts and generate checksum manifest - shell: bash - run: | - set -euo pipefail - test "$(find dist -maxdepth 1 -type f ! -name '*.sha256' | wc -l)" -eq 6 - test "$(find dist -maxdepth 1 -type f -name '*.sha256' | wc -l)" -eq 6 - (cd dist && sha256sum --check -- *.sha256) - ( - cd dist - find . -maxdepth 1 -type f ! -name '*.sha256' ! -name 'checksums.txt' -printf '%f\n' | sort | while read -r file; do - sha256sum "${file}" - done - ) > "${RUNNER_TEMP}/checksums.txt" - mv "${RUNNER_TEMP}/checksums.txt" dist/checksums.txt - - - name: Create GitHub release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 - with: - tag_name: ${{ needs.version.outputs.tag }} - target_commitish: ${{ needs.version.outputs.source_sha }} - files: dist/* - generate_release_notes: true - name: ${{ needs.version.outputs.tag }} - prerelease: ${{ needs.version.outputs.stable != 'true' }} - - - name: Trigger registry sync - shell: bash - env: - REGISTRY_SYNC_TOKEN: ${{ secrets.REGISTRY_SYNC_TOKEN }} - REGISTRY_SYNC_WEBHOOK: ${{ secrets.REGISTRY_SYNC_WEBHOOK }} - PLUGIN_OWNER: ${{ github.repository_owner }} - PLUGIN_REPO: ${{ github.event.repository.name }} - PLUGIN_TAG: ${{ needs.version.outputs.tag }} - run: | - set -euo pipefail - if [[ -n "${REGISTRY_SYNC_TOKEN}" ]]; then - payload=$(printf '{"event_type":"sync-plugins","client_payload":{"source_owner":"%s","source_repo":"%s","tag":"%s"}}' "${PLUGIN_OWNER}" "${PLUGIN_REPO}" "${PLUGIN_TAG}") - curl --fail --silent --show-error \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${REGISTRY_SYNC_TOKEN}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/SemRels/semrel-registry/dispatches \ - -d "${payload}" - elif [[ -n "${REGISTRY_SYNC_WEBHOOK}" ]]; then - payload=$(printf '{"repository":"%s","owner":"%s","tag":"%s"}' "${PLUGIN_REPO}" "${PLUGIN_OWNER}" "${PLUGIN_TAG}") - curl --fail --silent --show-error \ - -X POST \ - -H "Content-Type: application/json" \ - "${REGISTRY_SYNC_WEBHOOK}" \ - -d "${payload}" - else - echo "No registry sync secret configured; skipping dispatch." - fi - - reconcile: - name: Reconcile mutable image tags - needs: [version, docker, publish] - if: needs.version.outputs.stable == 'true' - runs-on: ubuntu-latest - concurrency: - group: mutable-image-tags - cancel-in-progress: false - permissions: - contents: read - packages: write - steps: - - name: Install ORAS - uses: oras-project/setup-oras@005458ad77f1c8facd38a094e4af2e69e5607ff4 # v2.0.2 - with: - version: 1.3.1 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Select globally greatest published stable versions - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - IMAGE: ${{ needs.version.outputs.image }} - run: | - set -euo pipefail - release_tags="${RUNNER_TEMP}/published-release-tags.txt" - resolved_images="${RUNNER_TEMP}/resolved-exact-images.tsv" - desired="${RUNNER_TEMP}/desired-mutable-tags.tsv" - gh api --paginate --jq \ - '.[] | select(.draft == false and .published_at != null) | .tag_name' \ - "repos/${GITHUB_REPOSITORY}/releases?per_page=100" > "${release_tags}" - : > "${resolved_images}" - resolve_registry_ref() { - local ref=$1 - local attempts=4 - local attempt output status - for ((attempt = 1; attempt <= attempts; attempt++)); do - if output=$(oras resolve "${ref}" 2>&1); then - if [[ ! "${output}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - printf 'Registry returned an invalid digest for %s: %s\n' "${ref}" "${output}" >&2 - return 1 - fi - printf '%s\n' "${output}" - return 0 - else - status=$? - fi - if ((attempt < attempts)); then - sleep "$((attempt * 2))" - fi - done - if grep -Eiq '(^|[^0-9])(401|403|408|409|425|429|5[0-9][0-9])([^0-9]|$)|unauthorized|denied|forbidden|too many requests|timed? out|connection|tls|eof|server error|service unavailable|temporar' <<< "${output}"; then - printf 'Registry authorization or transient failure resolving %s after %d attempts (exit %d): %s\n' \ - "${ref}" "${attempts}" "${status}" "${output}" >&2 - return 1 - fi - if grep -Eiq '(^|[^0-9])404([^0-9]|$)|manifest[ _]unknown|name[ _]unknown|(^|[^[:alpha:]])not found([^[:alpha:]]|$)' <<< "${output}"; then - return 4 - fi - printf 'Non-definitive registry failure resolving %s after %d attempts (exit %d): %s\n' \ - "${ref}" "${attempts}" "${status}" "${output}" >&2 - return 1 - } - semver_re='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' - while IFS= read -r published_tag; do - [[ "${published_tag}" != *$'\r'* && "${published_tag}" != *$'\n'* ]] || continue - [[ "${published_tag}" =~ ${semver_re} ]] || continue - normalized="${published_tag#v}" - without_build="${normalized%%+*}" - [[ "${without_build}" != *-* ]] || continue - exact="${normalized//+/_}" - if digest=$(resolve_registry_ref "${IMAGE}:${exact}"); then - printf '%s\t%s\n' "${exact}" "${digest}" >> "${resolved_images}" - else - resolve_status=$? - if ((resolve_status != 4)); then - printf 'Aborting reconcile before alias mutation because %s:%s was not definitively absent.\n' \ - "${IMAGE}" "${exact}" >&2 - exit "${resolve_status}" - fi - fi - done < "${release_tags}" - python3 - "${release_tags}" "${resolved_images}" "${desired}" <<'PY' - import re - import sys - from collections import defaultdict - - release_path, resolved_path, output_path = sys.argv[1:] - semver = re.compile( - r"^v?(?P0|[1-9][0-9]*)\." - r"(?P0|[1-9][0-9]*)\." - r"(?P0|[1-9][0-9]*)" - r"(?:-(?P
[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
-              r"(?:\+(?P[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
-          )
-          image_digests = {}
-          for raw in open(resolved_path, encoding="utf-8"):
-              exact, digest = raw.rstrip("\n").split("\t")
-              if not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
-                  raise SystemExit(f"invalid resolved digest for {exact}")
-              image_digests[exact] = digest
-          candidates = {}
-          for raw in open(release_path, encoding="utf-8"):
-              raw = raw.rstrip("\n")
-              match = semver.fullmatch(raw)
-              if not match:
-                  continue
-              prerelease = match.group("pre")
-              if prerelease:
-                  if any(part.isdigit() and len(part) > 1 and part[0] == "0"
-                         for part in prerelease.split(".")):
-                      continue
-                  continue
-              normalized = raw[1:] if raw.startswith("v") else raw
-              exact = normalized.replace("+", "_")
-              if exact not in image_digests:
-                  continue
-              candidates[normalized] = (
-                  int(match.group("major")),
-                  int(match.group("minor")),
-                  int(match.group("patch")),
-                  normalized,
-                  exact,
-                  image_digests[exact],
-              )
-
-          if not candidates:
-              raise SystemExit("no published stable SemVer release has an exact image tag")
-
-          # Build metadata is absent from the precedence tuple. The normalized
-          # full version is the stable ASCII tie-break for equal precedence.
-          def winner(values):
-              return max(values, key=lambda item: (item[0], item[1], item[2], item[3]))
-
-          values = list(candidates.values())
-          by_major = defaultdict(list)
-          by_minor = defaultdict(list)
-          for item in values:
-              by_major[item[0]].append(item)
-              by_minor[(item[0], item[1])].append(item)
-
-          desired = [("latest", winner(values))]
-          desired.extend(
-              (str(major), winner(group))
-              for major, group in sorted(by_major.items())
-          )
-          desired.extend(
-              (f"{major}.{minor}", winner(group))
-              for (major, minor), group in sorted(by_minor.items())
-          )
-          with open(output_path, "w", encoding="utf-8", newline="\n") as output:
-              for alias, item in desired:
-                  output.write(f"{alias}\t{item[4]}\t{item[3]}\t{item[5]}\n")
-          PY
-          test -s "${desired}"
-
-      - name: Point mutable tags at selected immutable digests
-        shell: bash
-        env:
-          IMAGE: ${{ needs.version.outputs.image }}
-        run: |
-          set -euo pipefail
-          resolve_registry_ref() {
-            local ref=$1
-            local attempts=4
-            local attempt output status
-            for ((attempt = 1; attempt <= attempts; attempt++)); do
-              if output=$(oras resolve "${ref}" 2>&1); then
-                if [[ ! "${output}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
-                  printf 'Registry returned an invalid digest for %s: %s\n' "${ref}" "${output}" >&2
-                  return 1
-                fi
-                printf '%s\n' "${output}"
-                return 0
-              else
-                status=$?
-              fi
-              if ((attempt < attempts)); then
-                sleep "$((attempt * 2))"
-              fi
-            done
-            printf 'Unable to verify mutable tag %s after %d attempts (exit %d): %s\n' \
-              "${ref}" "${attempts}" "${status}" "${output}" >&2
-            return 1
-          }
-          desired="${RUNNER_TEMP}/desired-mutable-tags.tsv"
-          while IFS=$'\t' read -r alias exact version digest; do
-            [[ "${alias}" == "latest" || "${alias}" =~ ^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?$ ]]
-            [[ "${exact}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(_[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]
-            [[ "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]
-            oras tag "${IMAGE}@${digest}" "${alias}"
-            if ! alias_digest=$(resolve_registry_ref "${IMAGE}:${alias}"); then
-              exit 1
-            fi
-            [[ "${alias_digest}" == "${digest}" ]]
-            printf '%s -> %s (%s)\n' "${alias}" "${exact}" "${version}" >> "${GITHUB_STEP_SUMMARY}"
-          done < "${desired}"
diff --git a/.github/workflows/semrel-release.yaml b/.github/workflows/semrel-release.yaml
deleted file mode 100644
index 06ae3d7..0000000
--- a/.github/workflows/semrel-release.yaml
+++ /dev/null
@@ -1,150 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-# SPDX-FileCopyrightText: 2026 The semrel Authors
-#
-# semrel-release — uses semrel to version this plugin.
-# Flow: push to main → semrel creates tag → release.yml validates and publishes it.
-name: Semrel Release
-
-on:
-  push:
-    branches: [main]
-  workflow_dispatch:
-
-concurrency:
-  # Serialize version calculation only. Exact release.yml runs have no shared
-  # concurrency group, so a dispatched version can never occupy this slot.
-  group: semrel-orchestrator-${{ github.ref }}
-  cancel-in-progress: false
-
-permissions:
-  contents: write
-  actions: write
-
-jobs:
-  release:
-    name: Run semrel
-    runs-on: ubuntu-latest
-    outputs:
-      released: ${{ steps.semrel.outputs.released }}
-      tag: ${{ steps.semrel.outputs.tag }}
-    steps:
-      - name: Check out repository
-        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
-        with:
-          fetch-depth: 0
-          token: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Set up Go
-        uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
-        with:
-          go-version: stable
-          cache: false
-
-      - name: Download semrel binary
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: |
-          set -euo pipefail
-          version=$(gh release view --repo SemRels/semrel --json tagName -q .tagName)
-          gh release download "${version}" --repo SemRels/semrel \
-            --pattern "semrel_*_linux_amd64.tar.gz" \
-            --pattern "semrel-*-linux-amd64.tar.gz" --dir "${RUNNER_TEMP}"
-          archive=$(find "${RUNNER_TEMP}" -maxdepth 1 -type f -name "*.tar.gz" -print -quit)
-          test -n "$archive"
-          tar -xzf "$archive" -C "${RUNNER_TEMP}"
-          chmod +x "${RUNNER_TEMP}/semrel"
-          "${RUNNER_TEMP}/semrel" --version
-
-      - name: Install plugins from source
-        run: |
-          set -euo pipefail
-          plugin_dir="${HOME}/.semrel/plugins"
-          mkdir -p "${plugin_dir}"
-          go install github.com/SemRels/condition-github-actions/cmd/plugin@v0.2.2
-          mv "$(go env GOPATH)/bin/plugin" "${plugin_dir}/semrel-plugin-condition-github-actions"
-
-      - name: Configure git identity
-        run: |
-          git config user.name "semrel-bot"
-          git config user.email "semrel-bot@users.noreply.github.com"
-
-      - name: Run semrel release
-        id: semrel
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-        run: "${RUNNER_TEMP}/semrel release --github-output"
-
-      - name: Push tag to GitHub
-        if: steps.semrel.outputs.released == 'true'
-        env:
-          TAG: ${{ steps.semrel.outputs.tag }}
-        run: |
-          set -euo pipefail
-          if [[ "${TAG}" == *$'\r'* || "${TAG}" == *$'\n'* ]]; then
-            printf 'Tag must not contain CR or LF characters.\n' >&2
-            exit 1
-          fi
-          semver_re='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
-          [[ "${TAG}" =~ ${semver_re} ]] || {
-            printf 'semrel returned an invalid SemVer tag: %s\n' "${TAG}" >&2
-            exit 1
-          }
-          without_build="${TAG#v}"
-          without_build="${without_build%%+*}"
-          if [[ "${without_build}" == *-* ]]; then
-            IFS='.' read -r -a identifiers <<< "${without_build#*-}"
-            for identifier in "${identifiers[@]}"; do
-              [[ ! "${identifier}" =~ ^0[0-9]+$ ]] || {
-                printf 'Numeric prerelease identifiers must not contain leading zeros.\n' >&2
-                exit 1
-              }
-            done
-          fi
-          if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
-            echo "Tag ${TAG} already exists on remote; skipping."
-          else
-            git push origin "refs/tags/${TAG}"
-          fi
-
-      - name: Trigger validated binary and image release
-        if: steps.semrel.outputs.released == 'true'
-        env:
-          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          TAG: ${{ steps.semrel.outputs.tag }}
-        run: |
-          set -euo pipefail
-          if [[ "${TAG}" == *$'\r'* || "${TAG}" == *$'\n'* ]]; then
-            printf 'Tag must not contain CR or LF characters.\n' >&2
-            exit 1
-          fi
-          semver_re='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
-          [[ "${TAG}" =~ ${semver_re} ]] || {
-            printf 'semrel returned an invalid SemVer tag: %s\n' "${TAG}" >&2
-            exit 1
-          }
-          without_build="${TAG#v}"
-          without_build="${without_build%%+*}"
-          if [[ "${without_build}" == *-* ]]; then
-            IFS='.' read -r -a identifiers <<< "${without_build#*-}"
-            for identifier in "${identifiers[@]}"; do
-              [[ ! "${identifier}" =~ ^0[0-9]+$ ]] || {
-                printf 'Numeric prerelease identifiers must not contain leading zeros.\n' >&2
-                exit 1
-              }
-            done
-          fi
-          echo "Triggering release.yml for tag ${TAG}"
-          gh workflow run release.yml --ref "${TAG}" --repo "${GITHUB_REPOSITORY}" \
-            --field "version=${TAG}"
-
-      - name: Summary
-        env:
-          RELEASED: ${{ steps.semrel.outputs.released }}
-          TAG: ${{ steps.semrel.outputs.tag }}
-          BUMP: ${{ steps.semrel.outputs.bump }}
-        run: |
-          if [[ "${RELEASED}" == "true" ]]; then
-            printf '### Released %s 🚀\n**Bump:** %s\n' "${TAG}" "${BUMP}" >> "${GITHUB_STEP_SUMMARY}"
-          else
-            echo "### No release — no releasable commits found." >> "${GITHUB_STEP_SUMMARY}"
-          fi
diff --git a/.github/workflows/semrel-release.yml b/.github/workflows/semrel-release.yml
new file mode 100644
index 0000000..e321fde
--- /dev/null
+++ b/.github/workflows/semrel-release.yml
@@ -0,0 +1,83 @@
+---
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: 2026 The semrel Authors
+# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json
+name: Semrel Release
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - main
+
+permissions:
+  contents: write
+  actions: write
+
+concurrency:
+  group: ${{ github.workflow }}
+  cancel-in-progress: false
+
+jobs:
+  release:
+    name: Run Semrel
+    runs-on: ubuntu-latest
+
+    outputs:
+      released: ${{ steps.semrel.outputs.released }}
+      tag: ${{ steps.semrel.outputs.tag }}
+
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          fetch-depth: 0
+          token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Install tools with Mise
+        uses: jdx/mise-action@v5
+
+      - name: Configure git identity
+        run: |
+          git config user.name  "semrel-bot"
+          git config user.email "semrel-bot@users.noreply.github.com"
+
+      - name: Run semrel release
+        id: semrel
+        run: semrel release --github-output
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Push tag to GitHub
+        if: steps.semrel.outputs.released == 'true'
+        run: |
+          TAG="${{ steps.semrel.outputs.tag }}"
+          if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
+            echo "Tag $TAG already exists on remote — skipping"
+          else
+            git push origin "refs/tags/$TAG"
+          fi
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Trigger binary build
+        if: steps.semrel.outputs.released == 'true'
+        run: |
+          TAG="${{ steps.semrel.outputs.tag }}"
+          echo "Triggering binaries.yml for tag $TAG"
+          gh workflow run binaries.yml --ref "$TAG" --repo "${{ github.repository }}"
+          echo "Triggering container.yml for tag $TAG"
+          gh workflow run container.yml --ref "$TAG" --repo "${{ github.repository }}"
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Summary
+        run: |
+          if [ "${{ steps.semrel.outputs.released }}" = "true" ]; then
+            echo "### Released ${{ steps.semrel.outputs.tag }} 🚀" >> $GITHUB_STEP_SUMMARY
+            echo "**Bump:** ${{ steps.semrel.outputs.bump }}" >> $GITHUB_STEP_SUMMARY
+          else
+            echo "### No release — no releasable commits found." >> $GITHUB_STEP_SUMMARY
+          fi
+
+...

From 38aaa89b07a903a27df0c5eed6a823eea354419d Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 14:29:06 +0200
Subject: [PATCH 09/22] chore: configure semrel release pipeline and ignore
 local state

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .gitignore   |  4 ++++
 .semrel.yaml | 35 +++++++++++++++++++++++++++++------
 2 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/.gitignore b/.gitignore
index 7dd05e7..d8ce187 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,7 @@ build/
 
 # Docker
 .docker/
+
+# Semrel
+.semrel/
+.semrel-analytics.jsonl
diff --git a/.semrel.yaml b/.semrel.yaml
index 2e86c5e..0c59225 100644
--- a/.semrel.yaml
+++ b/.semrel.yaml
@@ -1,21 +1,44 @@
+---
 # SPDX-License-Identifier: Apache-2.0
 # SPDX-FileCopyrightText: 2026 The semrel Authors
 # yaml-language-server: $schema=https://semrel.io/schema/v1.json
-#
+schemaVersion: 1
+
+tagPrefix: "v"
+commit_changelog: true
+tag_exists_strategy: update-changelog
+
 # semrel uses itself to manage this plugin's releases.
 # version_ceiling keeps releases in 0.x.x until deliberately promoted to 1.0.0.
 # To release 1.0.0: remove version_ceiling and commit.
 
-tagPrefix: "v"
 version_ceiling: "1.0.0"
 ceiling_strategy: clamp
-tag_exists_strategy: update-changelog
 
 branches:
   - name: main
 
+rules:
+    - type: feat
+      bump: minor
+    - type: fix
+      bump: patch
+    - type: perf
+      bump: patch
+    - type: revert
+      bump: patch
+
 plugins:
-  # condition-github-actions verifies we are running inside a legitimate
-  # GitHub Actions CI environment before any git work is done.
-  - uses: condition-github-actions
+  - uses: "@semrel/condition-github-actions"
     phase: condition
+  - uses: "@semrel/generator-changelog-md"
+    phase: pre-tag
+    args:
+      keep_releases: "10"
+  - uses: "@semrel/provider-github"
+    phase: release
+    args:
+      draft: "false"
+      prerelease: "false"
+
+...

From 5caa54c07dae7e6c5862b0363174f1e75069fd5b Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 15:04:33 +0200
Subject: [PATCH 10/22] chore: require one approving review on the default
 branch

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .github/settings.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/settings.yml b/.github/settings.yml
index 7a54134..cd0c459 100644
--- a/.github/settings.yml
+++ b/.github/settings.yml
@@ -77,7 +77,7 @@ rulesets:
           dismiss_stale_reviews_on_push: false
           require_code_owner_review: false
           require_last_push_approval: false
-          required_approving_review_count: 0
+          required_approving_review_count: 1
           required_review_thread_resolution: false
     bypass_actors:
       - actor_id: 1

From afab4c19a9d3c3a8616df447f4ba8dbbff4cf6d2 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:47:51 +0200
Subject: [PATCH 11/22] ci: drop required linear history, works with rebase
 pretty well

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .github/settings.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/settings.yml b/.github/settings.yml
index cd0c459..9486683 100644
--- a/.github/settings.yml
+++ b/.github/settings.yml
@@ -34,7 +34,6 @@ rulesets:
           - "~DEFAULT_BRANCH"
         exclude: []
     rules:
-      - type: required_linear_history
       - type: deletion
       - type: non_fast_forward
 

From 31fd4e3cf4059ccd6dc370a7c7fd4b2dc8107346 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:47:52 +0200
Subject: [PATCH 12/22] ci: drop auto-label concurrency and keep container runs
 from being cancelled

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .github/workflows/auto-label.yml | 4 ----
 .github/workflows/container.yml  | 2 +-
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml
index 8d8a2d3..571fe94 100644
--- a/.github/workflows/auto-label.yml
+++ b/.github/workflows/auto-label.yml
@@ -17,10 +17,6 @@ permissions:
   issues: write
   pull-requests: write
 
-concurrency:
-  group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: true
-
 jobs:
   label:
     runs-on: ubuntu-latest
diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml
index 7ca58eb..c89102e 100644
--- a/.github/workflows/container.yml
+++ b/.github/workflows/container.yml
@@ -23,7 +23,7 @@ permissions:
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: true
+  cancel-in-progress: false
 
 jobs:
   container:

From 74cf32d8588a2a0c7e023ec953189d4f218bccf4 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:47:52 +0200
Subject: [PATCH 13/22] feat: release a patch version for dependency updates

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .semrel.yaml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.semrel.yaml b/.semrel.yaml
index 0c59225..b16fd66 100644
--- a/.semrel.yaml
+++ b/.semrel.yaml
@@ -27,6 +27,8 @@ rules:
       bump: patch
     - type: revert
       bump: patch
+    - type: deps
+      bump: patch
 
 plugins:
   - uses: "@semrel/condition-github-actions"

From 2459195185007adf850ccc48c9656de4b5e9f111 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:47:52 +0200
Subject: [PATCH 14/22] deps(mise): update build tools

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 mise.toml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/mise.toml b/mise.toml
index 3e6ea77..73e429a 100644
--- a/mise.toml
+++ b/mise.toml
@@ -4,11 +4,11 @@
 #:schema https://mise.jdx.dev/schema/mise.json
 
 [tools]
-cosign = "3.1.3"
-go = "1.27.2"
+cosign = "3.1.4"
+go = "1.27.3"
 node = "24"
 prek = "0.5.7"
 task = "3.54.0"
 
 "github:SemRels/semrel" = "0.28.2"
-"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = "2.14.0"
+"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = "2.15.0"

From 89699d9e2d56e6648675cd05babe100530f173b2 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:47:52 +0200
Subject: [PATCH 15/22] docs: point cosign verification at the container
 workflow

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index b8be328..4ba0e13 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Images are signed with [cosign](https://github.com/sigstore/cosign) and include
 
 ```bash
 cosign verify ghcr.io/semrels/updater-python:latest \
-  --certificate-identity-regexp 'https://github.com/SemRels/updater-python/.github/workflows/release.yml.*' \
+  --certificate-identity-regexp 'https://github.com/SemRels/updater-python/.github/workflows/container.yml.*' \
   --certificate-oidc-issuer https://token.actions.githubusercontent.com
 ```
 

From 1078de8905ed0e836cdc10656b890c2768ea0447 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:48:14 +0200
Subject: [PATCH 16/22] docs: add status badges to the readme

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index 4ba0e13..d539c25 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
 # updater-python
 
 [![Latest Release](https://img.shields.io/github/v/release/SemRels/updater-python?label=version&color=blue)](https://github.com/SemRels/updater-python/releases/latest)
+[![CI](https://github.com/SemRels/updater-python/actions/workflows/ci.yml/badge.svg)](https://github.com/SemRels/updater-python/actions/workflows/ci.yml)
+[![License](https://img.shields.io/github/license/SemRels/updater-python)](LICENSE)
+[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/SemRels/updater-python/badge)](https://scorecard.dev/viewer/?uri=github.com/SemRels/updater-python)
+[![REUSE status](https://api.reuse.software/badge/github.com/SemRels/updater-python)](https://api.reuse.software/info/github.com/SemRels/updater-python)
 
 Updates the Python package version in the selected packaging backend.
 

From 053c62ec67efcf3a38dc9630195b8f4d160d71da Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:57:35 +0200
Subject: [PATCH 17/22] ci: pin workflow actions to the digests used by
 plugin-template

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .github/workflows/binaries.yml       |  2 +-
 .github/workflows/ci.yml             |  2 +-
 .github/workflows/container.yml      | 10 +++++-----
 .github/workflows/scorecard.yml      |  4 ++--
 .github/workflows/semrel-release.yml |  2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml
index 14571c2..821d955 100644
--- a/.github/workflows/binaries.yml
+++ b/.github/workflows/binaries.yml
@@ -28,7 +28,7 @@ jobs:
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
 
       - name: Install tools with Mise
-        uses: jdx/mise-action@v5
+        uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5
 
       - name: Build release binary
         run: task build:all
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eb80eb2..020cc5e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
 
       - name: Install tools with Mise
-        uses: jdx/mise-action@v5
+        uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5
 
       - name: Download dependencies
         run: go mod download
diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml
index c89102e..6945262 100644
--- a/.github/workflows/container.yml
+++ b/.github/workflows/container.yml
@@ -35,7 +35,7 @@ jobs:
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
 
       - name: Install tools with Mise
-        uses: jdx/mise-action@v5
+        uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5
 
       - name: Set version metadata
         id: version
@@ -74,14 +74,14 @@ jobs:
             type=raw,value=${{ steps.version.outputs.version_short }},enable=true
 
       - name: Set up Docker QEMU
-        uses: docker/setup-qemu-action@v4
+        uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
 
       - name: Set up Docker Buildx
         id: buildx
         uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
 
       - name: Log in to GitHub Container Registry
-        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v3
+        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
@@ -89,7 +89,7 @@ jobs:
 
       - name: Build and push multiarch image
         id: build
-        uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v6
+        uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
         with:
           builder: ${{ steps.buildx.outputs.name }}
           context: .
@@ -115,7 +115,7 @@ jobs:
 
       - name: Run Trivy vulnerability scanner
         if: github.event_name != 'pull_request'
-        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.30.0
+        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
         with:
           image-ref: ghcr.io/${{ steps.repo.outputs.lower }}:${{ steps.version.outputs.version_short }}
           format: table
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index b6501fb..cfea0f0 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -28,7 +28,7 @@ jobs:
         with:
           persist-credentials: false
 
-      - uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
+      - uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
         with:
           results_file: results.sarif
           results_format: sarif
@@ -40,7 +40,7 @@ jobs:
           path: results.sarif
           retention-days: 5
 
-      - uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
+      - uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4
         with:
           sarif_file: results.sarif
 
diff --git a/.github/workflows/semrel-release.yml b/.github/workflows/semrel-release.yml
index e321fde..9948776 100644
--- a/.github/workflows/semrel-release.yml
+++ b/.github/workflows/semrel-release.yml
@@ -35,7 +35,7 @@ jobs:
           token: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Install tools with Mise
-        uses: jdx/mise-action@v5
+        uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5
 
       - name: Configure git identity
         run: |

From d6243833a30d54d9078f0e9077156f36fc96c4b6 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:57:35 +0200
Subject: [PATCH 18/22] chore: pin Dockerfile base images to digests

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 Dockerfile | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 8afac5b..36f2af4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
 # SPDX-FileCopyrightText: 2026 The semrel Authors
 
 # ── build stage ────────────────────────────────────────────────────────────────
-FROM --platform=$BUILDPLATFORM debian:13-slim AS build
+FROM --platform=$BUILDPLATFORM debian:13-slim@sha256:62bd423aeee912639ca8ee681c2b762cb3e25137e0a92b20be455898a9f034ad AS build
 
 RUN apt-get update && \
     apt-get -y --no-install-recommends install ca-certificates curl git build-essential && \
@@ -38,7 +38,7 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
     task build
 
 # ── distroless release image ───────────────────────────────────────────────────
-FROM gcr.io/distroless/static-debian13:nonroot
+FROM gcr.io/distroless/static-debian13:nonroot@sha256:516757106de35205efb70b91ba03f6f1298424a0678c603255cf2852a7000087
 
 COPY --from=build /src/bin/plugin /usr/local/bin/plugin
 USER nonroot

From f796a1fc7bfdf94c7e5180fc0801384060448f57 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 16:59:33 +0200
Subject: [PATCH 19/22] docs: document plugin usage with uses, phase and args

Replace the local binary path in the configuration examples with the registry name (`uses: "@semrel/"`), set the phase the plugin runs in, and configure it through `args`. The options table now lists each argument next to its `SEMREL_PLUGIN_*` environment variable, which can be used instead of `args`.

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 README.md | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index d539c25..0d5ccdf 100644
--- a/README.md
+++ b/README.md
@@ -39,19 +39,21 @@ cosign verify ghcr.io/semrels/updater-python:latest \
 
 ```yaml
 plugins:
-  - name: updater-python
-    path: ~/.semrel/plugins/semrel-plugin-updater-python
-    env:
-      SEMREL_PLUGIN_FILE: "pyproject.toml"
-      SEMREL_PLUGIN_BACKEND: "pyproject"
+  - uses: "@semrel/updater-python"
+    phase: pre-tag
+    args:
+      file: "pyproject.toml"
+      backend: "pyproject"
 ```
 
-## `SEMREL_PLUGIN_*` variables
+## Options
 
-| Name | Required | Description | Default |
-| --- | --- | --- | --- |
-| `SEMREL_PLUGIN_FILE` | Optional | Path to the Python packaging file to update. | pyproject.toml |
-| `SEMREL_PLUGIN_BACKEND` | Optional | Backend format used to locate and update the version. | pyproject |
+Every option can be set as an entry under `args` in `.semrel.yaml`. semrel passes each entry to the plugin as the environment variable shown in the table, so you can also set that variable directly (for example in your CI environment) instead of using `args`.
+
+| Argument | Environment variable | Required | Description | Default |
+| --- | --- | --- | --- | --- |
+| `file` | `SEMREL_PLUGIN_FILE` | Optional | Path to the Python packaging file to update. | pyproject.toml |
+| `backend` | `SEMREL_PLUGIN_BACKEND` | Optional | Backend format used to locate and update the version. | pyproject |
 
 ## `SEMREL_*` release context used
 

From 036c997f973ad1f8b78c66e2e6c6a9147b4aac71 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 18:23:15 +0200
Subject: [PATCH 20/22] ci: add more allowed rules to semrel config

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .semrel.yaml | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.semrel.yaml b/.semrel.yaml
index b16fd66..9c67d07 100644
--- a/.semrel.yaml
+++ b/.semrel.yaml
@@ -27,6 +27,12 @@ rules:
       bump: patch
     - type: revert
       bump: patch
+    - type: major
+      bump: major
+    - type: minor
+      bump: minor
+    - type: patch
+      bump: patch
     - type: deps
       bump: patch
 

From 42ac29e52498d4438752efa0f8ffe4714d8a8787 Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 19:00:59 +0200
Subject: [PATCH 21/22] ci(tools): update github/codeql-action digest to
 9b32540

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 .github/workflows/scorecard.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index cfea0f0..fb1f0ba 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -40,7 +40,7 @@ jobs:
           path: results.sarif
           retention-days: 5
 
-      - uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4
+      - uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
         with:
           sarif_file: results.sarif
 

From 4282eb2eb76e3a57b0549248b1b7b2cdd1df2a8d Mon Sep 17 00:00:00 2001
From: Thomas Boerger 
Date: Wed, 21 Oct 2026 19:00:59 +0200
Subject: [PATCH 22/22] deps(docker): update
 gcr.io/distroless/static-debian13:nonroot digest to d620678

Co-Authored-By: Claude Sonnet 5.5 
Signed-off-by: Thomas Boerger 
---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index 36f2af4..553c02b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -38,7 +38,7 @@ RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
     task build
 
 # ── distroless release image ───────────────────────────────────────────────────
-FROM gcr.io/distroless/static-debian13:nonroot@sha256:516757106de35205efb70b91ba03f6f1298424a0678c603255cf2852a7000087
+FROM gcr.io/distroless/static-debian13:nonroot@sha256:d62067879de890a1def9da0e7766b7d20810a5e5555cef8506b752437d7a3e88
 
 COPY --from=build /src/bin/plugin /usr/local/bin/plugin
 USER nonroot