chore(release): carry 1.0.0.6 through repo.json and the issue template #57
Workflow file for this run
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: Release Aetherphone | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' | |
| # Manual fallback: when auto-tag pushes a tag via GITHUB_TOKEN, GitHub | |
| # sometimes refuses to cascade-trigger this workflow. Dispatch on the tag | |
| # ref to publish the release without having to delete and re-push the tag. | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to release (e.g. v1.0.0.0). Defaults to the ref this is dispatched on.' | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| jobs: | |
| release: | |
| runs-on: windows-latest | |
| # Job-level so the hub-ping step's `if` can see it; `secrets` can't be used | |
| # directly in an `if:` condition, but `env` mapped from a secret can. | |
| env: | |
| HUB_REPO_PAT: ${{ secrets.HUB_REPO_PAT }} | |
| steps: | |
| - name: Resolve target tag | |
| id: tag | |
| shell: bash | |
| run: | | |
| ref_name="${{ github.ref_name }}" | |
| input_tag="${{ inputs.tag }}" | |
| if [ -n "$input_tag" ]; then | |
| target="$input_tag" | |
| else | |
| target="$ref_name" | |
| fi | |
| if [[ ! "$target" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then | |
| echo "::error::Resolved target '$target' is not a v*.*.* tag; pass the tag input or dispatch on a tag ref." | |
| exit 1 | |
| fi | |
| echo "target=$target" >> "$GITHUB_OUTPUT" | |
| echo "version=${target#v}" >> "$GITHUB_OUTPUT" | |
| echo "Releasing $target" | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ steps.tag.outputs.target }} | |
| fetch-depth: 0 | |
| # PAT so the repo.json bump below can land on the protected default | |
| # branch: master requires the Guards and Build (Windows) checks, which | |
| # a freshly pushed commit cannot have yet, and github-actions[bot] gets | |
| # no bypass. The PAT owner is an admin and enforce_admins is off. | |
| token: ${{ secrets.HUB_REPO_PAT || secrets.GITHUB_TOKEN }} | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Download Dalamud | |
| shell: pwsh | |
| run: | | |
| $dalamudUrl = "https://goatcorp.github.io/dalamud-distrib/latest.zip" | |
| $dalamudDir = "$env:AppData\XIVLauncher\addon\Hooks\dev" | |
| New-Item -ItemType Directory -Force -Path $dalamudDir | Out-Null | |
| Invoke-WebRequest -Uri $dalamudUrl -OutFile dalamud.zip | |
| Expand-Archive -Path dalamud.zip -DestinationPath $dalamudDir -Force | |
| Remove-Item dalamud.zip | |
| - name: Restore (locked) | |
| run: dotnet restore Aetherphone.sln --locked-mode | |
| - name: Build (Release) | |
| run: dotnet build Aetherphone.sln --configuration Release --no-restore | |
| - name: Stage latest.zip | |
| shell: pwsh | |
| run: | | |
| $src = 'src/Aetherphone/bin/Release/Aetherphone/latest.zip' | |
| if (-not (Test-Path $src)) { throw "SDK-built plugin zip not found at $src" } | |
| Copy-Item $src 'latest.zip' -Force | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.target }} | |
| name: ${{ steps.tag.outputs.target }} | |
| files: latest.zip | |
| generate_release_notes: true | |
| fail_on_unmatched_files: true | |
| - name: Fetch total download count | |
| id: count | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| total=$(gh api "repos/${{ github.repository }}/releases" --paginate \ | |
| --jq '[.[].assets[].download_count] | add // 0') | |
| echo "total=$total" >> "$GITHUB_OUTPUT" | |
| echo "Total downloads across all releases: $total" | |
| - name: Announce the release on Discord | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| WEBHOOK_URL: ${{ secrets.RELEASE_WEBHOOK_URL }} | |
| PING_ROLE_ID: ${{ vars.RELEASE_PING_ROLE_ID }} | |
| TAG: ${{ steps.tag.outputs.target }} | |
| DOWNLOADS: ${{ steps.count.outputs.total }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| if ([string]::IsNullOrWhiteSpace($env:WEBHOOK_URL)) { | |
| Write-Host 'RELEASE_WEBHOOK_URL not set; skipping the announcement.' | |
| exit 0 | |
| } | |
| $icon = 'https://raw.githubusercontent.com/XeldarAlz/FFXIV-Aetherphone/master/src/Aetherphone/Images/Icon.png' | |
| $relList = "https://github.com/$($env:GH_REPO)/releases" | |
| $rel = gh release view $env:TAG --json name,tagName,body,url,isPrerelease,publishedAt | ConvertFrom-Json | |
| $title = if ([string]::IsNullOrEmpty($rel.name)) { $rel.tagName } else { $rel.name } | |
| $desc = if ($null -eq $rel.body) { '' } else { [string]$rel.body } | |
| $desc = $desc.Replace("`r", '').Replace([char]0x2014, '-').Replace([char]0x2013, '-') | |
| if ($desc.Length -gt 3800) { $desc = $desc.Substring(0, 3800) + "`n`n...full notes on GitHub" } | |
| $isPre = [bool]$rel.isPrerelease | |
| $content = if ($isPre) { '🧪 **New preview build just dropped!**' } else { '🚀 **New version dropped!**' } | |
| $pingRole = $env:PING_ROLE_ID | |
| if (-not [string]::IsNullOrWhiteSpace($pingRole)) { | |
| $content = "<@&$pingRole> $content" | |
| } | |
| $color = if ($isPre) { 10197915 } else { 9132790 } | |
| $channel = if ($isPre) { 'Preview' } else { 'Stable' } | |
| $fields = @( | |
| [ordered]@{ name = 'Version'; value = '`' + $rel.tagName + '`'; inline = $true } | |
| [ordered]@{ name = 'Channel'; value = $channel; inline = $true } | |
| ) | |
| if (-not [string]::IsNullOrWhiteSpace($env:DOWNLOADS)) { | |
| $fields += [ordered]@{ name = 'Total installs'; value = "$($env:DOWNLOADS)"; inline = $true } | |
| } | |
| $payload = [ordered]@{ | |
| username = 'Aetherphone' | |
| avatar_url = $icon | |
| content = $content | |
| embeds = @( | |
| [ordered]@{ | |
| author = [ordered]@{ name = 'Aetherphone'; url = $relList; icon_url = $icon } | |
| title = $title | |
| url = $rel.url | |
| description = $desc | |
| color = $color | |
| fields = $fields | |
| thumbnail = [ordered]@{ url = $icon } | |
| footer = [ordered]@{ text = 'Aetherphone'; icon_url = $icon } | |
| timestamp = $rel.publishedAt | |
| } | |
| ) | |
| } | |
| if (-not [string]::IsNullOrWhiteSpace($pingRole)) { | |
| $payload['allowed_mentions'] = [ordered]@{ roles = @($pingRole) } | |
| } | |
| $json = $payload | ConvertTo-Json -Depth 10 | |
| if ($json.Contains([char]0x2014) -or $json.Contains([char]0x2013)) { throw 'Em/en dash detected in payload' } | |
| $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) | |
| $resp = Invoke-WebRequest -Uri $env:WEBHOOK_URL -Method Post -Body $bytes -ContentType 'application/json; charset=utf-8' -UseBasicParsing | |
| Write-Host "Discord responded: $($resp.StatusCode) $($resp.StatusDescription) for $($env:TAG)" | |
| - name: Update local repo.json | |
| shell: pwsh | |
| run: | | |
| $v = '${{ steps.tag.outputs.version }}' | |
| $path = 'repo.json' | |
| $json = Get-Content -Raw -Path $path | ConvertFrom-Json | |
| $json[0].AssemblyVersion = $v | |
| $json[0].TestingAssemblyVersion = $v | |
| $json[0].DownloadCount = [int]'${{ steps.count.outputs.total }}' | |
| $json[0] | Add-Member -NotePropertyName 'LastUpdate' -NotePropertyValue ([string][int][double]::Parse((Get-Date -UFormat %s))) -Force | |
| # -AsArray preserves the array wrapper; piping a single-element array through | unrolls it and loses the [ ... ] Dalamud requires. | |
| ConvertTo-Json -InputObject $json -Depth 10 -AsArray | Set-Content -Path $path -Encoding utf8 | |
| - name: Commit local repo.json | |
| shell: pwsh | |
| run: | | |
| git config user.name 'github-actions[bot]' | |
| git config user.email 'github-actions[bot]@users.noreply.github.com' | |
| git add repo.json | |
| if (-not (git status --porcelain)) { | |
| Write-Host 'No changes to commit.' | |
| exit 0 | |
| } | |
| git commit -m "Release ${{ steps.tag.outputs.target }}: bump repo.json" | |
| # -X theirs auto-resolves any repo.json conflict in favour of our just-bumped values | |
| # (rebase semantics: "theirs" = the commits being rebased, i.e. ours). | |
| # Without it, a concurrent push that touched repo.json leaves conflict markers, | |
| # push then reports "Everything up-to-date" with exit 0, and downstream steps | |
| # silently consume the corrupted file. | |
| for ($i = 1; $i -le 5; $i++) { | |
| git fetch origin master | |
| git rebase -X theirs origin/master | |
| if ($LASTEXITCODE -ne 0) { | |
| git rebase --abort 2>$null | |
| Write-Host "Rebase failed (attempt $i); retrying..." | |
| Start-Sleep -Seconds 2 | |
| continue | |
| } | |
| git push origin HEAD:master | |
| if ($LASTEXITCODE -eq 0) { exit 0 } | |
| Write-Host "Push rejected (attempt $i); retrying after rebase..." | |
| Start-Sleep -Seconds 2 | |
| } | |
| Write-Error 'Failed to push repo.json bump after 5 attempts.' | |
| exit 1 | |
| # Ping the multi-plugin hub repo so it regenerates repo.json immediately | |
| # instead of waiting for its hourly cron. The hub workflow re-reads each | |
| # plugin's repo.json and refreshes DownloadCount from the GitHub API. | |
| - name: Trigger hub repo regeneration | |
| if: ${{ env.HUB_REPO_PAT != '' }} | |
| shell: bash | |
| run: | | |
| curl -fsSL -X POST \ | |
| -H "Authorization: Bearer $HUB_REPO_PAT" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| https://api.github.com/repos/XeldarAlz/DalamudPlugins/dispatches \ | |
| -d '{"event_type":"plugin-released","client_payload":{"plugin":"${{ github.event.repository.name }}","version":"${{ steps.tag.outputs.version }}"}}' |