Skip to content

Commit b4a0cc5

Browse files
ci: run checks and builds on GitHub Actions, with release-intent automation (#87)
* ci: add GitHub Actions checks and unsigned installer builds Runs the header, desktop, and Go test suites on pull requests and pushes to main and develop, plus unsigned installers across a six-way matrix. Windows builds natively rather than cross-compiled under wine. Also runs services/build.sh and build.bat natively on each platform. That is a second, independent build path over the same Go code: it parses versions with jq and stamps -ldflags -X main.Version, where the desktop path cross-compiles through a TypeScript script. Either can break alone. Neither workflow holds a secret or requests an OIDC token: both execute code from the pull request, so there must be no credential for that code to reach. Actions are pinned to full commit SHAs. Signed releases are built elsewhere, from a tag. Signed-off-by: Terve <ntervalon@nvidia.com> * docs: add public changelog seeded from the published releases Records the 0.1.0 and 0.1.1 notes as published on the releases page. This repository had no changelog, so release history was only discoverable there. Seeded from the published notes rather than carrying over historical development entries, and tracks the 0.1.x version that desktop/package.json and the release tags use. Signed-off-by: Terve <ntervalon@nvidia.com> * refactor: rename versions.json product to services, drop installer The field called `product` only ever versioned the services suite: it is declared inside services/, stamps the standalone installer and Go main.Version, and the version users install lives in desktop/package.json, which was explicitly out of scope for it. The two had drifted to 0.91.7 and 0.1.1, which is the clearest evidence they were never one number. `installer` is dropped: documented as 'always equals product', it could never differ from another field. Readers of '.installer // .product' now read '.services'. Updates all nine consumers, including build.sh which was reading .product and would have silently resolved null. The versions card already displayed this as 'Services', so no user-visible string changes. Signed-off-by: Terve <ntervalon@nvidia.com> * ci: add release-intent automation Contributors declare version bumps and changelog text in the pull request body; a check validates it, and after the merge lands on develop a bot applies it to desktop/package.json, services/versions.json, and CHANGELOG.md in one commit via the git data API. The contents API is one commit per file, which would land a release in pieces with a window where the changelog names a version package.json does not carry. The release version is not declared. It patch-bumps automatically whenever an intent declares a release, so the only judgement left is services and component severity; a minor or major release is a deliberate manual bump. validate_pr.py reads the body from the webhook payload, so it needs no credential and runs on fork pull requests. It has its own workflow so it can also trigger on 'edited': a body can change after checks go green, and apply reads the live body at merge time. Apply rejects unknown keys for the same reason, but tolerates missing ones, which is a concurrent pull request adding a component rather than tampering. The app token gets contents: write and not the Workflows permission, so a compromise of the apply job cannot rewrite the pipeline. Ports from the GitLab implementation, dropping its description-truncation fallback, the protected-variable split that fallback needed, and its url.insteadOf workaround — all GitLab Runner specifics. Also corrects the pull request template, which told contributors to hand-edit services/versions.json; the bot-owned path check rejects exactly that, so every pull request would have failed. Signed-off-by: Terve <ntervalon@nvidia.com> * ci: declare toolchain versions once in a composite action Node and Go versions were duplicated between ci.yml and build.yml. GitHub Actions has no shared constants file — env: is per-workflow and one workflow cannot read another — so a local composite action is the only way to declare them once without moving them out of the repository into repository variables, where they would stop being reviewable. Bumping one copy and forgetting the other would have pull request checks and release builds running different toolchains, which is the failure this removes. Inputs keep what each job actually needs: the header check takes Node without an npm cache it has no install to populate, and the build-script job takes Go without Node. Checkout stays in the callers, since a local action cannot exist before its own repository is on disk. Signed-off-by: Terve <ntervalon@nvidia.com> * ci: point setup-go at the module lockfiles setup-go caches by default and looks for a dependency file at the repository root. Every Go module lives under services/, so the action found nothing and reported 'Dependencies file is not found' on each run — a warning in current versions, a hard failure in some earlier ones. Naming the path fixes that and makes the caching real: a cold build pulls roughly 40 seconds of modules, repeated across the services job, the three-platform build-script job, and the installer builds. It also pins the cache key, which is version-dependent otherwise: older setup-go hashes go.sum, newer hashes go.mod. Signed-off-by: Terve <ntervalon@nvidia.com> * ci: assert staged binaries match versions.json, not a count The check asserted 'at least 13'. A floor stops asserting anything the moment the real number grows: add a fourteenth component and it keeps passing, so the check quietly becomes decoration. A hardcoded '-eq 13' fixes that but still only sees the count, and 13 is a magic number with no relationship to where components are actually declared. Compares the staged names against the component keys in versions.json instead. Adding, removing, or renaming a component now fails until the build script and the manifest agree, and the failure names which side is missing what rather than reporting a number. Signed-off-by: Terve <ntervalon@nvidia.com> * docs: regenerate services-api.md after the versions rename The product-to-services rename changed a line that verify-service-contracts generates into docs/services-api.md, but the generated file was not regenerated to match, so the freshness gate failed. Generated output only; produced by npm run service-contracts:write. Signed-off-by: Terve <ntervalon@nvidia.com> * ci: make the staged-binary check immune to line endings The check compared two lists built by different pipelines, so a stray carriage return made every entry differ while printing identically. The failure output then showed the same names on both sides, which reads as nonsense rather than as a diagnosis. Both sides now run through one normalizer, in an order that matters: CR is stripped before the .exe suffix, because under Git Bash a CRLF line leaves .exe mid-string and 's/\.exe$//' never matches. LC_ALL=C makes the sort byte-wise, which is what the comm calls already assumed. On failure it also dumps the raw listing and both lists through 'sed -n l', so an invisible character shows up as \r instead of as two identical lists. Verified against a CRLF versions.json with .exe binaries, a POSIX checkout, a missing component, and an undeclared one. Signed-off-by: Terve <ntervalon@nvidia.com> * ci: fix release-intent triggers and the installer path filter Three review findings. The apply workflow's concurrency group did not serialize applies, it dropped them. GitHub keeps one pending run per group and cancels any earlier one, and cancel-in-progress: false only protects the running job, not the queued one. Because an app installation token push does start workflow runs, the bot's own commit queues a third run that evicts a genuine merge waiting behind it — a lost bump with no failure anywhere. apply_pr.py already handles ordering by committing with force: false and re-reading a moved ref, so the group was an extra failure mode rather than belt and braces. Replaces it with a job-level guard that skips the bot's own pushes without booting a runner. The check workflow gated pull requests into main, but apply only runs on pushes to develop, so a pull request targeting main was required to declare bumps and then had them silently discarded. Restricting the check to develop also unblocks the release cut, whose develop -> main pull request carries the bot's own edits to the two paths check_forbidden_paths rejects. The installer path filter covered .github/workflows/** but not .github/actions/**, so a Node or Go bump in the composite action ran ci.yml on the new toolchain and skipped the six-way matrix. Signed-off-by: Terve <ntervalon@nvidia.com> * fix: rebuild cli-bin when only the services version changes The manifest currency check compared the fingerprint, platform, arch, and component versions but not the services version, so a services-only bump left an existing manifest counting as current and the stale version was what the UI reported in dev. Verified: with no change the build skips as before; bumping only services now rebuilds and rewrites the manifest. Signed-off-by: Terve <ntervalon@nvidia.com> * docs: point contributors at develop and the intent block AGENTS.md, CONTRIBUTING.md, and desktop/docs/services-backend.md all still told contributors to bump services/versions.json by hand, which CI now rejects. services/readme.md and the pull request template were updated when the rule landed; these three were missed. The internal job that used to enforce this is stripped from the public cut, so for public contributors the rule is new and the docs are the only thing announcing it. CONTRIBUTING.md also told contributors to branch from main and never named develop, walking them straight into a pull request whose declared bumps would be discarded on merge. Records four limits that were real but undocumented: apply resolves only the head commit's pull request, so develop must take one merge per push; the apply workflow must not gain a concurrency group; a literal '### ' line truncates a changelog body; and the gate assumes branch protection requires the check. Also notes that the app and its two settings must exist before the first push to develop. Signed-off-by: Terve <ntervalon@nvidia.com> --------- Signed-off-by: Terve <ntervalon@nvidia.com>
1 parent 2bc60e0 commit b4a0cc5

30 files changed

Lines changed: 2113 additions & 77 deletions

‎.github/PULL_REQUEST_TEMPLATE.md‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,40 @@
33
<!-- Explain the problem and the user-visible outcome. -->
44
<!-- Link issues with "Closes #123". -->
55

6+
## Release intent
7+
8+
<!-- Keep the fences exactly as they are; CI parses between them. -->
9+
<!-- Set each bump to none, patch, minor, or major. Leave the key names. -->
10+
<!-- `services` must be >= the highest component bump. -->
11+
<!-- Set the title and body to n/a when no bump is a release. -->
12+
<!-- The release version users see is NOT declared here: it patch-bumps by
13+
itself whenever any bump above is a release. -->
14+
<!-- Keep the changelog body to prose and bullets. A line starting with "### "
15+
ends the section and silently truncates the rest. -->
16+
17+
<!-- pair-release-intent:v1 -->
18+
### Changelog title
19+
n/a
20+
21+
### Changelog body
22+
n/a
23+
24+
### Bumps
25+
- services: none
26+
- nvpair-cluster-manager: none
27+
- nvpair-engine-manager: none
28+
- nvpair-errors: none
29+
- nvpair-job-scheduler: none
30+
- nvpair-manual-nodes: none
31+
- nvpair-node-info: none
32+
- nvpair-node-scanner: none
33+
- nvpair-node-settings: none
34+
- nvpair-proxy: none
35+
- nvpair-tui: none
36+
- nvpair-ui-broker: none
37+
- nvpair-workload-manager: none
38+
<!-- /pair-release-intent:v1 -->
39+
640
## Scope
741

842
<!-- What is intentionally included and excluded? -->
@@ -23,4 +57,4 @@
2357
- [ ] Relevant documentation is updated.
2458
- [ ] I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
2559
- [ ] I recorded the validation commands and results above.
26-
- [ ] I bumped any affected component in `services/versions.json`, and described user-visible changes above so they reach the release notes.
60+
- [ ] I declared version bumps in the release-intent block above. `services/versions.json` is written by automation — do not edit it by hand.

‎.github/actions/setup/action.yml‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# The single place the Node and Go versions are declared.
5+
#
6+
# GitHub Actions has no shared constants file: `env:` is per-workflow and one
7+
# workflow cannot read another. Four workflows exist because each needs a
8+
# distinct `on:` block, so without this they would each carry their own copy of
9+
# the versions, and bumping one while forgetting another would have pull request
10+
# checks and release builds running different toolchains.
11+
#
12+
# The caller must check out the repository first — a local action cannot exist
13+
# until its own repo is on disk, so checkout stays in the workflow.
14+
#
15+
# On a fork pull request this file comes from the fork, like every other script
16+
# those jobs run. That adds no exposure: those jobs already execute the pull
17+
# request's `npm ci` and `go test`, hold no secret, and run on hosted runners.
18+
name: Set up toolchains
19+
description: Installs the pinned Node and Go toolchains used across all workflows.
20+
21+
inputs:
22+
node:
23+
description: Install Node.
24+
default: 'true'
25+
go:
26+
description: Install Go.
27+
default: 'false'
28+
npm-cache:
29+
description: Cache the npm download cache. Pointless without an npm install.
30+
default: 'true'
31+
32+
runs:
33+
using: composite
34+
steps:
35+
# desktop/package.json engines requires >=25.5.0. Pinned to a major
36+
# rather than read from that range, which would drift to whatever Node
37+
# is newest.
38+
- if: inputs.node == 'true'
39+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
40+
with:
41+
node-version: '25'
42+
cache: ${{ inputs.npm-cache == 'true' && 'npm' || '' }}
43+
cache-dependency-path: desktop/package-lock.json
44+
45+
# Forbids the surprise toolchain download, so the version below is the
46+
# one that actually builds. Set before setup-go because setup-go reads
47+
# GOTOOLCHAIN when resolving a version file.
48+
- if: inputs.go == 'true'
49+
shell: bash
50+
run: echo 'GOTOOLCHAIN=local' >> "$GITHUB_ENV"
51+
52+
# Must be >= the highest `go` directive across services/*/go.mod, which
53+
# GOTOOLCHAIN=local would otherwise refuse to satisfy.
54+
#
55+
# cache-dependency-path is required, not an optimization. setup-go
56+
# caches by default and looks for the dependency file at the repository
57+
# root, where there is none — every Go module lives under services/.
58+
# Left unset it reports "Dependencies file is not found" on every run.
59+
# Naming the path also pins the cache key: older versions hash go.sum,
60+
# newer ones hash go.mod.
61+
#
62+
# scripts/ matches nothing today; it is listed so a Go dependency added
63+
# to the support tooling joins the key instead of silently missing it.
64+
- if: inputs.go == 'true'
65+
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
66+
with:
67+
go-version: '1.26.7'
68+
cache-dependency-path: |
69+
services/**/go.sum
70+
scripts/**/go.sum

‎.github/workflows/build.yml‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# Unsigned installer builds on GitHub-hosted runners.
5+
#
6+
# These are verification and pull-request artifacts, never releases: every
7+
# target ends in `electron-builder --publish never`, and this workflow holds no
8+
# signing or publishing credentials. Signed releases are produced separately
9+
# from a tag, and are rebuilt from source rather than signing anything this
10+
# workflow produced.
11+
name: Build
12+
13+
on:
14+
workflow_dispatch:
15+
push:
16+
branches: [main, develop]
17+
pull_request:
18+
branches: [main, develop]
19+
paths:
20+
- '.github/workflows/**'
21+
# The composite action pins the Node and Go versions. Without this
22+
# a toolchain bump would run ci.yml and skip the installer matrix,
23+
# which is the cross-workflow drift the action exists to prevent.
24+
- '.github/actions/**'
25+
- 'scripts/**'
26+
- 'services/**'
27+
- 'desktop/package.json'
28+
- 'desktop/package-lock.json'
29+
- 'desktop/src/**'
30+
- 'desktop/scripts/**'
31+
- 'desktop/native/**'
32+
- 'desktop/resources/**'
33+
- 'desktop/tsconfig*.json'
34+
- 'desktop/vite.*.config.ts'
35+
- 'desktop/electron.vite.config.ts'
36+
- 'desktop/electron-builder.config.ts'
37+
38+
permissions:
39+
contents: read
40+
41+
concurrency:
42+
group: build-${{ github.ref }}
43+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
44+
45+
# Toolchain versions are declared once, in .github/actions/setup.
46+
47+
jobs:
48+
build:
49+
name: ${{ matrix.name }}
50+
runs-on: ${{ matrix.os }}
51+
timeout-minutes: 60
52+
strategy:
53+
# One platform failing should not hide the state of the other five.
54+
fail-fast: false
55+
matrix:
56+
include:
57+
- name: linux-x64
58+
os: ubuntu-latest
59+
script: build:linux:x64
60+
- name: linux-arm64
61+
os: ubuntu-latest
62+
script: build:linux:arm64
63+
- name: win-x64
64+
os: windows-latest
65+
script: build:win:x64
66+
- name: win-arm64
67+
os: windows-latest
68+
script: build:win:arm64
69+
- name: mac-arm64
70+
os: macos-latest
71+
script: build:mac:arm64
72+
- name: mac-x64
73+
os: macos-15-intel
74+
script: build:mac:x64
75+
steps:
76+
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
77+
78+
# Recorded so a failure can be read against the machine it ran on.
79+
- name: Report runner capacity
80+
shell: bash
81+
run: |
82+
echo "label: ${{ matrix.os }}"
83+
echo "kernel: $(uname -s) $(uname -m)"
84+
echo "cpus: $(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo unknown)"
85+
free -h 2>/dev/null || sysctl -n hw.memsize 2>/dev/null || echo "mem: unknown"
86+
df -h . | tail -1
87+
88+
- uses: ./.github/actions/setup
89+
with:
90+
go: 'true'
91+
92+
# electron-builder needs fakeroot and rpm to stage Linux packages;
93+
# ubuntu-latest ships neither.
94+
- name: Install Linux packaging tools
95+
if: runner.os == 'Linux'
96+
run: sudo apt-get update -qq && sudo apt-get install -y -qq fakeroot rpm
97+
98+
- run: npm --prefix desktop ci --prefer-offline
99+
100+
- name: Build ${{ matrix.name }}
101+
run: npm --prefix desktop run ${{ matrix.script }}
102+
103+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
104+
if: always()
105+
with:
106+
name: unsigned-${{ matrix.name }}
107+
path: desktop/release/**
108+
retention-days: 7
109+
if-no-files-found: warn

0 commit comments

Comments
 (0)