Skip to content

ci: run checks and builds on GitHub Actions, with release-intent automation - #87

Open
Noah-Tervalon-Nvidia wants to merge 9 commits into
developfrom
ci/hosted-runner-workflows
Open

Noah-Tervalon-Nvidia wants to merge 9 commits into
developfrom
ci/hosted-runner-workflows

Conversation

@Noah-Tervalon-Nvidia

@Noah-Tervalon-Nvidia Noah-Tervalon-Nvidia commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Description

Moves checks and unsigned installer builds to GitHub Actions on GitHub-hosted
runners, and ports the release-intent automation that keeps version bumps and
changelog entries out of merge-conflict territory.

Self-hosted runners cannot serve a public repository: on a fork pull request
the workflow file comes from the contributor's merge ref, so runs-on is
attacker-chosen. Everything here runs on hosted runners and holds no secret,
so there is no credential for pull request code to reach. Signed releases stay
on internal infrastructure and are built from a tag.

Also renames the versions.json field product to services. It only ever
versioned the services suite — it stamps the standalone installer and Go
main.Version, while 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.

Resolves #22

Release intent

Changelog title

n/a

Changelog body

n/a

Bumps

  • services: none
  • lmstudio-proxy: none
  • nvpair-cluster-manager: none
  • nvpair-engine-manager: none
  • nvpair-errors: none
  • nvpair-job-scheduler: none
  • nvpair-manual-nodes: none
  • nvpair-node-info: none
  • nvpair-node-scanner: none
  • nvpair-node-settings: none
  • nvpair-tui: none
  • nvpair-ui-broker: none
  • nvpair-workload-manager: none
  • ollama-proxy: none

Scope

Included: workflows, the release-intent scripts, the product to services
rename across all nine consumers, a changelog seeded from the published 0.1.0
and 0.1.1 release notes.

Excluded: signing, publishing, and release. Those stay on internal
infrastructure and are a separate piece of work.

Validation

Locally, on this branch:

  • python3 scripts/release-intent/test_lib.py — 28 tests
  • npm --prefix desktop run typecheck — clean
  • npm --prefix desktop run lint — 0 errors
  • npm --prefix desktop run dead-code:check — no dead code
  • npm --prefix desktop run test:unit — 208 tests
  • actionlint on all four workflows — clean
  • node scripts/spdx-headers.mjs — 0 missing
  • apply_pr.py --dry-run produces 0.1.1 -> 0.1.2 and a correctly numbered
    changelog section

No workflow has executed before this pull request. The six-way build matrix is
unproven, and two legs are worth watching: Windows builds natively rather than
cross-compiled under wine, and macos-latest is the smallest box in the matrix
at 3 cores and 7 GB.

Risk

All bumps are none: no Go source changed, so no compiled output changes and
this is not a release.

services/versions.json and CHANGELOG.md are modified, which the new
bot-owned path check rejects by design. The allow-owned-files marker at the
top of this description is the documented override for a schema change the bot
cannot make itself.

Release intent apply does not run on a pull request. It first runs on merge
to develop, and needs the GitHub App configured before then or it will fail.

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s).
  • New or existing tests cover the change.
  • Relevant documentation is updated.
  • I checked the diff, changed filenames, and commit messages for
    credentials, private data, internal URLs, internal issue identifiers, and
    generated artifacts.
  • I recorded the validation commands and results above.
  • I declared version bumps in the release-intent block above.

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>
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>
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>
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>
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>
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>
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>
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant