xetup/.forgejo/workflows/release.yml
X9 Dev eabf207e3f
All checks were successful
release / build-and-release (push) Successful in 43s
feat(report): file failed runs as Forgejo issues + attach zipped log to email
On any ERROR step, xetup now files an issue in the private x9/xetup-runs
tracker (title "FAILED <host> - <version> - <date>", label "failed", body =
step table + tail-capped Deploy.log) via the xetup-bot write:issue token,
baked in through -ldflags. Best-effort and non-blocking; successful runs stay
silent and the email is the fallback.

The deployment email now attaches the zipped Deploy.log and links to this
run's issue plus the run history filtered to this machine.

- internal/buildinfo: ldflags-injected Version + RunsToken
- internal/runreport: issue filing, retries, tail-capped log
- internal/report: multipart/mixed with zip attachment, tracker links;
  buildMessage split out and covered by report_test.go
- release.yml: inject Version (tag/SHA) + FORGEJO_RUNS_TOKEN via ldflags

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 16:30:08 +02:00

191 lines
8.5 KiB
YAML

name: release
on:
workflow_dispatch: {}
push:
branches: [main]
tags: ['v*']
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'scripts/**'
- 'assets/**'
- 'embed.go'
- 'cmd/xetup/app.manifest'
- '.forgejo/workflows/release.yml'
jobs:
build-and-release:
# Runner label 'ubuntu-latest' maps to golang:1.24-alpine container (see runner config)
runs-on: ubuntu-latest
defaults:
run:
shell: sh
working-directory: /repo
steps:
- name: Setup
working-directory: /
run: |
apk add --no-cache git curl jq mingw-w64-gcc docker-cli
# Full clone (not --depth=1): a version tag can point at an older commit
# than the main tip, which a shallow clone would not contain.
git clone \
"http://x9:${{ secrets.FORGEJO_TOKEN }}@xetup-forgejo:3000/${{ github.repository }}.git" \
/repo
cd /repo
git checkout "${{ github.sha }}"
- name: Generate rsrc.syso (manifest + UAC)
run: |
go install github.com/akavel/rsrc@latest
rsrc -manifest cmd/xetup/app.manifest -o cmd/xetup/rsrc.syso
echo "rsrc.syso: $(ls -lh cmd/xetup/rsrc.syso | awk '{print $5}')"
- name: Build xetup.exe
env:
# write:issue token for x9/xetup-runs; baked in so runs self-report
# failures. Empty on forks/without-secret -> runreport is a no-op.
FORGEJO_RUNS_TOKEN: ${{ secrets.FORGEJO_RUNS_TOKEN }}
run: |
# Version = tag (v0.10) on a tag build, else short commit SHA.
case "${{ github.ref }}" in
refs/tags/v*) VERSION=$(echo "${{ github.ref }}" | sed 's#refs/tags/##') ;;
*) VERSION=$(echo "${{ github.sha }}" | cut -c1-7) ;;
esac
PKG=git.xetup.x9.cz/x9/xetup/internal/buildinfo
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
GOOS=windows GOARCH=amd64 \
go build \
-ldflags="-s -w -H windowsgui -X ${PKG}.Version=${VERSION} -X ${PKG}.RunsToken=${FORGEJO_RUNS_TOKEN}" \
-o xetup.exe ./cmd/xetup/
echo "Built: $(ls -lh xetup.exe | awk '{print $5}') - version ${VERSION}, runreport $([ -n "$FORGEJO_RUNS_TOKEN" ] && echo enabled || echo DISABLED)"
- name: Sign xetup.exe (Azure Trusted Signing)
env:
# Non-secret identifiers (Entra app + signing account) - safe to inline.
# Only the client secret is a Forgejo secret (Settings > Actions > Secrets).
AZURE_TENANT_ID: 7d36c38a-f04e-49b4-b500-b1677a7fe62f
AZURE_CLIENT_ID: a96e36b5-2661-497a-9d16-b70a6096e78b
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
TS_ENDPOINT: weu.codesigning.azure.net
TS_ACCOUNT: x9-signing
TS_PROFILE: x9-public
TS_TSA: http://timestamp.acs.microsoft.com
JSIGN_VERSION: "7.4"
JSIGN_SHA256: 2abf2ade9ea322acc2d60c24794eadc465ff9380938fca4c932d09e0b25f1c28
run: |
if [ -z "$AZURE_CLIENT_SECRET" ]; then
echo "ERROR: AZURE_CLIENT_SECRET not set (Forgejo > repo Settings > Actions > Secrets)" >&2
exit 1
fi
apk add --no-cache openjdk17-jre-headless
# Fetch jsign - pinned version, sha256-verified (supply-chain guard)
curl -fsSL -o /tmp/jsign.jar \
"https://github.com/ebourg/jsign/releases/download/${JSIGN_VERSION}/jsign-${JSIGN_VERSION}.jar"
echo "${JSIGN_SHA256} /tmp/jsign.jar" | sha256sum -c -
# Acquire short-lived Trusted Signing access token from the service principal
TOKEN=$(curl -fsS -X POST \
"https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token" \
-d grant_type=client_credentials \
-d "client_id=${AZURE_CLIENT_ID}" \
--data-urlencode "client_secret=${AZURE_CLIENT_SECRET}" \
--data-urlencode "scope=https://codesigning.azure.net/.default" \
| jq -r '.access_token')
[ -n "$TOKEN" ] && [ "$TOKEN" != "null" ] || { echo "ERROR: token acquisition failed" >&2; exit 1; }
echo "Trusted Signing token acquired (length ${#TOKEN})"
# Sign + RFC3161 timestamp. The signing cert is short-lived (~3 days);
# the timestamp is what keeps the signature valid after it expires, so
# timestamping must succeed - the step fails hard if it does not.
java -jar /tmp/jsign.jar \
--storetype TRUSTEDSIGNING \
--keystore "${TS_ENDPOINT}" \
--storepass "${TOKEN}" \
--alias "${TS_ACCOUNT}/${TS_PROFILE}" \
--tsaurl "${TS_TSA}" \
--tsmode RFC3161 \
--alg SHA-256 \
xetup.exe
echo "Signed and timestamped xetup.exe"
- name: Extract release notes from CHANGELOG.md
run: |
# On a version tag (v0.7) take that version's section; otherwise the
# latest released version section (first "## [<digit>"). Falls back to
# [Unreleased], then to a placeholder.
case "${{ github.ref }}" in
refs/tags/v*)
HEADER="## [$(echo "${{ github.ref }}" | sed 's#refs/tags/v##')]" ;;
*)
HEADER=$(grep -m1 -E '^## \[[0-9]' CHANGELOG.md 2>/dev/null || true) ;;
esac
[ -n "$HEADER" ] || HEADER="## [Unreleased]"
# Print the section body from HEADER (prefix match) until the next "## ".
awk -v h="$HEADER" '
index($0, h) == 1 { f = 1; next }
f && /^## / { exit }
f { print }
' CHANGELOG.md > /tmp/notes.md
[ -s /tmp/notes.md ] || echo "See CHANGELOG.md." > /tmp/notes.md
echo "Release notes for '$HEADER':"; cat /tmp/notes.md
- name: Publish release
env:
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
API: http://xetup-forgejo:3000/api/v1
REPO: ${{ github.repository }}
run: |
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
# Version tag -> named, non-prerelease; anything else -> rolling 'latest'.
case "${{ github.ref }}" in
refs/tags/*)
RELTAG=$(echo "${{ github.ref }}" | sed 's#refs/tags/##')
RELNAME="$RELTAG"; PRERELEASE=false ;;
*)
RELTAG="latest"; RELNAME="latest"; PRERELEASE=true ;;
esac
# Body = changelog section + build footer; built with jq so newlines and
# quotes are escaped safely.
BODY=$(printf '%s\n\n_Built from %s_' "$(cat /tmp/notes.md)" "$SHORT")
PAYLOAD=$(jq -n --arg tag "$RELTAG" --arg name "$RELNAME" \
--arg body "$BODY" --argjson pre "$PRERELEASE" \
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}')
# Replace any existing release for this tag. For rolling 'latest' also
# drop the git tag so it re-points to the new commit; never delete a
# real version tag.
RID=$(curl -sf -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/tags/$RELTAG" | jq -r '.id // empty')
[ -n "$RID" ] && curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$RID" || true
[ "$RELTAG" = "latest" ] && curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/tags/latest" || true
RID=$(curl -sf -X POST \
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$API/repos/$REPO/releases" -d "$PAYLOAD" | jq -r '.id')
curl -sf -X POST \
-H "Authorization: token $TOKEN" -H "Content-Type: application/octet-stream" \
"$API/repos/$REPO/releases/$RID/assets?name=xetup.exe" \
--data-binary @xetup.exe
echo "Released xetup.exe as '$RELNAME' (commit $SHORT)"
- name: Update deploy.json
# Cosmetic "last build" indicator. Requires docker.sock in the job
# container (runner container.docker_host). Non-fatal: the signed
# release is already published by this point, so a failure here must
# not fail the build.
continue-on-error: true
run: |
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
docker exec xetup-web sh -c \
"echo '{\"sha\":\"${SHORT}\",\"ts\":\"${TS}\"}' > /usr/share/nginx/html/data/deploy.json"
echo "deploy.json updated: ${SHORT} at ${TS}"