Workflows können nicht nur Code testen und deployen – sie können auch Code ändern. Automatisches Code-Formatting, Version-Bumps, Changelog-Generierung: All das sind Operationen, die Workflows ausführen und zurück ins Repository committen können.
Dieses Kapitel zeigt, wie Workflows zum aktiven Teilnehmer im Repository werden. Wir behandeln Git-Operationen aus Workflows, Semantic Versioning mit automatischen Version-Bumps, Conventional Commits als Basis für Changelog-Generierung und die Integration all dieser Komponenten in einen Release-Workflow.
Am Ende haben wir einen vollautomatischen Release-Prozess für badge-gen: Code wird formatiert, Version wird gebumpt, Changelog wird generiert, GitHub Release wird erstellt – alles durch einen Workflow.
Um Dateien zu ändern und zu committen, braucht der Workflow Schreibrechte:
jobs:
auto-format:
runs-on: ubuntu-latest
permissions:
contents: write # Erlaubt git push
steps:
# ...Wichtig: Ohne contents: write schlägt
git push mit Permission Denied fehl.
Der einfachste Ansatz: Standard-Git-Befehle im Workflow.
- name: Configure Git
run: |
git config user.name "GitHub Actions Bot"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Format code
run: |
black src/
ruff check --fix .
- name: Commit changes
run: |
git add .
git commit -m "style: auto-format code" || echo "No changes to commit"
git pushProblem: Was, wenn keine Änderungen vorliegen?
git commit schlägt fehl → Workflow bricht ab.
Lösung: || echo "No changes" fängt
Exit-Code 1 ab. Aber: Workflow zeigt trotzdem als “erfolgreich” an,
obwohl kein Commit passiert ist.
stefanzweifel/git-auto-commit-action – der De-facto-Standard für Auto-Commits:
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-format code"
file_pattern: "*.py *.md"Was die Action macht:
git status)git add + git commit +
git pushOutput:
- uses: stefanzweifel/git-auto-commit-action@v5
id: auto-commit
with:
commit_message: "style: auto-format code"
- name: Check if committed
run: |
if [[ "${{ steps.auto-commit.outputs.changes_detected }}" == "true" ]]; then
echo "Changes were committed"
else
echo "No changes detected"
fiProblem: Workflow wird durch Push getriggert → macht Änderung → pusht → triggert Workflow → …
GitHub’s eingebauter Schutz: Commits mit
GITHUB_TOKEN triggern keine weiteren
Workflows.
Exception: Wenn ein Workflow einen anderen Workflow explizit triggern soll (z.B. Deployment nach Auto-Format), braucht man einen Personal Access Token (PAT).
- uses: actions/checkout@v4
with:
token: ${{ secrets.PAT }} # Statt GITHUB_TOKEN
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-format code"Commits mit PAT triggern Workflows. Daher: [skip ci] im Commit-Message nutzen:
commit_message: "style: auto-format code [skip ci]"[skip ci] (oder [ci skip]) verhindert
Workflow-Trigger. Siehe auch: skip-checks: true im
Commit-Body.
Use-Case 1: Auto-Format auf PRs
on:
pull_request:
branches: [main]
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }} # PR-Branch auschecken
- name: Format code
run: black src/
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-format code"Wichtig: ref: ${{ github.head_ref }}
checkt den PR-Branch aus (nicht den Merge-Commit). Ohne das würde der
Commit auf einem detached HEAD landen.
Use-Case 2: Auto-Format auf main-Branch
on:
push:
branches: [main]
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Format code
run: black src/
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-format code [skip ci]"Hier ist [skip ci] kritisch – sonst
endlos-Loop.
name: Auto-Format
on:
pull_request:
push:
branches: [main, develop]
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref }}
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install formatters
run: pip install black ruff
- name: Format Python code
run: |
black src/ tests/
ruff check --fix src/ tests/
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-format with black and ruff"
file_pattern: "*.py"Workflow:
Semantic Versioning (Major.Minor.Patch) braucht Informationen über Art der Änderung. Conventional Commits kodiert diese Information im Commit-Message.
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Beispiele:
feat: add SVG export functionality
fix: correct badge width calculation
docs: update installation instructions
refactor: simplify color parsing logic
perf: optimize image rendering
test: add tests for badge validation
build: update dependencies
ci: add caching to workflow
chore: update .gitignore
Scopes (optional):
feat(api): add REST endpoint for badge generation
fix(cli): handle missing config file gracefully
docs(readme): add usage examples
Breaking Changes:
feat!: redesign CLI interface
BREAKING CHANGE: --output flag renamed to --out
| Commit Type | Version Bump |
|---|---|
fix:, perf: |
Patch (0.0.X) |
feat: |
Minor (0.X.0) |
BREAKING CHANGE: oder ! |
Major (X.0.0) |
docs:, style:, test:,
chore: |
Keine |
Beispiel:
v1.2.3 (aktuell)
↓
feat: add new badge style
↓
v1.3.0 (Minor bump)
↓
fix: correct color parsing
↓
v1.3.1 (Patch bump)
↓
feat!: redesign API
↓
v2.0.0 (Major bump)
1. Commitizen – Interaktiver Commit-Helper
npm install -g commitizen cz-conventional-changelog
# In Repository
echo '{ "path": "cz-conventional-changelog" }' > .czrc
# Statt git commit
git czCommitizen führt durch einen Wizard:
? Select the type of change: (Use arrow keys)
❯ feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code
refactor: A code change that neither fixes a bug nor adds a feature
perf: A code change that improves performance
test: Adding missing tests
2. commitlint – Commit-Message-Linting
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# .commitlintrc.js
module.exports = {
extends: ['@commitlint/config-conventional']
};
# Pre-commit Hook (Husky)
npx husky add .husky/commit-msg 'npx commitlint --edit $1'Effekt: Commit wird abgelehnt, wenn Message nicht dem Standard folgt.
git commit -m "added feature"
# ⧗ input: added feature
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]3. In GitHub Actions validieren
- uses: wagoid/commitlint-github-action@v6Prüft alle Commits im PR gegen Conventional Commits.
Prinzip: Letzter Tag + Commits seit Tag = Nächste Version
paulhatch/semantic-version – Berechnet Version, erstellt aber keinen Tag:
- uses: paulhatch/semantic-version@v5.4.0
id: version
with:
tag_prefix: "v"
major_pattern: "(BREAKING CHANGE:|!:)"
minor_pattern: "feat:"
version_format: "${major}.${minor}.${patch}"
- name: Show version
run: echo "Next version: ${{ steps.version.outputs.version }}"Outputs:
version: Die berechnete Version (z.B.
1.3.0)version_tag: Version mit Prefix (z.B.
v1.3.0)increment: Art des Bumps (major,
minor, patch, none)Use-Case: Version in Build einbetten (Docker-Tag, Package-Metadata) vor tatsächlichem Tag.
Problem: Version soll in Artefakt eingebaut werden, aber Tag erst nach erfolgreichem Build.
Lösung:
jobs:
calculate-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.semver.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Alle Tags brauchen
- uses: paulhatch/semantic-version@v5.4.0
id: semver
with:
tag_prefix: "v"
major_pattern: "BREAKING CHANGE:"
minor_pattern: "feat:"
build:
needs: calculate-version
runs-on: ubuntu-latest
steps:
- name: Build with version
run: |
VERSION=${{ needs.calculate-version.outputs.version }}
echo "Building version $VERSION"
# Docker build, Package build, etc.
tag:
needs: [calculate-version, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create tag
run: |
VERSION=${{ needs.calculate-version.outputs.version }}
git tag "v$VERSION"
git push origin "v$VERSION"Workflow:
Use-Case: pyproject.toml,
package.json, Cargo.toml mit Version
updaten.
Manuell:
- name: Update version in pyproject.toml
run: |
VERSION=${{ steps.semver.outputs.version }}
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.tomlMit Action (TriPSs/conventional-changelog-action):
- uses: TriPSs/conventional-changelog-action@v5
with:
github-token: ${{ github.token }}
version-file: './pyproject.toml'
version-path: 'tool.poetry.version'Findet automatisch die Version-Zeile in pyproject.toml
und updatet sie.
requarks/changelog-action – Generiert Changelog aus Commit-Messages:
- uses: requarks/changelog-action@v1
id: changelog
with:
token: ${{ github.token }}
fromTag: v1.2.0
toTag: v1.3.0
excludeTypes: "build,chore"Output:
${{ steps.changelog.outputs.changes }}
## Features
- feat: add SVG export (#42)
- feat(api): new REST endpoint (#45)
## Bug Fixes
- fix: correct badge width (#44)
- fix(cli): handle missing config (#46)Eigenes Template:
- uses: requarks/changelog-action@v1
with:
token: ${{ github.token }}
tag: ${{ github.ref_name }}
writeToFile: trueCHANGELOG.md wird aktualisiert:
# Changelog
## [1.3.0] - 2025-01-15
### Features
- Add SVG export functionality
- New REST API endpoint
### Bug Fixes
- Correct badge width calculation
- Handle missing config file gracefully
## [1.2.0] - 2024-12-10
...TriPSs/conventional-changelog-action – Macht alles: Version bump, Changelog, Tag, Commit:
- uses: TriPSs/conventional-changelog-action@v5
id: changelog
with:
github-token: ${{ github.token }}
output-file: "CHANGELOG.md"
skip-version-file: false
version-file: "pyproject.toml"
version-path: "tool.poetry.version"
git-message: "chore(release): {version}"
tag-prefix: "v"Was passiert:
pyproject.toml mit neuer VersionCHANGELOG.mdOutputs:
version: Neue Versiontag: Neuer Tagchangelog: Generierter Changelog-Textskipped: true wenn keine
Version-relevanten Commits- uses: TriPSs/conventional-changelog-action@v5
id: changelog
with:
github-token: ${{ github.token }}
- uses: ncipollo/release-action@v1
if: ${{ steps.changelog.outputs.skipped == 'false' }}
with:
token: ${{ github.token }}
tag: ${{ steps.changelog.outputs.tag }}
name: Release ${{ steps.changelog.outputs.tag }}
body: ${{ steps.changelog.outputs.clean_changelog }}Kombinieren wir alles für badge-gen.
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Für Tag-History
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# Tests ausführen
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -e .[dev]
- name: Run tests
run: pytest
# Version bump + Changelog
- uses: TriPSs/conventional-changelog-action@v5
id: changelog
with:
github-token: ${{ github.token }}
output-file: "CHANGELOG.md"
skip-version-file: false
version-file: "pyproject.toml"
version-path: "tool.poetry.version"
git-message: "chore(release): {version} [skip ci]"
tag-prefix: "v"
preset: "conventionalcommits"
# Build nur wenn Version gebumpt wurde
- name: Build distribution
if: ${{ steps.changelog.outputs.skipped == 'false' }}
run: |
pip install build
python -m build
# GitHub Release mit Artifacts
- name: Create GitHub Release
if: ${{ steps.changelog.outputs.skipped == 'false' }}
uses: ncipollo/release-action@v1
with:
token: ${{ github.token }}
tag: ${{ steps.changelog.outputs.tag }}
name: Release ${{ steps.changelog.outputs.tag }}
body: ${{ steps.changelog.outputs.clean_changelog }}
artifacts: "dist/*"
draft: false
prerelease: falseSzenario 1: Commits mit feat: oder
fix:
conventional-changelog-action analysiert Commits:
feat: add new badge style → Minor bump1.2.31.3.0pyproject.toml wird updated:
version = "1.3.0"CHANGELOG.md wird generiert/updated"chore(release): 1.3.0 [skip ci]"v1.3.0 wird erstelltdist/badge-gen-1.3.0.tar.gz)Szenario 2: Commits nur mit docs: oder
chore:
conventional-changelog-action analysiert Commits:
skipped: trueon:
push:
branches: [main, develop]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# ... Tests ...
- uses: TriPSs/conventional-changelog-action@v5
id: changelog
with:
github-token: ${{ github.token }}
output-file: "CHANGELOG.md"
version-file: "pyproject.toml"
version-path: "tool.poetry.version"
git-message: "chore(release): {version} [skip ci]"
tag-prefix: "v"
preset: "conventionalcommits"
pre-release: ${{ github.ref != 'refs/heads/main' }}
pre-release-identifier: "beta"
# ... Build + Release ...
- name: Create GitHub Release
if: ${{ steps.changelog.outputs.skipped == 'false' }}
uses: ncipollo/release-action@v1
with:
token: ${{ github.token }}
tag: ${{ steps.changelog.outputs.tag }}
name: ${{ github.ref == 'refs/heads/main' && 'Release' || 'Pre-Release' }} ${{ steps.changelog.outputs.tag }}
body: ${{ steps.changelog.outputs.clean_changelog }}
artifacts: "dist/*"
prerelease: ${{ github.ref != 'refs/heads/main' }}Versioning: - main: v1.3.0,
v1.3.1, v2.0.0 - develop:
v1.3.0-beta.1, v1.3.0-beta.2,
v1.3.1-beta.1
Symptom: Zwei Workflows laufen gleichzeitig, beide versuchen dieselbe Version zu erstellen.
Lösung: Concurrency-Control
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false # Wichtig: false, nicht trueWichtig: cancel-in-progress: false
bedeutet: Zweiter Run wartet bis erster fertig ist. So
wird sichergestellt, dass Versionen sequenziell gebumpt werden.
Symptom: Branch Protection Rule verlangt Review → Auto-Commit schlägt fehl.
Lösung 1: Bot-User aus Branch Protection ausnehmen
Settings → Branches → Edit Rule → ☐ Include administrators
Lösung 2: GitHub App statt GITHUB_TOKEN
GitHub Apps können Branch Protection Rules umgehen (mit korrekten Permissions).
- uses: tibdex/github-app-token@v2
id: generate-token
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: TriPSs/conventional-changelog-action@v5
with:
github-token: ${{ steps.generate-token.outputs.token }}Symptom: Obwohl [skip ci] im Message,
laufen Workflows weiter.
Ursache: [skip ci] funktioniert nur für
denselben Workflow. Andere Workflows ignorieren es.
Lösung: if Condition in anderen
Workflows
jobs:
test:
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-latest
# ...Symptom: version-path findet Version
nicht in pyproject.toml.
Beispiel pyproject.toml:
[tool.poetry]
name = "badge-gen"
version = "1.2.3"Korrekte version-path:
version-file: "pyproject.toml"
version-path: "tool.poetry.version"Für package.json:
version-file: "package.json"
version-path: "version"Für Cargo.toml:
version-file: "Cargo.toml"
version-path: "package.version"Lokale Validierung verhindert, dass Non-Conventional Commits ins Repository kommen.
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1Team-Setup:
{
"scripts": {
"prepare": "husky install"
},
"devDependencies": {
"husky": "^8.0.0",
"@commitlint/cli": "^18.0.0",
"@commitlint/config-conventional": "^18.0.0"
}
}Nach npm install sind Hooks automatisch aktiv.
Für Projekte mit hohem Release-Aufkommen: semantic-release
- uses: cycjimmy/semantic-release-action@v4
with:
branches: |
[
'main',
{
name: 'beta',
prerelease: true
}
]
env:
GITHUB_TOKEN: ${{ github.token }}semantic-release macht alles:
Nachteile:
Wann nutzen:
| Ansatz | Version Bump | Changelog | Tag | Release | Komplexität |
|---|---|---|---|---|---|
| Manuell | ✋ Hand | ✋ Hand | ✋ Hand | ✋ Hand | 😊 Niedrig |
| Semi-Auto | 🤖 Auto | ✋ Hand | 🤖 Auto | ✋ Hand | 😐 Mittel |
| Conventional Changelog Action | 🤖 Auto | 🤖 Auto | 🤖 Auto | ✋ Hand | 🙂 Mittel |
| Semantic Release | 🤖 Auto | 🤖 Auto | 🤖 Auto | 🤖 Auto | 🤔 Hoch |
Empfehlung für badge-gen: Conventional Changelog Action (Sweet Spot: viel Automation, wenig Komplexität).
Risiko: PR von Fork → Workflow läuft mit
GITHUB_TOKEN → kann Code committen → Malicious Code
injection.
Mitigation: pull_request_target NICHT
verwenden für Auto-Commits auf PRs von Forks.
Sicher:
on:
pull_request: # Nur für PRs im eigenen RepoUnsicher:
on:
pull_request_target: # Läuft auch für Forks mit Secrets!GITHUB_TOKEN: Nur contents: write wenn
nötig. Nicht workflow-level, sondern job-level:
jobs:
release:
permissions:
contents: write
test:
permissions:
contents: read # Nur lesenGitHub Actions Bot Commits sind nicht GPG-signiert. Für höhere Sicherheit:
- uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Commit with signature
run: |
git commit -S -m "chore(release): $VERSION"Commits erscheinen als “Verified” auf GitHub.