Files
HellionChat/.gitea/workflows/release.yml
T
JonKazama-Hellion 226174bf12
Security Scan (reusable) / Security Scan (push) Failing after 23s
Security / scan (push) Failing after 23s
Build / Build (Release) (push) Successful in 30s
ci(release): publish with curl instead of a go action, and lift MessagePack
The v2.0.0 tag built fine and then died on its last step: gitea.com's
release-action declares `using: go`, the runner has to compile it, and act
cannot -- exec: "go": executable file not found, exit 127, after a green build.
The zip existed and never got attached, so the Discord announcement went out
while the download link pointed at nothing.

This is a known failure. It was diagnosed on another repo in June and the note
from then says in as many words that this repo carries the same pattern and
should migrate before its next release. It did not, and here we are.

The publish step is a plain curl call against the Gitea API now, running in the
job image with curl and python3, independent of go, the action cache, and
whatever @main happens to point at. Idempotent by design: a re-run reuses an
existing release and replaces the asset rather than failing on the duplicate,
which is exactly the state a recovery run finds.

MessagePack moves from the 3.1.4 floor to 3.1.7, which is what the trivy scan
was failing on. The range already allowed it -- NuGet resolves the lower bound
of a range, and trivy reads it the same way, so the floor is the version that
counts. The advisories are recursion depth in Skip and an LZ4 decompression
fault, both reachable only through crafted input; this plugin serialises its own
payloads and reads back its own bytes from a local database, so the practical
exposure is someone who already has write access to the file. Lifted because it
costs nothing and a scan that stays red for a known-harmless reason is how a real
finding gets missed later.
2026-08-19 22:44:55 +02:00

208 lines
9.1 KiB
YAML

name: Release
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
# current Dalamud staging branch, locates the latest.zip produced by
# DalamudPackager and attaches it to the matching Gitea Release.
#
# User-controlled inputs touched by this workflow:
# - the tag name (filtered by on.tags = v*, validated again at runtime
# against ^v\d+\.\d+\.\d+$ before being used in any string)
# All other values are either repo-controlled (paths under
# HellionChat/bin/Release derived from find / Get-ChildItem) or pinned
# URLs to goatcorp / gitea. Nothing from a webhook event payload (issue/PR
# titles, commit messages, etc.) flows into a run-step.
#
# Linux runner: gitea.com Cloud Actions only ships ubuntu-latest. The
# plugin csproj targets net10.0-windows, `dotnet build` cross-compiles on
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
on:
push:
tags:
- 'v*'
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
# ref step below hard-fails if a non-tag ref is selected: the release
# name and body are both derived from the tag, so a branch ref would
# publish a release named after a branch.
workflow_dispatch:
permissions:
contents: write
jobs:
release:
name: Build and attach release ZIP
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# Validate up-front so a manual dispatch from a branch ref fails loud
# here instead of burning a full build before the publish step notices.
- name: Validate tag ref
run: |
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Release workflow must run on a v*.X.Y tag ref, got ${GITHUB_REF}"
echo "::error::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
exit 1
fi
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup .NET 10
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # 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: 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"
# 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:
# github.ref_name is the tag because Validate tag ref above
# already enforced refs/tags/v*. Read via env: so the value
# is a PowerShell variable, not inline shell text, and gets
# re-validated against the semver regex below.
TAG_NAME: ${{ 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
$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
# 4-space yaml indent (prettier convention) from each line.
$afterMarker = $raw.Substring($idx + $marker.Length)
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
}) -join "`n"
# Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
# matches verify-changelog-sync.sh and slim-rule grep.
$header = "**v$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**v", 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()
}
# 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
Write-Host "Generated release body for $tag :"
Write-Host "----------------------------------------"
Write-Host $body
Write-Host "----------------------------------------"
# The tag comes from GITHUB_REF, the body from the step above. Posted with
# curl rather than gitea.com/actions/release-action, which declares
# `using: go` and has to be compiled by the runner -- act cannot do that
# here and the step dies with exec: "go": executable file not found, exit
# 127, after a build that otherwise succeeded. This runs as a plain shell
# step in the job image, which has curl and python3.
#
# Idempotent on purpose: a re-run against an existing release reuses it and
# replaces the asset instead of failing on the duplicate.
- name: Attach to Gitea release
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ZIP_PATH: ${{ steps.locate.outputs.path }}
TAG_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}"
auth="Authorization: token ${GITEA_TOKEN}"
# Existing release for this tag, or create one.
rel_id="$(curl -sf -H "$auth" "$api/releases/tags/${TAG_NAME}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true)"
if [ -z "$rel_id" ]; then
payload="$(python3 -c '
import json, os, sys
body = open("release-body.md", encoding="utf-8").read()
json.dump({"tag_name": os.environ["TAG_NAME"], "name": os.environ["TAG_NAME"],
"body": body, "draft": False, "prerelease": False}, sys.stdout)
')"
rel_id="$(printf '%s' "$payload" \
| curl -sf -X POST -H "$auth" -H "Content-Type: application/json" -d @- "$api/releases" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')"
echo "Created release $rel_id for ${TAG_NAME}"
else
echo "Reusing release $rel_id for ${TAG_NAME}"
fi
# Drop a same-named asset from an earlier attempt, or the upload 409s.
old_id="$(curl -sf -H "$auth" "$api/releases/${rel_id}/assets" \
| python3 -c 'import sys,json; print(next((a["id"] for a in json.load(sys.stdin) if a["name"]=="latest.zip"), ""))' 2>/dev/null || true)"
if [ -n "$old_id" ]; then
curl -sf -X DELETE -H "$auth" "$api/releases/${rel_id}/assets/${old_id}"
echo "Replaced existing latest.zip (asset $old_id)"
fi
curl -sf -X POST -H "$auth" \
-F "attachment=@${ZIP_PATH};filename=latest.zip" \
"$api/releases/${rel_id}/assets?name=latest.zip" \
| python3 -c 'import sys,json; a=json.load(sys.stdin); print("Attached", a["name"], a["size"], "bytes")'