xetup/.forgejo/workflows/release.yml
X9 Dev 853908bedd ci: sign xetup.exe via Azure Trusted Signing (jsign)
Add a signing step after the build that authenticates the Entra service
principal (client_credentials), fetches a Trusted Signing access token, and
signs xetup.exe with jsign using the X9.cz s.r.o. certificate profile plus an
RFC3161 timestamp (timestamp.acs.microsoft.com). jsign is pinned by version
and sha256. Trusted Signing certs are short-lived (~3 days); the timestamp
keeps the signature valid past expiry, so timestamping must succeed and the
step fails hard otherwise.

Only AZURE_CLIENT_SECRET needs to be set as a Forgejo Actions secret; the
non-secret identifiers are inlined in the workflow.

gitignore the local manual-signing helpers (sign*.sh) and the *.unsigned
build backup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:00:12 +02:00

140 lines
5.6 KiB
YAML

name: release
on:
push:
branches: [main]
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
git clone --depth=1 \
"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
run: |
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
GOOS=windows GOARCH=amd64 \
go build -ldflags="-s -w -H windowsgui" -o xetup.exe ./cmd/xetup/
echo "Built: $(ls -lh xetup.exe | awk '{print $5}')"
- 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: Publish latest release
env:
TOKEN: ${{ secrets.FORGEJO_TOKEN }}
API: http://xetup-forgejo:3000/api/v1
REPO: ${{ github.repository }}
run: |
SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
# Delete existing 'latest' release and tag to recreate cleanly
RID=$(curl -sf -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/tags/latest" | jq -r '.id // empty')
if [ -n "$RID" ]; then
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$RID" || true
fi
curl -sf -X DELETE -H "Authorization: token $TOKEN" \
"$API/repos/$REPO/tags/latest" || true
# Create new 'latest' release
RID=$(curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "{\"tag_name\":\"latest\",\"name\":\"latest\",\"body\":\"Auto-built from ${SHORT}\",\"prerelease\":true}" \
| jq -r '.id')
# Upload xetup.exe
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 (commit ${SHORT})"
- name: Update deploy.json
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}"