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>
140 lines
5.6 KiB
YAML
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}"
|