chore: housekeeping — linter & formatter setup
Add .prettierrc.json, .markdownlint.json, .yamllint.yaml, .gitattributes Run CSharpier, Prettier and markdownlint across the entire codebase. No logic changes — formatting, using order and line endings only.
This commit is contained in:
+27
-27
@@ -11,43 +11,43 @@ name: Build
|
||||
# Dalamud SDK 15 uses on Linux).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
# Minimum permissions for a build-only workflow: read the repo, nothing
|
||||
# else. Closes the CodeQL "Workflow does not contain permissions" alert
|
||||
# and matches the principle-of-least-privilege the security guide
|
||||
# recommends for workflows that don't push or create releases.
|
||||
permissions:
|
||||
contents: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (Release)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
build:
|
||||
name: Build (Release)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Download Dalamud staging
|
||||
run: |
|
||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||
mkdir -p "$hooks"
|
||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||
unzip -oq dalamud.zip -d "$hooks"
|
||||
- name: Download Dalamud staging
|
||||
run: |
|
||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||
mkdir -p "$hooks"
|
||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||
unzip -oq dalamud.zip -d "$hooks"
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore HellionChat/HellionChat.csproj
|
||||
- name: Restore
|
||||
run: dotnet restore HellionChat/HellionChat.csproj
|
||||
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||
|
||||
@@ -17,209 +17,209 @@ name: Forge Announce
|
||||
# (issue titles, PR bodies, commit messages, etc.) flows into run-steps.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing tag to (re)post, e.g. v1.1.0'
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing tag to (re)post, e.g. v1.1.0"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
announce:
|
||||
name: Post changelog to Hellion Forge
|
||||
runs-on: ubuntu-latest
|
||||
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
||||
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
||||
# scope for every job by default, no environment: declaration needed.
|
||||
timeout-minutes: 5
|
||||
announce:
|
||||
name: Post changelog to Hellion Forge
|
||||
runs-on: ubuntu-latest
|
||||
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
||||
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
||||
# scope for every job by default, no environment: declaration needed.
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
# On push:tags github.ref points at the tag commit; on workflow_dispatch
|
||||
# the user supplies the tag explicitly. Always check out that tag so
|
||||
# the yaml + forge-posts file are read from the tagged tree, not main.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
steps:
|
||||
# On push:tags github.ref points at the tag commit; on workflow_dispatch
|
||||
# the user supplies the tag explicitly. Always check out that tag so
|
||||
# the yaml + forge-posts file are read from the tagged tree, not main.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
# Build embed-payload as a JSON file on disk. PowerShell-Core (pwsh)
|
||||
# ships pre-installed on ubuntu-latest so we get the same scripting
|
||||
# patterns release.yml uses on windows-latest. Tag is read via env: to
|
||||
# treat it as a string variable rather than inline shell text, and
|
||||
# validated against the semver regex before any interpolation.
|
||||
- name: Build embed payload
|
||||
id: build
|
||||
shell: pwsh
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
$tag = $env:TAG_NAME
|
||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||
throw "V1: Refusing to announce non-semver tag: $tag"
|
||||
}
|
||||
$version = $tag.Substring(1)
|
||||
|
||||
# ---------- Forge-Post-Datei lesen ----------
|
||||
$forgePath = ".github/forge-posts/$tag.md"
|
||||
if (-not (Test-Path $forgePath)) {
|
||||
throw "V2: Forge-Post-Datei für $tag fehlt unter .github/forge-posts/. Datei vor dem Tag anlegen, dann Tag re-pushen oder workflow_dispatch."
|
||||
}
|
||||
$forgeRaw = Get-Content -Path $forgePath -Raw
|
||||
|
||||
# Frontmatter (--- … ---) am Datei-Anfang
|
||||
if ($forgeRaw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)\z') {
|
||||
throw "V3: Frontmatter (---) fehlt oder ist defekt in $forgePath"
|
||||
}
|
||||
$fmText = $matches[1]
|
||||
$deBody = $matches[2].Trim()
|
||||
|
||||
$subtitle = $null
|
||||
$versionsnatur = $null
|
||||
foreach ($line in ($fmText -split "`r?`n")) {
|
||||
if ($line -match '^subtitle:\s*"?([^"]*)"?\s*$') { $subtitle = $matches[1] }
|
||||
if ($line -match '^versionsnatur:\s*"?([^"]*)"?\s*$') { $versionsnatur = $matches[1] }
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($subtitle)) { throw "V3: Frontmatter-Feld 'subtitle' fehlt in $forgePath" }
|
||||
if ([string]::IsNullOrWhiteSpace($versionsnatur)) { throw "V3: Frontmatter-Feld 'versionsnatur' fehlt in $forgePath" }
|
||||
if ($subtitle.Length -gt 60) { throw "V4: Frontmatter-Feld 'subtitle' überschreitet Limit ($($subtitle.Length) Char, max 60)" }
|
||||
if ($versionsnatur.Length -gt 40) { throw "V4: Frontmatter-Feld 'versionsnatur' überschreitet Limit ($($versionsnatur.Length) Char, max 40)" }
|
||||
if ([string]::IsNullOrWhiteSpace($deBody)) { throw "V3: DE-Body fehlt in $forgePath" }
|
||||
|
||||
# ---------- EN-Block aus HellionChat.yaml ziehen ----------
|
||||
# 1:1 Pattern aus release.yml — gleicher Header-Marker, gleiches
|
||||
# Trailer-Verhalten. Bei Drift die zwei Workflows synchron halten.
|
||||
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||
$raw = Get-Content -Path $yamlPath -Raw
|
||||
$marker = "changelog: |-"
|
||||
$idx = $raw.IndexOf($marker)
|
||||
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$header = "**Hellion Chat $version"
|
||||
$start = $changelogBody.IndexOf($header)
|
||||
if ($start -lt 0) {
|
||||
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||
}
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||
} elseif ($trailer -ge 0) {
|
||||
$enBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||
} else {
|
||||
$enBlock = $rest.TrimEnd()
|
||||
}
|
||||
|
||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
||||
$title = "Hellion Chat $version — $subtitle"
|
||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
||||
$footerText = "Hellion Forge · $versionsnatur"
|
||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
||||
if ($totalChars -gt 5500) {
|
||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
||||
}
|
||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
||||
|
||||
# ---------- Embed-Payload bauen ----------
|
||||
$payload = [ordered]@{
|
||||
username = "Forge Herald"
|
||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||
content = "<@&1500489631555260446>"
|
||||
allowed_mentions = [ordered]@{
|
||||
parse = @()
|
||||
roles = @("1500489631555260446")
|
||||
}
|
||||
embeds = @(
|
||||
[ordered]@{
|
||||
title = $title
|
||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
color = 12730636
|
||||
description = $description
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
# Build embed-payload as a JSON file on disk. PowerShell-Core (pwsh)
|
||||
# ships pre-installed on ubuntu-latest so we get the same scripting
|
||||
# patterns release.yml uses on windows-latest. Tag is read via env: to
|
||||
# treat it as a string variable rather than inline shell text, and
|
||||
# validated against the semver regex before any interpolation.
|
||||
- name: Build embed payload
|
||||
id: build
|
||||
shell: pwsh
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
$tag = $env:TAG_NAME
|
||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||
throw "V1: Refusing to announce non-semver tag: $tag"
|
||||
}
|
||||
)
|
||||
}
|
||||
$version = $tag.Substring(1)
|
||||
|
||||
$payloadJson = $payload | ConvertTo-Json -Depth 8 -Compress
|
||||
# Ausgabe-Datei ohne trailing newline für sauberes curl --data-binary @-
|
||||
[System.IO.File]::WriteAllText("$PWD/embed-payload.json", $payloadJson, [System.Text.UTF8Encoding]::new($false))
|
||||
# ---------- Forge-Post-Datei lesen ----------
|
||||
$forgePath = ".github/forge-posts/$tag.md"
|
||||
if (-not (Test-Path $forgePath)) {
|
||||
throw "V2: Forge-Post-Datei für $tag fehlt unter .github/forge-posts/. Datei vor dem Tag anlegen, dann Tag re-pushen oder workflow_dispatch."
|
||||
}
|
||||
$forgeRaw = Get-Content -Path $forgePath -Raw
|
||||
|
||||
Write-Host "Payload size: $($payloadJson.Length) chars"
|
||||
Write-Host "Embed title: $title"
|
||||
Write-Host "Embed footer: $footerText"
|
||||
# Frontmatter (--- … ---) am Datei-Anfang
|
||||
if ($forgeRaw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)\z') {
|
||||
throw "V3: Frontmatter (---) fehlt oder ist defekt in $forgePath"
|
||||
}
|
||||
$fmText = $matches[1]
|
||||
$deBody = $matches[2].Trim()
|
||||
|
||||
# POST to the Hellion Forge changelog webhook. curl from PowerShell-Core
|
||||
# so we can pipe the payload via stdin (--data-binary @-) and keep
|
||||
# secrets out of process arg lists. One retry on 5xx, hard fail on 4xx.
|
||||
- name: POST to Hellion Forge webhook
|
||||
shell: pwsh
|
||||
env:
|
||||
DISCORD_FORGE_WEBHOOK: ${{ secrets.DISCORD_FORGE_WEBHOOK }}
|
||||
run: |
|
||||
if ([string]::IsNullOrEmpty($env:DISCORD_FORGE_WEBHOOK)) {
|
||||
throw "V7: DISCORD_FORGE_WEBHOOK secret is empty. Check Settings → Environments → Webhook."
|
||||
}
|
||||
$subtitle = $null
|
||||
$versionsnatur = $null
|
||||
foreach ($line in ($fmText -split "`r?`n")) {
|
||||
if ($line -match '^subtitle:\s*"?([^"]*)"?\s*$') { $subtitle = $matches[1] }
|
||||
if ($line -match '^versionsnatur:\s*"?([^"]*)"?\s*$') { $versionsnatur = $matches[1] }
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($subtitle)) { throw "V3: Frontmatter-Feld 'subtitle' fehlt in $forgePath" }
|
||||
if ([string]::IsNullOrWhiteSpace($versionsnatur)) { throw "V3: Frontmatter-Feld 'versionsnatur' fehlt in $forgePath" }
|
||||
if ($subtitle.Length -gt 60) { throw "V4: Frontmatter-Feld 'subtitle' überschreitet Limit ($($subtitle.Length) Char, max 60)" }
|
||||
if ($versionsnatur.Length -gt 40) { throw "V4: Frontmatter-Feld 'versionsnatur' überschreitet Limit ($($versionsnatur.Length) Char, max 40)" }
|
||||
if ([string]::IsNullOrWhiteSpace($deBody)) { throw "V3: DE-Body fehlt in $forgePath" }
|
||||
|
||||
$payloadFile = "$PWD/embed-payload.json"
|
||||
if (-not (Test-Path $payloadFile)) {
|
||||
throw "Embed payload file missing — previous step did not produce embed-payload.json"
|
||||
}
|
||||
# ---------- EN-Block aus HellionChat.yaml ziehen ----------
|
||||
# 1:1 Pattern aus release.yml — gleicher Header-Marker, gleiches
|
||||
# Trailer-Verhalten. Bei Drift die zwei Workflows synchron halten.
|
||||
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||
$raw = Get-Content -Path $yamlPath -Raw
|
||||
$marker = "changelog: |-"
|
||||
$idx = $raw.IndexOf($marker)
|
||||
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$maxAttempts = 2
|
||||
$attempt = 0
|
||||
while ($attempt -lt $maxAttempts) {
|
||||
$attempt++
|
||||
Write-Host "POST attempt $attempt of $maxAttempts"
|
||||
$tmpResp = "$PWD/.webhook-response"
|
||||
$tmpHeaders = "$PWD/.webhook-headers"
|
||||
# --silent suppresses progress; --show-error prints errors so
|
||||
# the workflow log shows what happened. -w prints HTTP status
|
||||
# to stdout for inspection. -o captures body for diagnosis,
|
||||
# -D captures headers.
|
||||
$rawStatus = Get-Content $payloadFile -Raw |
|
||||
curl --silent --show-error `
|
||||
--header 'Content-Type: application/json' `
|
||||
--data-binary '@-' `
|
||||
-D $tmpHeaders `
|
||||
-o $tmpResp `
|
||||
-w '%{http_code}' `
|
||||
"$env:DISCORD_FORGE_WEBHOOK"
|
||||
$status = [int]$rawStatus
|
||||
Write-Host "HTTP status: $status"
|
||||
$header = "**Hellion Chat $version"
|
||||
$start = $changelogBody.IndexOf($header)
|
||||
if ($start -lt 0) {
|
||||
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||
}
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||
} elseif ($trailer -ge 0) {
|
||||
$enBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||
} else {
|
||||
$enBlock = $rest.TrimEnd()
|
||||
}
|
||||
|
||||
if ($status -ge 200 -and $status -lt 300) {
|
||||
Write-Host "Forge announce POST succeeded."
|
||||
exit 0
|
||||
}
|
||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
||||
$title = "Hellion Chat $version — $subtitle"
|
||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
||||
$footerText = "Hellion Forge · $versionsnatur"
|
||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
||||
if ($totalChars -gt 5500) {
|
||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
||||
}
|
||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
||||
|
||||
$bodySnippet = ""
|
||||
if (Test-Path $tmpResp) {
|
||||
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
|
||||
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
|
||||
}
|
||||
# ---------- Embed-Payload bauen ----------
|
||||
$payload = [ordered]@{
|
||||
username = "Forge Herald"
|
||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||
content = "<@&1500489631555260446>"
|
||||
allowed_mentions = [ordered]@{
|
||||
parse = @()
|
||||
roles = @("1500489631555260446")
|
||||
}
|
||||
embeds = @(
|
||||
[ordered]@{
|
||||
title = $title
|
||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
color = 12730636
|
||||
description = $description
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if ($status -ge 400 -and $status -lt 500) {
|
||||
# E2: 4xx is permanent — webhook revoked, channel deleted,
|
||||
# payload malformed. No retry.
|
||||
throw "E2: Discord-Webhook returned permanent $status. Body: $bodySnippet"
|
||||
}
|
||||
$payloadJson = $payload | ConvertTo-Json -Depth 8 -Compress
|
||||
# Ausgabe-Datei ohne trailing newline für sauberes curl --data-binary @-
|
||||
[System.IO.File]::WriteAllText("$PWD/embed-payload.json", $payloadJson, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
# E1: 5xx (or transport-level fail with status 0) — wait + retry once
|
||||
if ($attempt -lt $maxAttempts) {
|
||||
Write-Host "Transient $status — sleeping 30s before retry."
|
||||
Start-Sleep -Seconds 30
|
||||
} else {
|
||||
throw "E1: Discord-Webhook returned transient $status after $maxAttempts attempts. Body: $bodySnippet"
|
||||
}
|
||||
}
|
||||
Write-Host "Payload size: $($payloadJson.Length) chars"
|
||||
Write-Host "Embed title: $title"
|
||||
Write-Host "Embed footer: $footerText"
|
||||
|
||||
# POST to the Hellion Forge changelog webhook. curl from PowerShell-Core
|
||||
# so we can pipe the payload via stdin (--data-binary @-) and keep
|
||||
# secrets out of process arg lists. One retry on 5xx, hard fail on 4xx.
|
||||
- name: POST to Hellion Forge webhook
|
||||
shell: pwsh
|
||||
env:
|
||||
DISCORD_FORGE_WEBHOOK: ${{ secrets.DISCORD_FORGE_WEBHOOK }}
|
||||
run: |
|
||||
if ([string]::IsNullOrEmpty($env:DISCORD_FORGE_WEBHOOK)) {
|
||||
throw "V7: DISCORD_FORGE_WEBHOOK secret is empty. Check Settings → Environments → Webhook."
|
||||
}
|
||||
|
||||
$payloadFile = "$PWD/embed-payload.json"
|
||||
if (-not (Test-Path $payloadFile)) {
|
||||
throw "Embed payload file missing — previous step did not produce embed-payload.json"
|
||||
}
|
||||
|
||||
$maxAttempts = 2
|
||||
$attempt = 0
|
||||
while ($attempt -lt $maxAttempts) {
|
||||
$attempt++
|
||||
Write-Host "POST attempt $attempt of $maxAttempts"
|
||||
$tmpResp = "$PWD/.webhook-response"
|
||||
$tmpHeaders = "$PWD/.webhook-headers"
|
||||
# --silent suppresses progress; --show-error prints errors so
|
||||
# the workflow log shows what happened. -w prints HTTP status
|
||||
# to stdout for inspection. -o captures body for diagnosis,
|
||||
# -D captures headers.
|
||||
$rawStatus = Get-Content $payloadFile -Raw |
|
||||
curl --silent --show-error `
|
||||
--header 'Content-Type: application/json' `
|
||||
--data-binary '@-' `
|
||||
-D $tmpHeaders `
|
||||
-o $tmpResp `
|
||||
-w '%{http_code}' `
|
||||
"$env:DISCORD_FORGE_WEBHOOK"
|
||||
$status = [int]$rawStatus
|
||||
Write-Host "HTTP status: $status"
|
||||
|
||||
if ($status -ge 200 -and $status -lt 300) {
|
||||
Write-Host "Forge announce POST succeeded."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$bodySnippet = ""
|
||||
if (Test-Path $tmpResp) {
|
||||
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
|
||||
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
|
||||
}
|
||||
|
||||
if ($status -ge 400 -and $status -lt 500) {
|
||||
# E2: 4xx is permanent — webhook revoked, channel deleted,
|
||||
# payload malformed. No retry.
|
||||
throw "E2: Discord-Webhook returned permanent $status. Body: $bodySnippet"
|
||||
}
|
||||
|
||||
# E1: 5xx (or transport-level fail with status 0) — wait + retry once
|
||||
if ($attempt -lt $maxAttempts) {
|
||||
Write-Host "Transient $status — sleeping 30s before retry."
|
||||
Start-Sleep -Seconds 30
|
||||
} else {
|
||||
throw "E1: Discord-Webhook returned transient $status after $maxAttempts attempts. Body: $bodySnippet"
|
||||
}
|
||||
}
|
||||
|
||||
+131
-131
@@ -17,154 +17,154 @@ name: Release
|
||||
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
||||
# The tag input is validated against the same semver regex as the
|
||||
# auto-trigger before any string interpolation happens.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing tag to (re)release, e.g. v0.6.1'
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
||||
# The tag input is validated against the same semver regex as the
|
||||
# auto-trigger before any string interpolation happens.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing tag to (re)release, e.g. v0.6.1"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and attach release ZIP
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
release:
|
||||
name: Build and attach release ZIP
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
||||
# On workflow_dispatch, ref defaults to the branch the action was
|
||||
# invoked from; we need to explicitly check out the tag the user
|
||||
# supplied so the build comes from the tagged commit, not main.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
steps:
|
||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
||||
# On workflow_dispatch, ref defaults to the branch the action was
|
||||
# invoked from; we need to explicitly check out the tag the user
|
||||
# supplied so the build comes from the tagged commit, not main.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Download Dalamud staging
|
||||
run: |
|
||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||
mkdir -p "$hooks"
|
||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||
unzip -oq dalamud.zip -d "$hooks"
|
||||
- name: Download Dalamud staging
|
||||
run: |
|
||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||
mkdir -p "$hooks"
|
||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||
unzip -oq dalamud.zip -d "$hooks"
|
||||
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
|
||||
|
||||
- name: Locate latest.zip
|
||||
id: locate
|
||||
run: |
|
||||
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
||||
if [ -z "$zip" ]; then
|
||||
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Found: $zip"
|
||||
echo "path=$zip" >> "$GITHUB_OUTPUT"
|
||||
- name: Locate latest.zip
|
||||
id: locate
|
||||
run: |
|
||||
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
||||
if [ -z "$zip" ]; then
|
||||
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Found: $zip"
|
||||
echo "path=$zip" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Build a release body from the matching changelog block in
|
||||
# HellionChat.yaml plus a static install / docs footer. Fails the
|
||||
# workflow if no block exists for the tagged version, which is the
|
||||
# automated counterpart to the "yaml + repo.json + release body
|
||||
# kept in sync" rule.
|
||||
#
|
||||
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
|
||||
# tag value is treated as a PowerShell variable, not as inline shell
|
||||
# text. The strict regex below rejects anything that is not a clean
|
||||
# semver tag before it is used to build a string.
|
||||
- name: Generate release body
|
||||
shell: pwsh
|
||||
env:
|
||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
||||
# push:tags carries it in github.ref_name. Either way the value
|
||||
# is treated as a PowerShell variable (env-var pass), not as
|
||||
# inline shell text, and validated against the semver regex
|
||||
# below before any string interpolation.
|
||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
$tag = $env:TAG_NAME
|
||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||
throw "Refusing to generate release body for non-semver tag: $tag"
|
||||
}
|
||||
$version = $tag.Substring(1)
|
||||
# Build a release body from the matching changelog block in
|
||||
# HellionChat.yaml plus a static install / docs footer. Fails the
|
||||
# workflow if no block exists for the tagged version, which is the
|
||||
# automated counterpart to the "yaml + repo.json + release body
|
||||
# kept in sync" rule.
|
||||
#
|
||||
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
|
||||
# tag value is treated as a PowerShell variable, not as inline shell
|
||||
# text. The strict regex below rejects anything that is not a clean
|
||||
# semver tag before it is used to build a string.
|
||||
- name: Generate release body
|
||||
shell: pwsh
|
||||
env:
|
||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
||||
# push:tags carries it in github.ref_name. Either way the value
|
||||
# is treated as a PowerShell variable (env-var pass), not as
|
||||
# inline shell text, and validated against the semver regex
|
||||
# below before any string interpolation.
|
||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
$tag = $env:TAG_NAME
|
||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||
throw "Refusing to generate release body for non-semver tag: $tag"
|
||||
}
|
||||
$version = $tag.Substring(1)
|
||||
|
||||
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||
$raw = Get-Content -Path $yamlPath -Raw
|
||||
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||
$raw = Get-Content -Path $yamlPath -Raw
|
||||
|
||||
$marker = "changelog: |-"
|
||||
$idx = $raw.IndexOf($marker)
|
||||
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
|
||||
$marker = "changelog: |-"
|
||||
$idx = $raw.IndexOf($marker)
|
||||
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
|
||||
|
||||
# changelog: is the last top-level key in the manifest, so
|
||||
# everything after the marker is the literal block. Strip the
|
||||
# 2-space yaml indent from each line.
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
# changelog: is the last top-level key in the manifest, so
|
||||
# everything after the marker is the literal block. Strip the
|
||||
# 2-space yaml indent from each line.
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$header = "**Hellion Chat $version"
|
||||
$start = $changelogBody.IndexOf($header)
|
||||
if ($start -lt 0) {
|
||||
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||
}
|
||||
$header = "**Hellion Chat $version"
|
||||
$start = $changelogBody.IndexOf($header)
|
||||
if ($start -lt 0) {
|
||||
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||
}
|
||||
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
|
||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||
} elseif ($trailer -ge 0) {
|
||||
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||
} else {
|
||||
$currentBlock = $rest.TrimEnd()
|
||||
}
|
||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||
} elseif ($trailer -ge 0) {
|
||||
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||
} else {
|
||||
$currentBlock = $rest.TrimEnd()
|
||||
}
|
||||
|
||||
# Static install / docs / licence footer is maintained as a
|
||||
# separate file so the workflow YAML stays clean (no embedded
|
||||
# heredoc that would have to be indented under the run-block).
|
||||
$footerPath = ".github/release-footer.md"
|
||||
if (-not (Test-Path $footerPath)) {
|
||||
throw "Release footer template not found: $footerPath"
|
||||
}
|
||||
$footer = Get-Content -Path $footerPath -Raw
|
||||
# Static install / docs / licence footer is maintained as a
|
||||
# separate file so the workflow YAML stays clean (no embedded
|
||||
# heredoc that would have to be indented under the run-block).
|
||||
$footerPath = ".github/release-footer.md"
|
||||
if (-not (Test-Path $footerPath)) {
|
||||
throw "Release footer template not found: $footerPath"
|
||||
}
|
||||
$footer = Get-Content -Path $footerPath -Raw
|
||||
|
||||
$body = $currentBlock + "`n" + $footer
|
||||
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
|
||||
$body = $currentBlock + "`n" + $footer
|
||||
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
|
||||
|
||||
Write-Host "Generated release body for $tag :"
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host $body
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host "Generated release body for $tag :"
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host $body
|
||||
Write-Host "----------------------------------------"
|
||||
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one. body_path provides the
|
||||
# generated release body, files attaches latest.zip. The auto-injected
|
||||
# GITHUB_TOKEN on Gitea Actions has Gitea-API scope and is sufficient
|
||||
# for release write.
|
||||
- name: Attach to Gitea release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
# Explicit tag_name so the action targets the correct release in
|
||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||
# modes. Without this, dispatch runs would default to the branch
|
||||
# ref (main) and fail to find the release.
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body_path: release-body.md
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one. body_path provides the
|
||||
# generated release body, files attaches latest.zip. The auto-injected
|
||||
# GITHUB_TOKEN on Gitea Actions has Gitea-API scope and is sufficient
|
||||
# for release write.
|
||||
- name: Attach to Gitea release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
# Explicit tag_name so the action targets the correct release in
|
||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||
# modes. Without this, dispatch runs would default to the branch
|
||||
# ref (main) and fail to find the release.
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body_path: release-body.md
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
Reference in New Issue
Block a user