diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..f3a71c0 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: 2025 RAprogramm +# +# SPDX-License-Identifier: MIT + +name: Auto Release + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + branches: + - main + +permissions: + contents: write + +jobs: + check-and-release: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install dependencies + shell: bash + run: | + if ! command -v jq >/dev/null 2>&1; then + sudo apt-get update -y && sudo apt-get install -y jq + fi + + - name: Get local version + id: local_version + shell: bash + run: | + VERSION=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version') + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Local version: ${VERSION}" + + - name: Get crates.io version + id: cratesio_version + shell: bash + run: | + CRATE_NAME="masterror" + CRATESIO_VER=$(curl -s "https://crates.io/api/v1/crates/${CRATE_NAME}" | jq -r '.crate.max_version // empty') + + if [ -z "${CRATESIO_VER}" ]; then + echo "Crate not found on crates.io" + CRATESIO_VER="0.0.0" + fi + + echo "version=${CRATESIO_VER}" >> "$GITHUB_OUTPUT" + echo "crates.io version: ${CRATESIO_VER}" + + - name: Compare versions + id: version_check + shell: bash + run: | + LOCAL="${{ steps.local_version.outputs.version }}" + REMOTE="${{ steps.cratesio_version.outputs.version }}" + + if [ "${LOCAL}" = "${REMOTE}" ]; then + echo "Versions are equal, skipping release" + echo "should_release=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Use semver comparison + TMPDIR=$(mktemp -d) + mkdir -p "${TMPDIR}/src" + + cat > "${TMPDIR}/src/main.rs" << 'EOF' + use std::env; + fn main() { + let args: Vec = env::args().collect(); + if args.len() != 3 { std::process::exit(1); } + let v1 = semver::Version::parse(&args[1]).unwrap(); + let v2 = semver::Version::parse(&args[2]).unwrap(); + if v1 > v2 { println!("greater"); } + else if v1 < v2 { println!("less"); } + else { println!("equal"); } + } + EOF + + cat > "${TMPDIR}/Cargo.toml" << 'EOF' + [package] + name = "semver-compare" + version = "0.1.0" + edition = "2024" + [dependencies] + semver = "1.0" + EOF + + COMPARISON=$(cd "${TMPDIR}" && cargo run --quiet -- "${LOCAL}" "${REMOTE}" 2>/dev/null || echo "unknown") + rm -rf "${TMPDIR}" + + if [ "${COMPARISON}" = "greater" ]; then + echo "Local version ${LOCAL} > crates.io ${REMOTE}, creating release" + echo "should_release=true" >> "$GITHUB_OUTPUT" + else + echo "Local version ${LOCAL} not greater than crates.io ${REMOTE}" + echo "should_release=false" >> "$GITHUB_OUTPUT" + fi + + - name: Get last release tag + id: last_tag + if: steps.version_check.outputs.should_release == 'true' + shell: bash + run: | + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "${LAST_TAG}" ]; then + # No tags, use first commit + LAST_TAG=$(git rev-list --max-parents=0 HEAD) + fi + echo "tag=${LAST_TAG}" >> "$GITHUB_OUTPUT" + echo "Last tag: ${LAST_TAG}" + + - name: Generate changelog + id: changelog + if: steps.version_check.outputs.should_release == 'true' + shell: bash + run: | + VERSION="${{ steps.local_version.outputs.version }}" + LAST_TAG="${{ steps.last_tag.outputs.tag }}" + + CHANGELOG_FILE=$(mktemp) + + echo "## Changes" >> "${CHANGELOG_FILE}" + echo "" >> "${CHANGELOG_FILE}" + + # Get commits since last tag, excluding [skip ci] and readme auto-refresh + COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%H|%s|%an" --no-merges | \ + grep -v "\[skip ci\]" | \ + grep -v "chore(readme): auto-refresh" || true) + + # Group commits by type + declare -A SECTIONS + SECTIONS=( + ["feat"]="### Features" + ["fix"]="### Bug Fixes" + ["perf"]="### Performance" + ["refactor"]="### Refactoring" + ["docs"]="### Documentation" + ["test"]="### Tests" + ["ci"]="### CI/CD" + ["chore"]="### Chore" + ) + + # Track which commits were categorized + CATEGORIZED_HASHES="" + + # Parse commits by type + for TYPE in feat fix perf refactor docs test ci chore; do + TYPED_COMMITS=$(echo "${COMMITS}" | grep -E "^\w+\|#?[0-9]* ${TYPE}:" || true) + + if [ -n "${TYPED_COMMITS}" ]; then + echo "" >> "${CHANGELOG_FILE}" + echo "${SECTIONS[$TYPE]}" >> "${CHANGELOG_FILE}" + echo "" >> "${CHANGELOG_FILE}" + + while IFS='|' read -r HASH SUBJECT AUTHOR; do + # Skip empty lines + [ -z "${HASH}" ] && continue + + # Mark as categorized + CATEGORIZED_HASHES="${CATEGORIZED_HASHES}${HASH}"$'\n' + + # Extract issue number if present + ISSUE=$(echo "${SUBJECT}" | grep -oE '#[0-9]+' | head -1 || echo "") + # Remove issue number and type prefix from subject + CLEAN_SUBJECT=$(echo "${SUBJECT}" | sed -E 's/^#?[0-9]* [a-z]+: //') + + SHORT_HASH=$(echo "${HASH}" | cut -c1-7) + COMMIT_LINK="[\`${SHORT_HASH}\`](https://github.com/${{ github.repository }}/commit/${HASH})" + + if [ -n "${ISSUE}" ]; then + ISSUE_LINK="([${ISSUE}](https://github.com/${{ github.repository }}/issues/${ISSUE#\#}))" + echo "- ${CLEAN_SUBJECT} ${ISSUE_LINK} ${COMMIT_LINK}" >> "${CHANGELOG_FILE}" + else + echo "- ${CLEAN_SUBJECT} ${COMMIT_LINK}" >> "${CHANGELOG_FILE}" + fi + done <<< "${TYPED_COMMITS}" + fi + done + + # Add uncategorized commits as "Other Changes" + UNCATEGORIZED="" + while IFS='|' read -r HASH SUBJECT AUTHOR; do + [ -z "${HASH}" ] && continue + if ! echo "${CATEGORIZED_HASHES}" | grep -q "${HASH}"; then + UNCATEGORIZED="${UNCATEGORIZED}${HASH}|${SUBJECT}|${AUTHOR}"$'\n' + fi + done <<< "${COMMITS}" + + if [ -n "${UNCATEGORIZED}" ]; then + echo "" >> "${CHANGELOG_FILE}" + echo "### Other Changes" >> "${CHANGELOG_FILE}" + echo "" >> "${CHANGELOG_FILE}" + + while IFS='|' read -r HASH SUBJECT AUTHOR; do + [ -z "${HASH}" ] && continue + SHORT_HASH=$(echo "${HASH}" | cut -c1-7) + COMMIT_LINK="[\`${SHORT_HASH}\`](https://github.com/${{ github.repository }}/commit/${HASH})" + echo "- ${SUBJECT} ${COMMIT_LINK}" >> "${CHANGELOG_FILE}" + done <<< "${UNCATEGORIZED}" + fi + + # Add comparison link + echo "" >> "${CHANGELOG_FILE}" + echo "---" >> "${CHANGELOG_FILE}" + echo "" >> "${CHANGELOG_FILE}" + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${LAST_TAG}...v${VERSION}" >> "${CHANGELOG_FILE}" + + # Output changelog + CHANGELOG_CONTENT=$(cat "${CHANGELOG_FILE}") + echo "changelog<> "$GITHUB_OUTPUT" + echo "${CHANGELOG_CONTENT}" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + rm -f "${CHANGELOG_FILE}" + + - name: Create and push tag + if: steps.version_check.outputs.should_release == 'true' + shell: bash + run: | + VERSION="${{ steps.local_version.outputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git tag -a "v${VERSION}" -m "Release v${VERSION}" + git push origin "v${VERSION}" + + - name: Create GitHub release + if: steps.version_check.outputs.should_release == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.local_version.outputs.version }} + name: v${{ steps.local_version.outputs.version }} + body: ${{ steps.changelog.outputs.changelog }} + draft: false + prerelease: false