name: Release
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
workflow_dispatch:
inputs:
version:
description: 'Version to release (leave empty to use Cargo.toml version)'
required: false
type: string
env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
check-version:
name: Check Version
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
outputs:
version: ${{ steps.get_version.outputs.version }}
version_num: ${{ steps.get_version.outputs.version_num }}
should_release: ${{ steps.check_release.outputs.should_release }}
steps:
- uses: actions/checkout@v4
- name: Get version from Cargo.toml
id: get_version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION_NUM="${{ inputs.version }}"
else
VERSION_NUM=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
fi
echo "version=v${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "version_num=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "Version: v${VERSION_NUM}"
- name: Check if release exists
id: check_release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION=${{ steps.get_version.outputs.version }}
if gh release view "$VERSION" > /dev/null 2>&1; then
echo "Release $VERSION already exists, skipping"
echo "should_release=false" >> $GITHUB_OUTPUT
else
echo "Release $VERSION does not exist, proceeding"
echo "should_release=true" >> $GITHUB_OUTPUT
fi
build-release:
name: Build Release
needs: check-version
if: needs.check-version.outputs.should_release == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: undoc
archive_name: undoc-linux-x86_64
archive_ext: tar.gz
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: undoc.exe
archive_name: undoc-windows-x86_64
archive_ext: zip
- os: macos-latest
target: x86_64-apple-darwin
artifact_name: undoc
archive_name: undoc-macos-x86_64
archive_ext: tar.gz
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: undoc
archive_name: undoc-macos-aarch64
archive_ext: tar.gz
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }} -p undoc-cli
- name: Create archive (Unix)
if: matrix.archive_ext == 'tar.gz'
run: |
cd target/${{ matrix.target }}/release
tar -czvf ${{ matrix.archive_name }}-${{ needs.check-version.outputs.version }}.tar.gz ${{ matrix.artifact_name }}
- name: Create archive (Windows)
if: matrix.archive_ext == 'zip'
shell: pwsh
run: |
cd target/${{ matrix.target }}/release
Compress-Archive -Path ${{ matrix.artifact_name }} -DestinationPath "${{ matrix.archive_name }}-${{ needs.check-version.outputs.version }}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.archive_name }}
path: target/${{ matrix.target }}/release/${{ matrix.archive_name }}-${{ needs.check-version.outputs.version }}.${{ matrix.archive_ext }}
retention-days: 1
create-release:
name: Create Release
needs: [check-version, build-release]
if: needs.check-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: List artifacts
run: find artifacts -type f
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.check-version.outputs.version }}
name: Release ${{ needs.check-version.outputs.version }}
files: |
artifacts/**/*
generate_release_notes: true
draft: false
prerelease: ${{ contains(needs.check-version.outputs.version, 'alpha') || contains(needs.check-version.outputs.version, 'beta') || contains(needs.check-version.outputs.version, 'rc') }}
- name: Delete artifacts after release
uses: geekyeggo/delete-artifact@v5
with:
name: |
undoc-*
publish-crates:
name: Publish to crates.io
needs: [check-version, create-release]
if: needs.check-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Publish library (undoc)
run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
continue-on-error: true
- name: Wait for crates.io index update
run: sleep 30
- name: Publish CLI (undoc-cli)
run: cargo publish -p undoc-cli --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
continue-on-error: true
publish-npm:
name: Publish npm (@iyulab/undoc)
needs: [check-version, create-release]
if: needs.check-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-wasm-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-wasm-release-
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build WASM package
run: wasm-pack build undoc-wasm --target bundler --out-dir undoc-wasm/pkg
- name: Set scoped npm package name
run: |
jq '.name = "@iyulab/undoc"' undoc-wasm/pkg/package.json > /tmp/pkg.json
mv /tmp/pkg.json undoc-wasm/pkg/package.json
- name: Copy LICENSE into pkg
run: cp LICENSE undoc-wasm/pkg/
- name: Publish to npm
run: wasm-pack publish undoc-wasm/pkg --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
cleanup-old-releases:
name: Cleanup Old Releases
needs: [check-version, create-release]
if: needs.check-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Delete releases and tags beyond the 10 most recent
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail
# Fetch up to 100 recent releases. Sort by creation time desc and
# take everything AFTER index 10 — keep 10 newest, delete the rest.
# --cleanup-tag removes the underlying git tag too, so the repo's
# release list AND tag list stay bounded. Aligns with CLAUDE.md's
# GitHub Actions 리소스 관리 정책 (reduce storage use).
mapfile -t OLD < <(
gh release list -R "$REPO" --limit 100 --json tagName,createdAt \
| jq -r 'sort_by(.createdAt) | reverse | .[10:] | .[].tagName'
)
if [ ${#OLD[@]} -eq 0 ]; then
echo "Nothing to clean up (<=10 releases exist)."
exit 0
fi
echo "Deleting ${#OLD[@]} old release(s):"
printf ' %s\n' "${OLD[@]}"
for tag in "${OLD[@]}"; do
# --cleanup-tag deletes the git tag alongside the release.
# --yes skips the interactive confirmation.
gh release delete "$tag" --yes --cleanup-tag -R "$REPO"
done