Skip to content

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

Merged
Noah-Tervalon-Nvidia merged 13 commits into
developfrom
ci/hosted-runner-workflows
Sep 21, 2026
Merged

Noah-Tervalon-Nvidia merged 13 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
  • 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-proxy: none
  • nvpair-tui: none
  • nvpair-ui-broker: none
  • nvpair-workload-manager: 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>
Comment thread .github/workflows/release-intent-apply.yml Outdated
Comment thread .github/workflows/release-intent-check.yml Outdated
Comment thread services/VERSIONING.md
Comment thread .github/workflows/build.yml
Comment thread desktop/scripts/build-modular-binaries.ts

@mkalkere mkalkere left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the release-intent scripts locally off this branch. test_lib.py: 27/28 (the 28th needs a repo checkout since it reads the real versions.json). validate_pr.py passes on this PR's body. apply_pr.py --dry-run with a release intent gives 0.1.1 -> 0.1.2, services 0.91.7 -> 0.91.8, package.json touched on one line, changelog entry lands in the right spot. Matches the validation section.

A few things:

  1. apply_pr.py only applies the intent for the head commit's PR. If one push to develop ever carries two merges (direct push of a range, merge queue batch), the second intent is silently skipped and nothing fails. The description covers this for main cuts but develop has the same hole. Either iterate the push's commits or document that develop doesn't take batched pushes.

  2. _parse_section ends a section at the first line starting with "### ". A changelog body containing a literal "### " line gets cut silently. Minor, a doc note would do.

  3. The app plus RELEASE_INTENT_APP_ID / RELEASE_INTENT_APP_PRIVATE_KEY have to exist before the first push to develop, or the apply job fails. It fails loud (exit 2) and the README covers backfill, so not a defect, but I'd make it a pre-merge checkbox in the description.

  4. The whole chain assumes branch protection requires the release-intent check. Admin-merging around it with a bad body fails loud at apply (exit 1) and versions just don't bump. Fine, but worth stating as an assumption.

Checked: macos-15-intel is a valid label (good until the macOS 15 image retires in fall 2027), so the mac-x64 leg will run. The product -> services rename covers all nine consumers including both sides of the IPC type, and dropping the installer key is deliberate and tested. Token scoping looks right: one-hour app token, contents:write, no Workflows permission, apply only on push to develop.

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>
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>
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>
@Noah-Tervalon-Nvidia

Noah-Tervalon-Nvidia commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

@mkalkere

Thanks for running it end to end; matching numbers on the dry run is reassuring.

All four are real. Three are now documented as known limits in scripts/release-intent/README.md (e035658) rather than fixed.

Batched pushes — agreed, but since I'll be the one merging everything (along with the team) for now I think not worth the fixes to get there since we will be doing squash commits.

'###' truncation — documented in the README and as a comment in the PR template, right next to the fence where an author would hit it.

Credential before first push — added to the README's credential section, and I've already verified it, but merging is the definitive check.

Branch protection assumption — stated in the README, including the admin-merge behaviour: fails loud at exit 1, versions simply don't bump.

@mkalkere

Copy link
Copy Markdown

Verified the follow-ups — b0c3815, e035658, and 2d87948 all contain exactly what's described above. The diffs read clean.

Documenting the four findings as known limits rather than fixing them is a reasonable call, and the batched-push deferral makes sense while merges stay manual. Only thing I'd keep an eye on: the batched-push case is the one I'd least want to rediscover the hard way, since the skip is silent. It's documented now, so that's a conscious trade rather than a trap — just flagging it in case merge-queue batching ever comes up.

Thanks for the thorough response.

develop unified ollama-proxy and lmstudio-proxy into nvpair-proxy, taking
versions.json from 13 components to 12 and moving product/installer to 1.2.0.

Conflicts were all the same collision: this branch renames product/installer
to a single services key, develop changed the component list underneath it.
Resolved by taking develop's components and this branch's key.

  services/versions.json  services: 1.2.0, develop's 12 components
  services/build.sh       V_SERVICES + nvpair-proxy
  services/build.bat      same

Three follow-ups git could not do:
  - the pull request template's bump list had to move to the 12 new keys, or
    every pull request would fail the strict key check
  - services/nvpair-cluster-manager/spec.md, new on develop, told contributors
    to bump installer and product by hand
  - docs/services-api.md regenerated

The two clocks have not converged: product went to 1.2.0 while
desktop/package.json stayed 0.1.1, so the rename is if anything better
motivated than when it was written.
@Noah-Tervalon-Nvidia
Noah-Tervalon-Nvidia merged commit b4a0cc5 into develop Sep 21, 2026
13 of 14 checks passed
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.

[Task]: No public CI workflows, and the Makefile references a ci/pipeline.yml that is not in the repository

3 participants