be17472cd5
Gitea Actions reads exclusively from .gitea/workflows/, not from .github/workflows/. Since the cutover in v1.4.3 only the security workflow has been running — release and forge-announce silently sat in the wrong directory and never fired on any tag push. v1.4.3 must have been released manually. Move build, release and forge-announce yamls to .gitea/workflows/. The .github/forge-posts/ and .github/release-footer.md data files stay where they are; the workflows reference them by repo-relative path and that keeps working. For the v1.4.4 backfill: workflow_dispatch via the Gitea web UI with tag=v1.4.4 will run release.yml + forge-announce.yml against the tagged tree (which doesn't contain this migration). The dispatch yaml itself is read from the default branch, not the tag, so the missing yamls in the v1.4.4 tag tree don't matter.
226 lines
12 KiB
YAML
226 lines
12 KiB
YAML
name: Forge Announce
|
|
|
|
# Triggered when a vX.Y.Z tag is pushed. Reads .github/forge-posts/<tag>.md
|
|
# (Frontmatter + DE bullet body) and the matching English block from
|
|
# HellionChat/HellionChat.yaml, builds a Discord-Webhook embed and posts
|
|
# it to the Hellion Forge #changelog channel.
|
|
#
|
|
# Decoupled from release.yml: a fail here does not block the GitHub
|
|
# release, and a fail there does not block the announce. Spec lives in
|
|
# the Vault under "Hellion Chat Forge-Auto-Announce Spec".
|
|
#
|
|
# Security: the only user-controlled inputs that enter run-steps are the
|
|
# tag name and the frontmatter values from a repo-internal markdown file.
|
|
# Tag name is read via env: (TAG_NAME, $env:TAG_NAME) and validated against
|
|
# ^v\d+\.\d+\.\d+$ before any string interpolation. Frontmatter values are
|
|
# parsed by regex with explicit length caps. No webhook event payload data
|
|
# (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
|
|
|
|
permissions:
|
|
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
|
|
|
|
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")
|
|
}
|
|
)
|
|
}
|
|
|
|
$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))
|
|
|
|
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"
|
|
}
|
|
}
|