feat: operational tooling — signing, versioning, changelog, roadmap (SPEC-001)
Establish GuruConnect's release engineering and project tracking (SPEC-001): - docs/ scaffold: FEATURE_ROADMAP, ARCHITECTURE_DECISIONS (ADR-001 standalone+contract, ADR-002 Gitea Actions + Azure Trusted Signing), docs/specs/SPEC-001, CHANGELOG. - .gitea/workflows/release.yml: conventional-commit auto-versioning, git-cliff changelog, Windows agent build, Azure Trusted Signing via jsign (reusing the shared ACG cert profile), Gitea release via REST API. build-and-test.yml is the PR/push gate; deploy.yml de-duplicated. - server: GET /api/changelog/:component/:version (latest + by-version), path-traversal hardened. - cliff.toml; server/.env.example documents CHANGELOG_DIR. Reviewed (Code Review Agent): axum route-conflict blocker fixed; CHANGELOG ordering, toolchain target, breaking-change parsing, empty-changelog fallback addressed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
name: Build and Test
|
||||
|
||||
# PR/push CI gate (SPEC-001): fmt, clippy -D warnings, build, test, cargo-audit.
|
||||
# This workflow does NOT version, sign, or release — that is release.yml's job. The agent build
|
||||
# here is a compile gate only (it produces an unsigned artifact for inspection). Release commits
|
||||
# carry `[skip ci]` so this workflow does not re-run on the version-bump commit.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -77,7 +82,8 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
# Single source of truth for the Windows target used by the build below.
|
||||
target: x86_64-pc-windows-gnu
|
||||
override: true
|
||||
|
||||
- name: Install cross-compilation tools
|
||||
@@ -100,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Build agent (cross-compile for Windows)
|
||||
run: |
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
# Target is installed by the toolchain step above (single source of truth).
|
||||
cd agent
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
name: Deploy to Production
|
||||
|
||||
# Server deployment only. Release creation and agent signing live in release.yml (SPEC-001) —
|
||||
# this workflow no longer creates releases, so there is exactly one release producer in the repo.
|
||||
#
|
||||
# Triggers on a pushed vX.Y.Z tag (which release.yml creates) or manual dispatch. The previous
|
||||
# GitHub-only `actions/create-release@v1` + GITHUB_TOKEN job has been removed; it does not work on
|
||||
# Gitea. Gitea releases are produced by release.yml via the Gitea REST API.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -30,6 +37,11 @@ jobs:
|
||||
toolchain: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pkg-config libssl-dev protobuf-compiler
|
||||
|
||||
- name: Build server
|
||||
run: |
|
||||
cd server
|
||||
@@ -37,12 +49,17 @@ jobs:
|
||||
|
||||
- name: Create deployment package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p deploy
|
||||
cp server/target/x86_64-unknown-linux-gnu/release/guruconnect-server deploy/
|
||||
cp -r server/static deploy/
|
||||
cp -r server/migrations deploy/
|
||||
# Ship generated changelogs so the server's /api/changelog endpoint can serve them
|
||||
# (CHANGELOG_DIR points at this directory in production).
|
||||
if [ -d changelogs ]; then cp -r changelogs deploy/; fi
|
||||
cp server/.env.example deploy/.env.example
|
||||
tar -czf guruconnect-server-${{ github.ref_name }}.tar.gz -C deploy .
|
||||
echo "[OK] Packaged guruconnect-server-${{ github.ref_name }}.tar.gz"
|
||||
|
||||
- name: Upload deployment package
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -54,35 +71,8 @@ jobs:
|
||||
- name: Deploy to server (production)
|
||||
if: github.event.inputs.environment == 'production' || startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
echo "Deployment command would run here"
|
||||
echo "SSH to 172.16.3.30 and deploy"
|
||||
echo "[INFO] Deployment command would run here"
|
||||
echo "[INFO] SSH to 172.16.3.30 and deploy"
|
||||
# Actual deployment would use SSH keys and run:
|
||||
# scp guruconnect-server-*.tar.gz guru@172.16.3.30:/tmp/
|
||||
# ssh guru@172.16.3.30 'bash /home/guru/guru-connect/scripts/deploy.sh'
|
||||
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy-server
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Create Release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
release_name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload Release Assets
|
||||
run: |
|
||||
echo "Upload server and agent binaries to release"
|
||||
# Would attach artifacts to the release here
|
||||
|
||||
484
.gitea/workflows/release.yml
Normal file
484
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,484 @@
|
||||
name: Release
|
||||
|
||||
# SPEC-001 §2/§3/§4 — auto-versioning, signed Windows build, changelog generation, release.
|
||||
#
|
||||
# On every push to main this workflow:
|
||||
# 1. version — determine the next semver from conventional commits, bump component manifests,
|
||||
# commit `chore: release vX.Y.Z [skip ci]`, and create + push tag vX.Y.Z.
|
||||
# 2. changelog — generate CHANGELOG.md + per-component changelogs with git-cliff (run inside
|
||||
# the version job so it is part of the release commit).
|
||||
# 3. build — cross-compile the Windows agent (x86_64-pc-windows-gnu) to guruconnect.exe.
|
||||
# 4. sign — sign guruconnect.exe with Azure Trusted Signing via jsign (fails the job if
|
||||
# signing fails — never publish unsigned).
|
||||
# 5. publish — upload signed exe + .sha256 + changelog artifacts; create a Gitea release.
|
||||
#
|
||||
# Loop guard: the workflow skips entirely when the head commit is a release commit
|
||||
# (`chore: release` / `[skip ci]`), and the release commit itself carries `[skip ci]`.
|
||||
#
|
||||
# All jobs run on ubuntu-latest. GuruConnect ships a single .exe (no WiX/MSI); jsign is a Java
|
||||
# tool that signs PE binaries on Linux, so no Windows runner is required.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# §3 VERSION + §4 CHANGELOG
|
||||
# ---------------------------------------------------------------------------
|
||||
version:
|
||||
name: Version + Changelog
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.bump.outputs.version }}
|
||||
released: ${{ steps.bump.outputs.released }}
|
||||
steps:
|
||||
- name: Checkout (full history + tags)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# CI_PUSH_TOKEN is a push-capable token used to commit the bump and push the tag.
|
||||
token: ${{ secrets.CI_PUSH_TOKEN }}
|
||||
|
||||
- name: Loop guard - skip release commits
|
||||
id: guard
|
||||
run: |
|
||||
HEAD_MSG="$(git log -1 --pretty=%s)"
|
||||
echo "[INFO] HEAD commit subject: ${HEAD_MSG}"
|
||||
if echo "${HEAD_MSG}" | grep -qiE '\[skip ci\]|^chore: release'; then
|
||||
echo "[INFO] Head commit is a release/skip-ci commit; skipping release."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Install git-cliff
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CLIFF_VERSION="2.6.1"
|
||||
URL="https://github.com/orhun/git-cliff/releases/download/v${CLIFF_VERSION}/git-cliff-${CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
echo "[INFO] Downloading git-cliff ${CLIFF_VERSION}"
|
||||
curl -fsSL "$URL" -o /tmp/git-cliff.tar.gz
|
||||
tar -xzf /tmp/git-cliff.tar.gz -C /tmp
|
||||
sudo install -m 0755 "/tmp/git-cliff-${CLIFF_VERSION}/git-cliff" /usr/local/bin/git-cliff
|
||||
git-cliff --version
|
||||
|
||||
- name: Determine next version and bump components
|
||||
id: bump
|
||||
if: steps.guard.outputs.skip != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# ----- locate the last release tag (vX.Y.Z) -----
|
||||
LAST_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n1 || true)"
|
||||
if [ -z "${LAST_TAG}" ]; then
|
||||
echo "[INFO] No prior release tag found; baseline is current manifest version."
|
||||
BASE_VERSION="$(grep -m1 '^version' agent/Cargo.toml | sed -E 's/.*"([0-9]+\.[0-9]+\.[0-9]+)".*/\1/')"
|
||||
RANGE=""
|
||||
else
|
||||
echo "[INFO] Last release tag: ${LAST_TAG}"
|
||||
BASE_VERSION="${LAST_TAG#v}"
|
||||
RANGE="${LAST_TAG}..HEAD"
|
||||
fi
|
||||
echo "[INFO] Base version: ${BASE_VERSION}"
|
||||
|
||||
# ----- collect commit subjects (and full bodies) since the last tag -----
|
||||
if [ -n "${RANGE}" ]; then
|
||||
COMMITS="$(git log "${RANGE}" --pretty=%s || true)"
|
||||
# Full messages, NUL-delimited, for BREAKING CHANGE footer detection.
|
||||
BODIES="$(git log "${RANGE}" --pretty=%B || true)"
|
||||
CHANGED_FILES="$(git diff --name-only "${LAST_TAG}" HEAD || true)"
|
||||
else
|
||||
COMMITS="$(git log --pretty=%s || true)"
|
||||
BODIES="$(git log --pretty=%B || true)"
|
||||
CHANGED_FILES="$(git ls-files)"
|
||||
fi
|
||||
|
||||
# ----- determine bump level from conventional commits -----
|
||||
# Breaking changes (pre-1.0): a `!` before the colon on any type/scope (feat!:, fix!:,
|
||||
# type(scope)!:) or a `BREAKING CHANGE` footer forces at least a MINOR bump. We do NOT
|
||||
# force a major bump while the project is pre-1.0; existing feat->minor, fix/perf->patch
|
||||
# logic is preserved otherwise.
|
||||
BUMP="none"
|
||||
while IFS= read -r line; do
|
||||
if [ -z "$line" ]; then continue; fi
|
||||
case "$line" in
|
||||
# `<type>!:` or `<type>(scope)!:` — breaking change marker in the subject.
|
||||
*'!:'*)
|
||||
BUMP="minor"
|
||||
;;
|
||||
feat:*|feat\(*)
|
||||
BUMP="minor"
|
||||
;;
|
||||
fix:*|fix\(*|perf:*|perf\(*)
|
||||
if [ "$BUMP" != "minor" ]; then BUMP="patch"; fi
|
||||
;;
|
||||
esac
|
||||
done <<< "$COMMITS"
|
||||
|
||||
# `BREAKING CHANGE:` / `BREAKING-CHANGE:` footer anywhere in a commit body -> at least minor.
|
||||
if printf '%s' "$BODIES" | grep -qE 'BREAKING[ -]CHANGE'; then
|
||||
echo "[INFO] BREAKING CHANGE footer detected; bumping at least minor (pre-1.0)."
|
||||
BUMP="minor"
|
||||
fi
|
||||
|
||||
# If only chores/docs but code actually changed, default to patch.
|
||||
if [ "$BUMP" = "none" ]; then
|
||||
if echo "$CHANGED_FILES" | grep -qE '^(agent/|server/|dashboard/|proto/)'; then
|
||||
echo "[INFO] Only chores in commits but code changed; defaulting to patch."
|
||||
BUMP="patch"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$BUMP" = "none" ]; then
|
||||
echo "[INFO] No release-worthy changes detected; skipping release."
|
||||
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "[INFO] Bump level: ${BUMP}"
|
||||
|
||||
# ----- compute next version -----
|
||||
IFS='.' read -r MAJ MIN PAT <<< "${BASE_VERSION}"
|
||||
case "$BUMP" in
|
||||
minor) MIN=$((MIN + 1)); PAT=0 ;;
|
||||
patch) PAT=$((PAT + 1)) ;;
|
||||
esac
|
||||
NEXT="${MAJ}.${MIN}.${PAT}"
|
||||
echo "[INFO] Next version: ${NEXT}"
|
||||
echo "version=${NEXT}" >> "$GITHUB_OUTPUT"
|
||||
echo "released=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ----- determine which components changed (bump only those) -----
|
||||
agent_changed=false; server_changed=false; dashboard_changed=false
|
||||
echo "$CHANGED_FILES" | grep -qE '^(agent/|proto/)' && agent_changed=true || true
|
||||
echo "$CHANGED_FILES" | grep -qE '^(server/|proto/)' && server_changed=true || true
|
||||
echo "$CHANGED_FILES" | grep -qE '^dashboard/' && dashboard_changed=true || true
|
||||
# On the very first release (no prior tag) bump all components together.
|
||||
if [ -z "${RANGE}" ]; then
|
||||
agent_changed=true; server_changed=true; dashboard_changed=true
|
||||
fi
|
||||
echo "[INFO] Changed: agent=${agent_changed} server=${server_changed} dashboard=${dashboard_changed}"
|
||||
|
||||
# ----- bump manifests for changed components -----
|
||||
bump_cargo() {
|
||||
local file="$1"
|
||||
# Replace only the first top-level `version = "x.y.z"` (the [package] version).
|
||||
sed -i -E "0,/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/s//version = \"${NEXT}\"/" "$file"
|
||||
echo "[OK] Bumped ${file} -> ${NEXT}"
|
||||
}
|
||||
if [ "$agent_changed" = "true" ]; then bump_cargo agent/Cargo.toml; fi
|
||||
if [ "$server_changed" = "true" ]; then bump_cargo server/Cargo.toml; fi
|
||||
if [ "$dashboard_changed" = "true" ]; then
|
||||
# Bump the "version" field in dashboard/package.json without extra tooling.
|
||||
sed -i -E "0,/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/s//\"version\": \"${NEXT}\"/" dashboard/package.json
|
||||
echo "[OK] Bumped dashboard/package.json -> ${NEXT}"
|
||||
fi
|
||||
|
||||
# Keep the workspace version in sync (Cargo.toml [workspace.package]).
|
||||
if [ -f Cargo.toml ]; then
|
||||
sed -i -E "0,/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/s//version = \"${NEXT}\"/" Cargo.toml || true
|
||||
fi
|
||||
|
||||
- name: Generate changelog (git-cliff)
|
||||
if: steps.guard.outputs.skip != 'true' && steps.bump.outputs.released == 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.bump.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p changelogs
|
||||
|
||||
# Per-version notes: only the NEW version's section (unreleased commits, tagged). Strip
|
||||
# the header AND footer so this fragment is reusable for per-component files (it must not
|
||||
# carry the # Changelog preamble or the [0.1.0] footer).
|
||||
git-cliff --config cliff.toml --tag "v${VERSION}" --unreleased --strip all \
|
||||
--output /tmp/version-notes.md
|
||||
echo "[OK] Generated per-version notes"
|
||||
|
||||
# Fallback when the version block has no user-facing entries (chore/docs-only release):
|
||||
# ensure the per-component files and release notes are never effectively empty.
|
||||
if ! grep -qE '^- ' /tmp/version-notes.md; then
|
||||
echo "[INFO] No user-facing changelog entries; using maintenance fallback line."
|
||||
{
|
||||
printf '## [%s] - %s\n\n' "${VERSION}" "$(date -u +%Y-%m-%d)"
|
||||
printf -- '- Maintenance release — no user-facing changes.\n'
|
||||
} > /tmp/version-notes.md
|
||||
fi
|
||||
|
||||
# Regenerate the WHOLE CHANGELOG.md over full history with --output (NOT --prepend).
|
||||
# git-cliff emits: header (# Changelog preamble) -> version blocks newest-first ->
|
||||
# footer ([0.1.0] carried verbatim from cliff.toml). This is idempotent and always
|
||||
# well-formed; --prepend would insert the new block above the # Changelog title.
|
||||
echo "[INFO] Regenerating CHANGELOG.md over full history (tag v${VERSION})"
|
||||
git-cliff --config cliff.toml --tag "v${VERSION}" --output CHANGELOG.md
|
||||
echo "[OK] Updated CHANGELOG.md"
|
||||
|
||||
# Write per-component + LATEST files for each component that changed.
|
||||
write_component() {
|
||||
local comp="$1"
|
||||
local upper
|
||||
upper="$(echo "$comp" | tr '[:lower:]' '[:upper:]')"
|
||||
mkdir -p "changelogs/${comp}"
|
||||
cp /tmp/version-notes.md "changelogs/${comp}/v${VERSION}.md"
|
||||
cp /tmp/version-notes.md "changelogs/LATEST_${upper}.md"
|
||||
echo "[OK] Wrote changelogs/${comp}/v${VERSION}.md and changelogs/LATEST_${upper}.md"
|
||||
}
|
||||
|
||||
# Re-derive the set of changed components (same logic as the bump step). On the first
|
||||
# release (no prior tag) all components are considered changed.
|
||||
LAST_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n1 || true)"
|
||||
if [ -z "${LAST_TAG}" ]; then
|
||||
CHANGED_FILES="$(git ls-files)"
|
||||
FIRST_RELEASE=true
|
||||
else
|
||||
CHANGED_FILES="$(git diff --name-only "${LAST_TAG}" HEAD || true)"
|
||||
FIRST_RELEASE=false
|
||||
fi
|
||||
|
||||
if [ "$FIRST_RELEASE" = "true" ]; then
|
||||
write_component agent
|
||||
write_component server
|
||||
write_component dashboard
|
||||
else
|
||||
echo "$CHANGED_FILES" | grep -qE '^(agent/|proto/)' && write_component agent || true
|
||||
echo "$CHANGED_FILES" | grep -qE '^(server/|proto/)' && write_component server || true
|
||||
echo "$CHANGED_FILES" | grep -qE '^dashboard/' && write_component dashboard || true
|
||||
fi
|
||||
|
||||
- name: Commit release + create tag
|
||||
if: steps.guard.outputs.skip != 'true' && steps.bump.outputs.released == 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.bump.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "guruconnect-ci"
|
||||
git config user.email "ci@azcomputerguru.com"
|
||||
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "[WARNING] No changes staged for release commit; skipping commit/tag."
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "chore: release v${VERSION} [skip ci]"
|
||||
git tag "v${VERSION}"
|
||||
|
||||
# Push commit and tag using CI_PUSH_TOKEN embedded in the remote URL.
|
||||
# GITHUB_REPOSITORY is provided by the Actions runner (Gitea-compatible).
|
||||
REMOTE="https://${{ secrets.CI_PUSH_TOKEN }}@git.azcomputerguru.com/${GITHUB_REPOSITORY}.git"
|
||||
git push "${REMOTE}" "HEAD:${GITHUB_REF_NAME}"
|
||||
git push "${REMOTE}" "v${VERSION}"
|
||||
echo "[OK] Pushed release commit and tag v${VERSION}"
|
||||
|
||||
- name: Upload changelog artifact
|
||||
if: steps.guard.outputs.skip != 'true' && steps.bump.outputs.released == 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: changelog
|
||||
path: |
|
||||
CHANGELOG.md
|
||||
changelogs/
|
||||
retention-days: 90
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §2 BUILD + SIGN + PUBLISH
|
||||
# ---------------------------------------------------------------------------
|
||||
build-sign-publish:
|
||||
name: Build, Sign, Publish Agent
|
||||
runs-on: ubuntu-latest
|
||||
needs: version
|
||||
if: needs.version.outputs.released == 'true'
|
||||
steps:
|
||||
- name: Checkout the release tag
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Build the exact commit that was tagged (the release commit), not the pre-bump head.
|
||||
ref: v${{ needs.version.outputs.version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
# Single source of truth for the Windows target used by the build below.
|
||||
target: x86_64-pc-windows-gnu
|
||||
override: true
|
||||
|
||||
- name: Install cross-compilation tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mingw-w64
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-agent-release-${{ hashFiles('agent/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-agent-release-
|
||||
${{ runner.os }}-cargo-agent-
|
||||
|
||||
- name: Build agent (cross-compile for Windows)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Target is installed by the toolchain step above (single source of truth).
|
||||
cd agent
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
echo "[OK] Built agent for x86_64-pc-windows-gnu"
|
||||
|
||||
- name: Stage unsigned binary
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp agent/target/x86_64-pc-windows-gnu/release/guruconnect.exe ./guruconnect.exe
|
||||
ls -l ./guruconnect.exe
|
||||
|
||||
# --- §2 Azure Trusted Signing (port of sign-windows.sh) ---
|
||||
- name: Acquire Azure Trusted Signing token
|
||||
id: token
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "[INFO] Requesting Azure code-signing access token"
|
||||
RESP="$(curl -fsS -X POST \
|
||||
"https://login.microsoftonline.com/${{ secrets.AZURE_TENANT_ID }}/oauth2/v2.0/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "grant_type=client_credentials" \
|
||||
--data-urlencode "client_id=${{ secrets.AZURE_CLIENT_ID }}" \
|
||||
--data-urlencode "client_secret=${{ secrets.AZURE_CLIENT_SECRET }}" \
|
||||
--data-urlencode "scope=https://codesigning.azure.net/.default")"
|
||||
TOKEN="$(printf '%s' "$RESP" | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')"
|
||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "None" ]; then
|
||||
echo "[ERROR] Failed to acquire Azure Trusted Signing token"
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Acquired signing token"
|
||||
# Mask and export the token for the sign step without printing it.
|
||||
echo "::add-mask::${TOKEN}"
|
||||
echo "TS_TOKEN=${TOKEN}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install JRE + jsign
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y default-jre-headless
|
||||
JSIGN_VERSION="6.0"
|
||||
curl -fsSL "https://github.com/ebourg/jsign/releases/download/${JSIGN_VERSION}/jsign-${JSIGN_VERSION}.jar" \
|
||||
-o /tmp/jsign.jar
|
||||
echo "[OK] Installed JRE and jsign ${JSIGN_VERSION}"
|
||||
|
||||
- name: Sign guruconnect.exe (Azure Trusted Signing)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "[INFO] Signing guruconnect.exe with Azure Trusted Signing"
|
||||
java -jar /tmp/jsign.jar \
|
||||
--storetype TRUSTEDSIGNING \
|
||||
--keystore "${{ secrets.TS_ENDPOINT }}" \
|
||||
--storepass "${TS_TOKEN}" \
|
||||
--alias "${{ secrets.TS_ACCOUNT }}/${{ secrets.TS_CERT_PROFILE }}" \
|
||||
--tsaurl "${{ secrets.TS_TIMESTAMP_URL }}" \
|
||||
--tsmode RFC3161 \
|
||||
--alg SHA-256 \
|
||||
--name "GuruConnect Agent" \
|
||||
--url "https://www.azcomputerguru.com" \
|
||||
--replace \
|
||||
guruconnect.exe
|
||||
echo "[OK] Signing command completed"
|
||||
|
||||
- name: Verify signature present (fail release if unsigned)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "[INFO] Verifying Authenticode signature is present"
|
||||
# jsign's --info on a signed PE lists the signature(s); fail if none reported.
|
||||
OUT="$(java -jar /tmp/jsign.jar --info guruconnect.exe 2>&1 || true)"
|
||||
echo "$OUT"
|
||||
if echo "$OUT" | grep -qiE 'signature|signer|signed'; then
|
||||
echo "[OK] Signature present"
|
||||
else
|
||||
echo "[ERROR] No signature detected on guruconnect.exe - refusing to publish unsigned binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compute SHA-256 of signed binary
|
||||
id: sha
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sha256sum guruconnect.exe | awk '{print $1}' > guruconnect.exe.sha256
|
||||
SUM="$(cat guruconnect.exe.sha256)"
|
||||
echo "[OK] SHA-256: ${SUM}"
|
||||
echo "sha256=${SUM}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download changelog artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: changelog
|
||||
path: changelog-artifact
|
||||
|
||||
- name: Upload signed agent artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: guruconnect-agent-signed
|
||||
path: |
|
||||
guruconnect.exe
|
||||
guruconnect.exe.sha256
|
||||
retention-days: 90
|
||||
|
||||
# --- §2/§4 PUBLISH: create a Gitea release and attach assets ---
|
||||
#
|
||||
# Gitea release mechanism (decision): the GitHub-only actions/create-release@v1 +
|
||||
# GITHUB_TOKEN flow used by the old deploy.yml does NOT work on Gitea. We use the Gitea
|
||||
# REST API directly via curl, which is guaranteed available on the ubuntu-latest runner and
|
||||
# does not depend on a third-party action being registered in this Gitea instance.
|
||||
# POST /api/v1/repos/{owner}/{repo}/releases (create release for the tag)
|
||||
# POST /api/v1/repos/{owner}/{repo}/releases/{id}/assets (attach each asset)
|
||||
# Auth: CI_PUSH_TOKEN (token=...). GITHUB_REPOSITORY / GITHUB_SERVER_URL come from the runner.
|
||||
- name: Create Gitea release and upload assets
|
||||
env:
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
SHA256: ${{ steps.sha.outputs.sha256 }}
|
||||
GITEA_TOKEN: ${{ secrets.CI_PUSH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
API_BASE="https://git.azcomputerguru.com/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
TAG="v${VERSION}"
|
||||
echo "[INFO] Creating Gitea release ${TAG} on ${GITHUB_REPOSITORY}"
|
||||
|
||||
BODY="$(printf 'GuruConnect %s\n\nSHA-256 (guruconnect.exe): %s\n\nSee CHANGELOG.md and /api/changelog for details.' "${TAG}" "${SHA256}")"
|
||||
|
||||
# Build the JSON payload with python (handles escaping of the multi-line body safely).
|
||||
CREATE_PAYLOAD="$(TAG="$TAG" BODY="$BODY" python3 -c 'import json,os; print(json.dumps({"tag_name": os.environ["TAG"], "name": "Release " + os.environ["TAG"], "body": os.environ["BODY"], "draft": False, "prerelease": False}))')"
|
||||
|
||||
RELEASE_JSON="$(curl -fsS -X POST \
|
||||
"${API_BASE}/releases" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${CREATE_PAYLOAD}")"
|
||||
|
||||
RELEASE_ID="$(printf '%s' "$RELEASE_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')"
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "None" ]; then
|
||||
echo "[ERROR] Failed to create Gitea release"
|
||||
echo "$RELEASE_JSON"
|
||||
exit 1
|
||||
fi
|
||||
echo "[OK] Created release id=${RELEASE_ID}"
|
||||
|
||||
upload_asset() {
|
||||
local file="$1"
|
||||
echo "[INFO] Uploading asset: ${file}"
|
||||
curl -fsS -X POST \
|
||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=$(basename "$file")" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${file}" > /dev/null
|
||||
echo "[OK] Uploaded ${file}"
|
||||
}
|
||||
upload_asset guruconnect.exe
|
||||
upload_asset guruconnect.exe.sha256
|
||||
if [ -f changelog-artifact/CHANGELOG.md ]; then
|
||||
upload_asset changelog-artifact/CHANGELOG.md
|
||||
fi
|
||||
echo "[OK] Release ${TAG} published with signed binary, checksum, and changelog"
|
||||
Reference in New Issue
Block a user