Create Release #43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Create Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| rc_tag: | |
| description: 'The Release Candidate tag to use as a base (e.g., v0.1.0-rc.1)' | |
| required: true | |
| type: string | |
| jobs: | |
| build_and_release: | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| final_version_tag: ${{ steps.create_final_tag.outputs.final_version_tag }} | |
| final_version_number: ${{ steps.extract_final_version.outputs.final_version_number }} | |
| steps: | |
| - name: Get RC Tag Input | |
| id: get_rc_tag_input | |
| shell: bash | |
| run: echo "rc_tag_name=${{ github.event.inputs.rc_tag }}" >> $GITHUB_OUTPUT | |
| - name: Validate RC Tag Format | |
| id: validate_rc_tag | |
| shell: bash | |
| run: | | |
| rc_tag="${{ steps.get_rc_tag_input.outputs.rc_tag_name }}" | |
| if [[ ! "$rc_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then | |
| echo "::error::Invalid RC tag format: $rc_tag. Expected format: vX.Y.Z-rc.N" | |
| exit 1 | |
| fi | |
| echo "RC tag $rc_tag is valid." | |
| - name: Extract Final Version from RC Tag | |
| id: extract_final_version | |
| shell: bash | |
| run: | | |
| rc_tag="${{ steps.get_rc_tag_input.outputs.rc_tag_name }}" | |
| final_version_tag=$(echo "$rc_tag" | sed -E 's/-rc\.[0-9]+$//') | |
| final_version_number=$(echo "$final_version_tag" | sed 's/^v//') | |
| echo "final_version_tag=$final_version_tag" >> $GITHUB_OUTPUT | |
| echo "final_version_number=$final_version_number" >> $GITHUB_OUTPUT | |
| echo "Extracted final version tag: $final_version_tag" | |
| echo "Extracted final version number: $final_version_number" | |
| - name: Checkout Code at RC Tag | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.PAT_TOKEN }} | |
| ref: ${{ steps.get_rc_tag_input.outputs.rc_tag_name }} # Checkout the specific RC tag | |
| submodules: recursive | |
| fetch-depth: 0 # Fetch all history for all tags and branches | |
| - name: Aggregate All RC Release Notes for this Version | |
| id: aggregate_all_rc_notes # New ID, update downstream references if needed | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} # Required for gh release view | |
| shell: bash | |
| run: | | |
| echo "Ensuring all tags are available locally..." | |
| # It's highly recommended to use fetch-depth: 0 in your main actions/checkout step. | |
| # This fetch is a safeguard. | |
| git fetch --tags --force --quiet | |
| EFFECTIVE_RC_TAG_FOR_BASE_DERIVATION="${{ steps.get_rc_tag_input.outputs.rc_tag_name }}" | |
| echo "Effective RC Tag (used to derive base version): $EFFECTIVE_RC_TAG_FOR_BASE_DERIVATION" | |
| # Extract base version (e.g., v1.2.3 from v1.2.3-rc.4) | |
| if [[ "$EFFECTIVE_RC_TAG_FOR_BASE_DERIVATION" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+)(-rc\.[0-9]+)?$ ]]; then | |
| BASE_VERSION="${BASH_REMATCH[1]}" | |
| else | |
| echo "::error::Could not determine base version from EFFECTIVE_RC_TAG_FOR_BASE_DERIVATION: '$EFFECTIVE_RC_TAG_FOR_BASE_DERIVATION'. Expected format like vX.Y.Z-rc.N or vX.Y.Z." | |
| exit 1 | |
| fi | |
| echo "Base version determined: $BASE_VERSION" | |
| RC_TAG_PATTERN="${BASE_VERSION}-rc.*" | |
| echo "Searching for RC tags matching pattern: $RC_TAG_PATTERN" | |
| # List all RC tags for this base version, sorted by version number. | |
| # grep ensures we only get tags like vX.Y.Z-rc.NUM and not vX.Y.Z-rc.somethingelse | |
| ALL_RC_TAGS_FOR_BASE=$(git tag --list "$RC_TAG_PATTERN" | grep -E "^${BASE_VERSION}-rc\.[0-9]+$" | sort -V) | |
| if [ -z "$ALL_RC_TAGS_FOR_BASE" ]; then | |
| echo "::warning::No RC tags found for base version $BASE_VERSION (pattern: $RC_TAG_PATTERN)." | |
| DELIMITER_EMPTY=$(openssl rand -hex 16) | |
| { | |
| echo "full_release_notes<<$DELIMITER_EMPTY" | |
| echo "No release candidate tags found for version $BASE_VERSION to aggregate notes from." | |
| echo "$DELIMITER_EMPTY" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 # Exit successfully with a message, rather than failing the workflow. | |
| fi | |
| echo "Found the following RC tags for base version $BASE_VERSION to aggregate notes from:" | |
| echo "$ALL_RC_TAGS_FOR_BASE" | |
| COMBINED_NOTES_CONTENT="This release includes updates from the following release candidates for version $BASE_VERSION:\n" | |
| # Append a markdown list of RC tags to the header | |
| while IFS= read -r rc_tag_item; do | |
| COMBINED_NOTES_CONTENT+="\n- **${rc_tag_item}**" | |
| done <<< "$ALL_RC_TAGS_FOR_BASE" | |
| COMBINED_NOTES_CONTENT+="\n\n---\n" # Separator before detailed notes | |
| RC_NOTES_CONTENT_ACCUMULATED="" | |
| while IFS= read -r rc_tag_item; do | |
| echo "Fetching release notes for $rc_tag_item..." | |
| # Attempt to fetch release body. If 'gh release view' fails or body is empty, notes_for_tag will be empty. | |
| # Errors from 'gh' are sent to /dev/null to prevent them from breaking the JSON parsing if notes are missing. | |
| notes_for_tag=$(gh release view "$rc_tag_item" --json body --jq '.body' 2>/dev/null || true) | |
| if [ -n "$notes_for_tag" ]; then | |
| RC_NOTES_CONTENT_ACCUMULATED+="## Notes from ${rc_tag_item}\n\n${notes_for_tag}\n\n---\n" | |
| echo "Successfully fetched and appended notes for $rc_tag_item." | |
| else | |
| echo "::warning::No release notes body found or release does not exist for tag $rc_tag_item. Including a placeholder." | |
| RC_NOTES_CONTENT_ACCUMULATED+="## Notes from ${rc_tag_item}\n\n_No release notes body found for this tag._\n\n---\n" | |
| fi | |
| done <<< "$ALL_RC_TAGS_FOR_BASE" | |
| COMBINED_NOTES_CONTENT+="$RC_NOTES_CONTENT_ACCUMULATED" | |
| echo "Aggregated release notes successfully." | |
| # Output the combined notes using a delimiter for multiline support | |
| DELIMITER=$(openssl rand -hex 16) | |
| { | |
| echo "full_release_notes<<$DELIMITER" | |
| echo -e "$COMBINED_NOTES_CONTENT" # -e interprets backslashes for newlines | |
| echo "$DELIMITER" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Generate Final Release Notes with AI | |
| id: generate_final_release_notes | |
| shell: bash | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| # Pass the aggregated notes from the previous step | |
| AGGREGATED_RC_NOTES: ${{ steps.aggregate_all_rc_notes.outputs.full_release_notes }} | |
| run: | | |
| echo "ℹ️ Preparing to generate final release notes using AI..." | |
| echo "Input (Aggregated RC Notes - first 500 chars):" | |
| echo "${AGGREGATED_RC_NOTES:0:500}..." | |
| echo "" | |
| # Ensure jq is available (GitHub Actions runners usually have it) | |
| if ! command -v jq &> /dev/null | |
| then | |
| echo "::error::jq command could not be found. Please ensure jq is installed on the runner." | |
| exit 1 | |
| fi | |
| echo "ℹ️ Using jq from path: $(which jq)" | |
| echo "ℹ️ jq version: $(jq --version)" | |
| echo "" | |
| # Define system prompt for summarizing combined RC notes | |
| SYSTEM_PROMPT="You are a helpful assistant who creates user-friendly release notes for open-source applications like games and clip recorders. Your task is to transform a collection of release notes from several release candidates (RCs) for the same version into a single, cohesive, and easy-to-understand final release note. Merge similar changes, prioritize user-facing updates, and maintain a casual, direct tone. Avoid overly technical jargon and details not relevant to the end-user. Do not include items like 'chore' or 'refactor' if they don't offer direct user value. The final output should be the release note itself, without any introductory phrases like 'Release Notes:' or concluding remarks like 'Thanks.'. Ensure the output is clean and ready for direct use." | |
| echo "🔧 Escaping content..." | |
| # Use printf for system prompt to handle potential special characters better before piping to jq | |
| ESCAPED_SYSTEM=$(printf "%s" "$SYSTEM_PROMPT" | jq -Rs .) | |
| ESCAPED_INPUT_NOTES=$(printf "%s" "$AGGREGATED_RC_NOTES" | jq -Rs .) | |
| echo "🔒 Escaped System Prompt (first 100 chars): ${ESCAPED_SYSTEM:0:100}..." | |
| echo "🔒 Escaped Input Notes (first 100 chars): ${ESCAPED_INPUT_NOTES:0:100}..." | |
| echo "" | |
| # Build JSON payload | |
| JSON_PAYLOAD=$(jq -n \ | |
| --arg model "gpt-4o" \ | |
| --argjson system_prompt_content "$ESCAPED_SYSTEM" \ | |
| --argjson user_content "$ESCAPED_INPUT_NOTES" \ | |
| '{ | |
| "model": $model, | |
| "messages": [ | |
| {"role": "system", "content": $system_prompt_content}, | |
| {"role": "user", "content": $user_content} | |
| ] | |
| }') | |
| echo "📦 JSON Payload (first 300 chars):" | |
| echo "${JSON_PAYLOAD:0:300}..." | |
| echo "" | |
| echo "📡 Making API request to OpenAI..." | |
| RESPONSE=$(curl -s -X POST "https://api.openai.com/v1/chat/completions" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer $OPENAI_API_KEY" \ | |
| -d "$JSON_PAYLOAD") | |
| echo "📨 Raw API Response (first 300 chars):" | |
| echo "${RESPONSE:0:300}..." | |
| echo "" | |
| # Extract release notes, check for errors or empty response | |
| AI_GENERATED_NOTES=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ""') # Fallback to empty string if path is null | |
| if [ -z "$AI_GENERATED_NOTES" ] || [ "$AI_GENERATED_NOTES" == "null" ] || echo "$RESPONSE" | jq -e '.error' > /dev/null; then | |
| echo "::warning::AI summarization failed or returned empty/error response." | |
| if echo "$RESPONSE" | jq -e '.error' > /dev/null; then | |
| echo "API Error details: $(echo "$RESPONSE" | jq -c '.error')" | |
| fi | |
| else | |
| echo "🎉 Successfully generated release notes with AI." | |
| echo "📝 Final Release Notes:" | |
| echo "$AI_GENERATED_NOTES" | |
| echo "" | |
| fi | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: latest | |
| - name: Configure Git User | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| shell: pwsh | |
| - name: Update Frontend package.json with Final Version | |
| id: update_frontend_version | |
| working-directory: ./Frontend | |
| shell: pwsh | |
| run: | | |
| $finalVersion = "${{ steps.extract_final_version.outputs.final_version_number }}" | |
| Write-Host "Updating Frontend package.json to version: $finalVersion" | |
| $packageJson = Get-Content package.json -Raw | ConvertFrom-Json | |
| $packageJson.version = $finalVersion | |
| $packageJson | ConvertTo-Json -Depth 100 | Set-Content package.json -NoNewline | |
| Write-Host "Updated Frontend version in package.json: $(Select-String -Path package.json -Pattern 'version')" | |
| - name: Install Frontend dependencies | |
| working-directory: ./Frontend | |
| run: bun install --frozen-lockfile | |
| - name: Build Frontend | |
| working-directory: ./Frontend | |
| run: bun run build | |
| - name: Move Frontend Build to Resources/wwwroot | |
| run: | | |
| $source = "./Frontend/dist" | |
| $destination = "./Resources/wwwroot" | |
| if (-Not (Test-Path -Path $destination)) { New-Item -ItemType Directory -Path $destination -Force } | |
| Remove-Item -Path "$destination\*" -Recurse -Force | |
| Copy-Item -Path "$source\*" -Destination $destination -Recurse -Force | |
| shell: pwsh | |
| - name: Install vpk as .NET Global Tool | |
| run: | | |
| dotnet tool install -g vpk | |
| $env:PATH += ";$env:USERPROFILE\.dotnet\tools" | |
| shell: pwsh | |
| - name: Publish the App | |
| run: dotnet publish -c Release --self-contained -r win-x64 -o publish | |
| shell: pwsh | |
| - name: Package with vpk for Final Release | |
| run: vpk pack -u Segra -v ${{ steps.extract_final_version.outputs.final_version_number }} -p ./publish -e Segra.exe -o ./output --packTitle "Segra" --noPortable | |
| shell: pwsh | |
| - name: Create Final Git Tag | |
| id: create_final_tag | |
| shell: pwsh | |
| env: | |
| GH_PAT: ${{ secrets.PAT_TOKEN }} | |
| run: | | |
| $final_tag = "${{ steps.extract_final_version.outputs.final_version_tag }}" | |
| Write-Host "Creating final Git tag: $final_tag" | |
| # Override the default remote to use the PAT | |
| git remote set-url origin https://x-access-token:${env:GH_PAT}@github.com/${{ github.repository }}.git | |
| git tag $final_tag | |
| git push origin $final_tag | |
| echo "final_version_tag=$final_tag" >> $env:GITHUB_OUTPUT | |
| Write-Host "Final Git tag $final_tag created and pushed." | |
| - name: Upload Final Release Artifact (for release job) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: Segra-Final-Build | |
| path: ./output | |
| retention-days: 1 | |
| create_github_release: | |
| needs: build_and_release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # Required to create a release | |
| steps: | |
| - name: Download Final Build Artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: Segra-Final-Build | |
| - name: Create Final GitHub Release | |
| id: create_final_gh_release | |
| uses: actions/create-release@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: ${{ needs.build_and_release.outputs.final_version_tag }} | |
| release_name: "Release ${{ needs.build_and_release.outputs.final_version_tag }}" | |
| body: "" | |
| draft: false | |
| prerelease: false # This is a final release | |
| - name: Upload Setup File to Final Release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create_final_gh_release.outputs.upload_url }} | |
| asset_path: ./Segra-win-Setup.exe | |
| asset_name: Segra-win-Setup.exe | |
| asset_content_type: application/octet-stream | |
| - name: Upload RELEASES File to Final Release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create_final_gh_release.outputs.upload_url }} | |
| asset_path: ./RELEASES | |
| asset_name: RELEASES | |
| asset_content_type: text/plain | |
| - name: Upload releases.win.json to Final Release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create_final_gh_release.outputs.upload_url }} | |
| asset_path: ./releases.win.json | |
| asset_name: releases.win.json | |
| asset_content_type: application/json | |
| - name: Upload assets.win.json to Final Release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create_final_gh_release.outputs.upload_url }} | |
| asset_path: ./assets.win.json | |
| asset_name: assets.win.json | |
| asset_content_type: application/json | |
| - name: Upload nupkg to Final Release | |
| uses: actions/upload-release-asset@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| upload_url: ${{ steps.create_final_gh_release.outputs.upload_url }} | |
| asset_path: ./Segra-${{ needs.build_and_release.outputs.final_version_number }}-full.nupkg | |
| asset_name: Segra-${{ needs.build_and_release.outputs.final_version_number }}-full.nupkg | |
| asset_content_type: application/octet-stream |