diff --git a/.github/workflows/pr-version-label.yml b/.github/workflows/pr-version-label.yml new file mode 100644 index 0000000..dec5c37 --- /dev/null +++ b/.github/workflows/pr-version-label.yml @@ -0,0 +1,36 @@ +name: PR version label + +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] + +permissions: + pull-requests: write + +jobs: + validate-version-label: + runs-on: ubuntu-latest + steps: + - name: Default to Tag/Patch on a brand-new PR + if: github.event.action == 'opened' + env: + GH_TOKEN: ${{ github.token }} + run: | + labels=$(gh pr view "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name') + if ! grep -qE '^Tag/(Patch|Minor|Major)$' <<< "$labels"; then + gh pr edit "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --add-label "Tag/Patch" + fi + + - name: Exactly one Tag/* label, or none at all + env: + GH_TOKEN: ${{ github.token }} + run: | + # Re-read labels rather than trusting the event payload -- the + # step above may have just changed them. + labels=$(gh pr view "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name') + count=$(grep -cE '^Tag/(Patch|Minor|Major)$' <<< "$labels" || true) + echo "Tag/* labels present: $count" + if [ "$count" -gt 1 ]; then + echo "::error::More than one Tag/Patch, Tag/Minor, Tag/Major label is present -- pick exactly one, or remove all of them to skip versioning this PR." + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..32253e8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: Version & Release + +# Tagging/release happens on merge, reading whichever Tag/* label the PR +# carried at that point -- not at label-add time. That's a deliberate +# choice: tagging the PR branch's head commit as soon as a label is +# added would (a) create an orphan tag if the PR is later closed +# unmerged, and (b) point at a commit SHA that doesn't survive a squash +# merge (main gets a brand-new commit). Using +# `pull_request.merge_commit_sha` from the `closed`+merged event instead +# is correct for merge, squash, and rebase alike, and never tags +# anything that didn't actually land on main. + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +jobs: + tag-and-release: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + + - name: Determine bump level from the merged PR's labels + id: bump + env: + GH_TOKEN: ${{ github.token }} + run: | + labels=$(gh pr view "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name') + matches=$(grep -cE '^Tag/(Patch|Minor|Major)$' <<< "$labels" || true) + + if [ "$matches" -eq 0 ]; then + echo "No Tag/* label on this PR -- skipping version bump and release." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "$matches" -gt 1 ]; then + echo "::error::More than one Tag/* label on the merged PR -- the validate-version-label required check should have blocked this. Refusing to guess which one wins." + exit 1 + fi + + if grep -qxE 'Tag/Major' <<< "$labels"; then + echo "level=major" >> "$GITHUB_OUTPUT" + elif grep -qxE 'Tag/Minor' <<< "$labels"; then + echo "level=minor" >> "$GITHUB_OUTPUT" + else + echo "level=patch" >> "$GITHUB_OUTPUT" + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + + - name: Compute next version + if: steps.bump.outputs.skip != 'true' + id: version + run: | + latest=$(git tag -l | { grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -t. -k1,1n -k2,2n -k3,3n | tail -1) + latest=${latest:-0.0.0} + IFS='.' read -r major minor patch <<< "$latest" + case "${{ steps.bump.outputs.level }}" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + *) echo "::error::unknown bump level '${{ steps.bump.outputs.level }}'"; exit 1 ;; + esac + new="$major.$minor.$patch" + echo "Bumping $latest -> $new (${{ steps.bump.outputs.level }})" + echo "new_version=$new" >> "$GITHUB_OUTPUT" + + - name: Create tag + release + if: steps.bump.outputs.skip != 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ steps.version.outputs.new_version }}" \ + --repo "${{ github.repository }}" \ + --target "${{ github.event.pull_request.merge_commit_sha }}" \ + --title "${{ steps.version.outputs.new_version }}" \ + --generate-notes diff --git a/LICENSE b/LICENSE index 41ba56f..581da2d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 InvForge contributors +Copyright (c) 2026 InvForge contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal