All checks were successful
release / build-and-release (push) Successful in 43s
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>
191 lines
8.5 KiB
YAML
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}"
|