This commit is contained in:
parent
8c64d9261c
commit
75f6a9f756
4 changed files with 333 additions and 60 deletions
176
.forgejo/actions/version/action.yml
Normal file
176
.forgejo/actions/version/action.yml
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
name: Calculate version
|
||||||
|
description: Validate Conventional Commits and calculate the build version
|
||||||
|
inputs:
|
||||||
|
commit-types:
|
||||||
|
description: Comma-separated Conventional Commit types
|
||||||
|
required: false
|
||||||
|
default: feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert
|
||||||
|
outputs:
|
||||||
|
version:
|
||||||
|
description: Version for dotnet build and pack
|
||||||
|
value: ${{ steps.version.outputs.version }}
|
||||||
|
base-version:
|
||||||
|
description: Version before the current changes
|
||||||
|
value: ${{ steps.version.outputs.base-version }}
|
||||||
|
label:
|
||||||
|
description: Build label, such as next or pr.42
|
||||||
|
value: ${{ steps.version.outputs.label }}
|
||||||
|
metadata:
|
||||||
|
description: Build metadata, such as sha.abc123
|
||||||
|
value: ${{ steps.version.outputs.metadata }}
|
||||||
|
bump:
|
||||||
|
description: Highest version increment found
|
||||||
|
value: ${{ steps.version.outputs.bump }}
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- id: version
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
VERSION_EVENT_NAME: ${{ github.event_name }}
|
||||||
|
VERSION_REF_NAME: ${{ github.ref_name }}
|
||||||
|
VERSION_BASE_REF: ${{ github.base_ref }}
|
||||||
|
VERSION_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
VERSION_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
VERSION_COMMIT_TYPES: ${{ inputs.commit-types }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo "::error::$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v git >/dev/null || error "git is required"
|
||||||
|
|
||||||
|
event_name="${VERSION_EVENT_NAME:-${GITHUB_EVENT_NAME:-}}"
|
||||||
|
ref_name="${VERSION_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||||
|
base_ref="${VERSION_BASE_REF:-${GITHUB_BASE_REF:-}}"
|
||||||
|
head_sha="${VERSION_HEAD_SHA:-${GITHUB_SHA:-HEAD}}"
|
||||||
|
pr_number="${VERSION_PR_NUMBER:-${PR_NUMBER:-}}"
|
||||||
|
commit_types="${VERSION_COMMIT_TYPES:-feat,fix,docs,style,refactor,perf,test,build,ci,chore,revert}"
|
||||||
|
type_pattern="${commit_types//,/|}"
|
||||||
|
output_file="${GITHUB_OUTPUT:-}"
|
||||||
|
|
||||||
|
write_output() {
|
||||||
|
[[ -n "$output_file" ]] && printf '%s=%s\n' "$1" "$2" >>"$output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_version() {
|
||||||
|
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
tag_version() {
|
||||||
|
valid_version "$1" || error "Tag '$1' is not a valid release version (expected MAJOR.MINOR.PATCH)"
|
||||||
|
printf '%s' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_release_tag() {
|
||||||
|
git tag --list '[0-9]*' --sort=-version:refname | head -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
version_parts() {
|
||||||
|
IFS=. read -r major minor patch <<<"$1"
|
||||||
|
printf '%s %s %s' "$major" "$minor" "$patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_version() {
|
||||||
|
local version=$1 bump=$2 major minor patch
|
||||||
|
read -r major minor patch <<<"$(version_parts "$version")"
|
||||||
|
case "$bump" in
|
||||||
|
major) major=$((major + 1)); minor=0; patch=0 ;;
|
||||||
|
minor) minor=$((minor + 1)); patch=0 ;;
|
||||||
|
patch) patch=$((patch + 1)) ;;
|
||||||
|
none) ;;
|
||||||
|
*) error "Unknown version bump '$bump'" ;;
|
||||||
|
esac
|
||||||
|
printf '%s.%s.%s' "$major" "$minor" "$patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
commit_bump() {
|
||||||
|
local commit=$1 validate=${2:-1} subject body bump=none type
|
||||||
|
subject=$(git log -1 --format=%s "$commit")
|
||||||
|
body=$(git log -1 --format=%b "$commit")
|
||||||
|
|
||||||
|
if ! [[ "$subject" =~ ^($type_pattern)(\([^()]+\))?(!)?:[[:space:]]+[^[:space:]].*$ ]]; then
|
||||||
|
[[ "$validate" == 0 ]] && { printf none; return; }
|
||||||
|
error "Commit $commit has an invalid Conventional Commit subject: $subject"
|
||||||
|
fi
|
||||||
|
type=${BASH_REMATCH[1]}
|
||||||
|
|
||||||
|
if [[ "$subject" =~ ^[^:]+\!: ]] || [[ "$body" == *"BREAKING CHANGE:"* ]] || [[ "$body" == *"BREAKING-CHANGE:"* ]]; then
|
||||||
|
bump=major
|
||||||
|
elif [[ "$type" == feat ]]; then
|
||||||
|
bump=minor
|
||||||
|
elif [[ "$type" == fix ]]; then
|
||||||
|
bump='patch'
|
||||||
|
fi
|
||||||
|
printf '%s' "$bump"
|
||||||
|
}
|
||||||
|
|
||||||
|
higher_bump() {
|
||||||
|
case "$1:$2" in
|
||||||
|
major:*) printf major ;; *:major) printf major ;;
|
||||||
|
minor:minor|minor:patch|patch:minor) printf minor ;;
|
||||||
|
patch:patch) printf patch ;;
|
||||||
|
none:*) printf '%s' "$2" ;;
|
||||||
|
*:none) printf '%s' "$1" ;;
|
||||||
|
*) error "Cannot compare bumps '$1' and '$2'" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
commits_between() {
|
||||||
|
git rev-list --reverse --no-merges "$1..$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate_bump() {
|
||||||
|
local commits=$1 validate=${2:-1} commit bump=none current
|
||||||
|
while IFS= read -r commit; do
|
||||||
|
[[ -n "$commit" ]] || continue
|
||||||
|
current=$(commit_bump "$commit" "$validate")
|
||||||
|
bump=$(higher_bump "$bump" "$current")
|
||||||
|
done <<<"$commits"
|
||||||
|
printf '%s' "$bump"
|
||||||
|
}
|
||||||
|
|
||||||
|
tag=$(latest_release_tag || true)
|
||||||
|
if [[ -n "$tag" ]]; then
|
||||||
|
base_version=$(tag_version "$tag")
|
||||||
|
tag_start=$tag
|
||||||
|
else
|
||||||
|
base_version=0.0.0
|
||||||
|
tag_start=$(git rev-list --max-parents=0 HEAD | tail -n 1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$event_name" == pull_request || -n "$base_ref" ]]; then
|
||||||
|
[[ -n "$pr_number" ]] || error "Pull request number is required"
|
||||||
|
[[ -n "$base_ref" ]] || error "Pull request base ref is required"
|
||||||
|
base_commit=$(git rev-parse "$base_ref" 2>/dev/null || git rev-parse "origin/$base_ref")
|
||||||
|
head_commit=$(git rev-parse "$head_sha")
|
||||||
|
pr_commits=$(commits_between "$base_commit" "$head_commit")
|
||||||
|
pr_bump=$(calculate_bump "$pr_commits")
|
||||||
|
base_bump=$(calculate_bump "$(commits_between "$tag_start" "$base_commit")" 0)
|
||||||
|
bump=$(higher_bump "$base_bump" "$pr_bump")
|
||||||
|
version=$(bump_version "$base_version" "$bump")-pr."$pr_number".sha."$head_commit"
|
||||||
|
label="pr.$pr_number"
|
||||||
|
metadata="sha.$head_commit"
|
||||||
|
elif [[ "$event_name" == push && "$ref_name" == [0-9]* || "$ref_name" == [0-9]* ]]; then
|
||||||
|
version=$(tag_version "$ref_name")
|
||||||
|
bump=none
|
||||||
|
label=release
|
||||||
|
metadata=none
|
||||||
|
else
|
||||||
|
head_commit=$(git rev-parse "$head_sha")
|
||||||
|
commits=$(commits_between "$tag_start" "$head_commit")
|
||||||
|
bump=$(calculate_bump "$commits")
|
||||||
|
version=$(bump_version "$base_version" "$bump")-next.sha."$head_commit"
|
||||||
|
label=next
|
||||||
|
metadata="sha.$head_commit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_output version "$version"
|
||||||
|
write_output base-version "$base_version"
|
||||||
|
write_output label "$label"
|
||||||
|
write_output metadata "$metadata"
|
||||||
|
write_output bump "$bump"
|
||||||
|
printf 'Calculated version: %s\n' "$version"
|
||||||
|
|
@ -3,12 +3,9 @@ name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
tags: ['*']
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
enable-openid-connect: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: build
|
name: build
|
||||||
|
|
@ -18,65 +15,15 @@ jobs:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: https://code.geekeey.de/actions/checkout@54953ce9d6c7d0a9cbd89d185c5b2c7515358c5f # 1
|
uses: https://code.geekeey.de/actions/checkout@54953ce9d6c7d0a9cbd89d185c5b2c7515358c5f # 1
|
||||||
|
|
||||||
|
- name: version
|
||||||
|
id: version
|
||||||
|
uses: ./.forgejo/actions/version
|
||||||
|
|
||||||
- name: dotnet format check
|
- name: dotnet format check
|
||||||
run: dotnet format arcade.slnx --verify-no-changes --verbosity minimal
|
run: dotnet format arcade.slnx --verify-no-changes --verbosity minimal
|
||||||
|
|
||||||
- name: dotnet pack
|
- name: dotnet build
|
||||||
shell: bash
|
run: dotnet build arcade.slnx -p:Version=${{ steps.version.outputs.version }}
|
||||||
env:
|
|
||||||
TAG: ${{ forgejo.ref }}
|
|
||||||
TAG_NAME: ${{ forgejo.ref_name }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ "$TAG" == refs/tags/* ]]; then
|
|
||||||
version="${TAG_NAME#v}"
|
|
||||||
if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
echo "::error::Tag '$TAG_NAME' is not a valid release version (expected MAJOR.MINOR.PATCH)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
export version
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet pack arcade.slnx
|
|
||||||
|
|
||||||
- name: upload packages
|
|
||||||
uses: https://code.geekeey.de/actions/artifacts/push@ae8d20e174a8706675e6d54ebd3719a2802ea9dd # 1
|
|
||||||
with:
|
|
||||||
name: nuget-packages
|
|
||||||
path: artifacts/package/release
|
|
||||||
pattern: |
|
|
||||||
*.nupkg
|
|
||||||
*.snupkg
|
|
||||||
|
|
||||||
- name: dotnet test
|
- name: dotnet test
|
||||||
run: dotnet test src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj
|
run: dotnet test arcade.slnx -p:Version=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
- name: publish to nuget
|
|
||||||
if: startsWith(forgejo.ref, 'refs/tags/')
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
AUDIENCE: "u:1:00000000-0000-0000-0000-000000000000"
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then
|
|
||||||
echo "::error::OpenID Connect is not enabled. Set enable-openid-connect: true on the workflow or job."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
response=$(curl --fail --silent --show-error \
|
|
||||||
--header "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${AUDIENCE}")
|
|
||||||
token=$(printf '%s' "${response}" | grep --only-matching '"value":"[^"]*"' | cut --delimiter '"' --fields 4)
|
|
||||||
|
|
||||||
if [[ -z "${token}" ]]; then
|
|
||||||
echo "::error::Failed to obtain an OpenID Connect token from Forgejo."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "::add-mask::${token}"
|
|
||||||
|
|
||||||
feed="${{ forgejo.server_url }}/api/packages/${{ forgejo.repository_owner }}/nuget/index.json"
|
|
||||||
|
|
||||||
dotnet nuget add source --name forgejo --username forgejo-publish --password "$token" --store-password-in-clear-text "$feed"
|
|
||||||
dotnet nuget push "artifacts/package/release/*.nupkg" "artifacts/package/release/*.snupkg" --source forgejo --skip-duplicate
|
|
||||||
|
|
|
||||||
60
.forgejo/workflows/release.yml
Normal file
60
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['*']
|
||||||
|
|
||||||
|
enable-openid-connect: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: publish
|
||||||
|
runs-on: debian-latest
|
||||||
|
container: mcr.microsoft.com/dotnet/sdk:10.0@sha256:0d99ebf1ccad15037b5c98c9fe8979d6b8dbac23812feae2277a0b277b5c5ffa
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: https://code.geekeey.de/actions/checkout@54953ce9d6c7d0a9cbd89d185c5b2c7515358c5f # 1
|
||||||
|
|
||||||
|
- name: version
|
||||||
|
id: version
|
||||||
|
uses: ./.forgejo/actions/version
|
||||||
|
|
||||||
|
- name: dotnet pack
|
||||||
|
run: dotnet pack arcade.slnx -p:Version=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
|
- name: upload packages
|
||||||
|
uses: https://code.geekeey.de/actions/artifacts/push@ae8d20e174a8706675e6d54ebd3719a2802ea9dd # 1
|
||||||
|
with:
|
||||||
|
name: nuget-packages
|
||||||
|
path: artifacts/package/release
|
||||||
|
pattern: |
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
|
||||||
|
- name: publish to nuget
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
AUDIENCE: "u:1:49f9431c-8baf-4432-8254-72fd1b00b8f9"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then
|
||||||
|
echo "::error::OpenID Connect is not enabled. Set enable-openid-connect: true on the workflow or job."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
response=$(curl --fail --silent --show-error \
|
||||||
|
--header "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${AUDIENCE}")
|
||||||
|
token=$(printf '%s' "${response}" | grep --only-matching '"value":"[^"]*"' | cut --delimiter '"' --fields 4)
|
||||||
|
|
||||||
|
if [[ -z "${token}" ]]; then
|
||||||
|
echo "::error::Failed to obtain an OpenID Connect token from Forgejo."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::add-mask::${token}"
|
||||||
|
|
||||||
|
feed="${{ forgejo.server_url }}/api/packages/${{ forgejo.repository_owner }}/nuget/index.json"
|
||||||
|
|
||||||
|
dotnet nuget add source --name forgejo --username forgejo-publish --password "$token" --store-password-in-clear-text "$feed"
|
||||||
|
dotnet nuget push "artifacts/package/release/*.nupkg" "artifacts/package/release/*.snupkg" --source forgejo --skip-duplicate
|
||||||
90
doc/versioning.md
Normal file
90
doc/versioning.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Versioning
|
||||||
|
|
||||||
|
Versions are driven by semantic commits and release tags. We do not infer a
|
||||||
|
release version from branch names or build history.
|
||||||
|
|
||||||
|
## Semantic commits
|
||||||
|
|
||||||
|
Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
for changes. The format is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
```
|
||||||
|
|
||||||
|
Common types are:
|
||||||
|
|
||||||
|
- `feat`: a new feature
|
||||||
|
- `fix`: a bug fix
|
||||||
|
- `docs`: documentation-only changes
|
||||||
|
- `style`: formatting or other non-functional changes
|
||||||
|
- `refactor`: code changes without a feature or fix
|
||||||
|
- `perf`: a performance improvement
|
||||||
|
- `test`: tests
|
||||||
|
- `build`: build or dependency changes
|
||||||
|
- `ci`: continuous-integration changes
|
||||||
|
- `chore`: maintenance work
|
||||||
|
- `revert`: a reverted commit
|
||||||
|
|
||||||
|
Scopes are optional and identify the affected area, for example:
|
||||||
|
`feat(api): add health endpoint`. Use a short, consistent, project-specific
|
||||||
|
scope when one is useful.
|
||||||
|
|
||||||
|
Append `!` after the type or scope for a breaking change, or include a
|
||||||
|
`BREAKING CHANGE:` footer. For example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
feat(api)!: replace authentication endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version increments
|
||||||
|
|
||||||
|
The next release version is incremented from the previous release using only
|
||||||
|
`fix`, `feat`, and breaking-change markers:
|
||||||
|
|
||||||
|
- **Major**: any commit marked with `!` or containing a `BREAKING CHANGE:`
|
||||||
|
footer
|
||||||
|
- **Minor**: otherwise, any `feat` commit
|
||||||
|
- **Patch**: otherwise, any `fix` commit
|
||||||
|
|
||||||
|
Other commit types do not increment the version. If multiple applicable
|
||||||
|
commits exist, the highest increment wins.
|
||||||
|
|
||||||
|
## Build metadata
|
||||||
|
|
||||||
|
Builds that are not releases carry metadata identifying their source:
|
||||||
|
|
||||||
|
- **Pull requests:** `pr.<number>.sha.<sha>`
|
||||||
|
- **Main branch:** `sha.<sha>`
|
||||||
|
|
||||||
|
The main branch is labelled `next` for the upcoming release.
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
A tag is the release. The version is taken directly from the tag; it is never
|
||||||
|
computed by the build system. Release tags must therefore contain the complete
|
||||||
|
release version (for example, `1.2.3`). The tag and the version exported to
|
||||||
|
the .NET CLI are both `1.2.3`; no `v` prefix is used.
|
||||||
|
|
||||||
|
## Version action
|
||||||
|
|
||||||
|
Version calculation and commit validation are provided by a reusable action. It
|
||||||
|
is written in Bash only and runs as the first step after checkout in each
|
||||||
|
workflow.
|
||||||
|
|
||||||
|
The action selects commits according to the workflow ref:
|
||||||
|
|
||||||
|
- **Pull requests:** commits belonging to the pull request only; commits
|
||||||
|
already present on the target branch are excluded.
|
||||||
|
- **Main:** commits since the previous release tag.
|
||||||
|
- **Release tags:** no version is computed; the tag version is used directly.
|
||||||
|
|
||||||
|
The action then:
|
||||||
|
|
||||||
|
1. validates the applicable commit messages against the Conventional Commits
|
||||||
|
format;
|
||||||
|
2. calculates the version from the previous release tag and the applicable
|
||||||
|
commits, unless the current ref is a release tag; and
|
||||||
|
3. exports the calculated version and its label/build metadata as action
|
||||||
|
outputs for later steps. Jobs are responsible for mapping those outputs to
|
||||||
|
environment variables when needed.
|
||||||
Loading…
Reference in a new issue