name: Release on: push: tags: - 'v*' concurrency: group: release-${{ github.ref_name }} cancel-in-progress: false jobs: release: runs-on: ubuntu-latest env: DOTNET_ROOT: /home/mika/.dotnet GITEA_API: https://git.kuns.dev/api/v1 REPO: releases/ClaudeDo steps: - name: Resolve version id: ver run: | set -euo pipefail TAG="${{ github.ref_name }}" VERSION="${TAG#v}" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Building version: $VERSION (tag: $TAG)" - name: Prepare workspace id: ws run: | set -euo pipefail WORK="$(mktemp -d -t claudedo-release-XXXXXX)" echo "dir=$WORK" >> "$GITHUB_OUTPUT" echo "Workspace: $WORK" - name: Checkout tag env: TOKEN: ${{ secrets.GITEA_TOKEN }} WORK: ${{ steps.ws.outputs.dir }} TAG: ${{ steps.ver.outputs.tag }} run: | set -euo pipefail # Full clone (with tags) so release notes can diff against the previous tag. git clone --branch "$TAG" \ "https://oauth2:${TOKEN}@git.kuns.dev/${REPO}.git" \ "$WORK/src" git -C "$WORK/src" log -1 --oneline - name: Generate release notes env: WORK: ${{ steps.ws.outputs.dir }} TAG: ${{ steps.ver.outputs.tag }} run: | set -euo pipefail cd "$WORK/src" PREV="$(git tag --sort=v:refname | grep -E '^v' \ | awk -v t="$TAG" '$0==t{print prev} {prev=$0}')" if [ -n "$PREV" ]; then RANGE="${PREV}..${TAG}" else RANGE="$TAG" fi emit_group() { # $1 conventional-type, $2 heading local lines lines="$(git log "$RANGE" --no-merges --pretty=format:'%s|%h' \ | grep -E "^${1}(\([^)]*\))?(!)?: " || true)" [ -z "$lines" ] && return 0 printf '### %s\n\n' "$2" while IFS='|' read -r subject hash; do printf -- '- %s (%s)\n' "${subject#*: }" "$hash" done <<< "$lines" printf '\n' } { emit_group feat "Features" emit_group fix "Fixes" emit_group perf "Performance" emit_group refactor "Refactoring" emit_group docs "Documentation" } > RELEASE_NOTES.md echo "--- release notes ---" cat RELEASE_NOTES.md - name: Publish ClaudeDo.App (win-x64, self-contained) env: WORK: ${{ steps.ws.outputs.dir }} VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail export PATH="$DOTNET_ROOT:$PATH" cd "$WORK/src" dotnet publish src/ClaudeDo.App/ClaudeDo.App.csproj \ -c Release -r win-x64 --self-contained true \ /p:MinVerVersionOverride=$VERSION -o out/app - name: Publish ClaudeDo.Worker (win-x64, self-contained) env: WORK: ${{ steps.ws.outputs.dir }} VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail export PATH="$DOTNET_ROOT:$PATH" cd "$WORK/src" dotnet publish src/ClaudeDo.Worker/ClaudeDo.Worker.csproj \ -c Release -r win-x64 --self-contained true \ /p:MinVerVersionOverride=$VERSION -o out/worker - name: Publish ClaudeDo.Installer (win-x64, single-file, framework-dependent) env: WORK: ${{ steps.ws.outputs.dir }} VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail export PATH="$DOTNET_ROOT:$PATH" cd "$WORK/src" # Framework-dependent — WPF runtime pack isn't distributed on Linux SDK; # the previous self-contained bundle crashed at startup (apphost AV). # Target machines need .NET 8 Desktop Runtime (x64). dotnet publish src/ClaudeDo.Installer/ClaudeDo.Installer.csproj \ -c Release -r win-x64 --self-contained false \ /p:MinVerVersionOverride=$VERSION /p:PublishSingleFile=true \ -o out/installer - name: Package assets env: WORK: ${{ steps.ws.outputs.dir }} VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail cd "$WORK/src" mkdir -p assets # 1) App + Worker bundle (top-level dirs /app and /worker) rm -rf bundle mkdir -p bundle cp -r out/app bundle/app cp -r out/worker bundle/worker ZIP_NAME="ClaudeDo-${VERSION}-win-x64.zip" ( cd bundle && zip -r -q "../assets/${ZIP_NAME}" app worker ) # 2) Installer single-file exe (renamed) INSTALLER_EXE=$(ls out/installer/*.exe | head -n 1) if [ -z "$INSTALLER_EXE" ]; then echo "::error::No .exe produced by installer publish" >&2 exit 1 fi cp "$INSTALLER_EXE" "assets/ClaudeDo.Installer-${VERSION}.exe" # 3) Checksums (sha256, relative filenames) ( cd assets && sha256sum \ "ClaudeDo-${VERSION}-win-x64.zip" \ "ClaudeDo.Installer-${VERSION}.exe" \ > checksums.txt ) echo "--- assets ---" ls -la assets - name: Create Gitea Release id: release env: WORK: ${{ steps.ws.outputs.dir }} TAG: ${{ steps.ver.outputs.tag }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail BODY=$(jq -n \ --arg tag "$TAG" \ --arg name "$TAG" \ --rawfile body "$WORK/src/RELEASE_NOTES.md" \ '{tag_name:$tag, name:$name, body:$body, draft:true, prerelease:false, target_commitish:"main"}') RESP=$(curl -sS -X POST \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ -d "$BODY" \ "${GITEA_API}/repos/${REPO}/releases") RELEASE_ID=$(echo "$RESP" | jq -r '.id // empty') if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then echo "::error::Release creation failed" >&2 echo "$RESP" >&2 exit 1 fi echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT" echo "Created release id=$RELEASE_ID for tag=$TAG" - name: Upload release assets env: WORK: ${{ steps.ws.outputs.dir }} VERSION: ${{ steps.ver.outputs.version }} RELEASE_ID: ${{ steps.release.outputs.release_id }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail cd "$WORK/src/assets" for f in \ "ClaudeDo-${VERSION}-win-x64.zip" \ "ClaudeDo.Installer-${VERSION}.exe" \ "checksums.txt" do echo "Uploading: $f" curl -sS --fail-with-body -X POST \ -H "Authorization: token ${TOKEN}" \ -F "attachment=@${f}" \ "${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${f}" \ > /dev/null done echo "All assets uploaded." - name: Publish release env: RELEASE_ID: ${{ steps.release.outputs.release_id }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail curl -sS --fail-with-body -X PATCH \ -H "Authorization: token ${TOKEN}" \ -H "Content-Type: application/json" \ -d '{"draft":false}' \ "${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}" \ > /dev/null echo "Release ${RELEASE_ID} published." - name: Delete draft release on failure if: failure() && steps.release.outputs.release_id != '' env: RELEASE_ID: ${{ steps.release.outputs.release_id }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | curl -sS -X DELETE \ -H "Authorization: token ${TOKEN}" \ "${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}" \ > /dev/null || true echo "Cleaned up draft release ${RELEASE_ID}." - name: Cleanup workspace if: always() env: WORK: ${{ steps.ws.outputs.dir }} run: | rm -rf "$WORK" || true