Skip to content

docs: release process; feat(template): registry-auth-id + port-labels (DR-1399) - #297

Merged
justinwlin merged 9 commits into
mainfrom
docs/release-process
Jul 13, 2026
Merged

docs: release process; feat(template): registry-auth-id + port-labels (DR-1399)#297
justinwlin merged 9 commits into
mainfrom
docs/release-process

Conversation

@justinwlin

@justinwlin justinwlin commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Two independent changes, bundled per request:

  1. docs: a "release process" section in the README.
  2. feat (DR-1399): the template command's missing flags — --registry-auth-id and --port-labels.

Linear: DR-1399 (https://linear.app/runpod/issue/DR-1399) — leave open, the release ships later. Not using a closing keyword on purpose.


1. docs: release process

Documents the goreleaser flow so it isn't tribal knowledge: push a vX.Y.Z tag → the release workflow runs goreleaser → builds binaries + the GitHub release and auto-opens 2 PRs on runpod/homebrew-runpodctl (formula + cask) that a human merges. Verified accurate against release.yml, .goreleaser.yml, and the live tap repo.

2. feat: template flags (DR-1399)

The ticket reported two real gaps, both confirmed against the code and fixed:

--registry-auth-id on create + update

Templates couldn't attach a container registry auth, so private-image pulls failed and users had to fix it in the console after every create. pod create already had this flag; templates didn't. Added to both commands (REST field containerRegistryAuthId). On update it's a *string, so passing an empty value clears the auth.

--port-labels on create + update

--ports "22/tcp,8888/http" created the port mappings but dropped the human-readable labels shown in the dashboard. Labels live in GraphQL's portsConfig, not the public REST template schema — so:

  • A new --port-labels flag accepts port=name pairs ("22=ssh,8888=jupyter lab") or JSON, validated against --ports.
  • Labels are applied with a follow-up GraphQL saveTemplate that re-reads the template's full save-state (retrying briefly on stale reads right after a create) and merges in the values just written over REST, so a fresh write isn't reverted.
  • create is atomic: if the label step fails, the just-created template is deleted so you don't end up with a half-configured template.

template get now shows labels

GetTemplate reads REST first (no label field), so labels were saved but never echoed back. It now backfills portsConfig from GraphQL when a user-owned template has ports but REST returned no labels.

Where the code lives

  • cmd/template/create.go, update.go, port_labels.go — flags, parsing, validation, orchestration
  • internal/api/template_port_labels.go — GraphQL saveTemplate + save-state reconstruction
  • internal/api/templates.gocontainerRegistryAuthId / portsConfig fields + the get backfill

Approach adapted from the customer's fork (kodxana/runpodctl), rebased onto current main and split into reviewable commits.


How this was validated

Automated: go build ./..., go vet ./..., go test ./... — all green. New unit tests cover registry-auth marshalling + clear, port-label parse/validate, save-state preservation, and the get backfill.

Live e2e against the Runpod API (every result confirmed by querying the GraphQL server directly, not just CLI output; all temp templates deleted afterward):

Scenario Result
create with --registry-auth-id containerRegistryAuthId persists server-side
create with --port-labels portsConfig persists server-side (shows in dashboard)
update --port-labels (rename a label) server reflects the new label
template get after create now returns portsConfig
create with --docker-start-cmd + --docker-entrypoint and --port-labels docker entrypoint/start-cmd survive the label save (checked because the save rewrites the whole template via GraphQL dockerArgs; confirmed no data loss)

Reviewer notes / known trade-offs

  • The port-labels-only update path and the get backfill each make an extra GraphQL round-trip. Accepted: labels aren't in REST, so a second read is unavoidable; the backfill only fires when a template actually has ports.
  • The customer-facing skill repo (runpod/skills/runpodctl) should get the two new flags documented — separate repo, follow-up, not in this PR.
  • "env override and disk bugs" from the ticket title is not template-side; that's pod-create template inheritance, tracked/handled separately in fix: pod create template inheritance for containerDiskInGb, volumeInGb, and env overrides #294.

@justinwlin justinwlin changed the title docs: document the release process in readme docs: release process; feat(template): registry-auth-id + port-labels (DR-1399) Jun 30, 2026
@justinwlin
justinwlin marked this pull request as ready for review July 1, 2026 14:51
@justinwlin
justinwlin force-pushed the docs/release-process branch from b60d7aa to c6c7208 Compare July 1, 2026 14:53
@promptless

promptless Bot commented Jul 1, 2026

Copy link
Copy Markdown

Promptless prepared a documentation update related to this change.

Triggered by runpodctl PR #297

Updated the runpodctl template CLI reference to document the new --registry-auth-id and --port-labels flags on template create and template update (the port=name/JSON label syntax, --ports validation, and empty-value clear/detach behavior on update), and noted that template get now shows port labels.

Review: Document runpodctl template registry-auth-id and port-labels flags

@justinwlin

Copy link
Copy Markdown
Contributor Author

Review follow-up + live e2e validation

The review put a hold on this PR, driven mainly by one fear about the port-labels path. I validated it against the live API and it turned out not to be real; the smaller items are addressed in commit bf1e549.

Top concern — "a label write silently wipes fields" → refuted by live test

The worry: labels aren't in the REST schema, so they're applied via a GraphQL saveTemplate that reads ~18 template fields and rewrites the whole object. Static reading suggested any field not in that read (notably dockerEntrypoint / dockerStartCmd) would be blanked on the label write.

Live test on a real template:

  1. created a template with dockerStartCmd=python,-u,handler.py, dockerEntrypoint=/bin/bash,-c, env vars, ports, disk, readme (no labels)
  2. applied --port-labels "22=ssh,8888=jupyter" (this runs the read-whole/write-whole GraphQL path)
  3. re-fetched fresh from the server

Everything survived — start command, entrypoint, env, ports, disk, readme — plus the new labels. Crucially, dockerStartCmd/dockerEntrypoint are never sent in the label-save request, yet came back untouched. So saveTemplate only overwrites the fields it's given; omitted fields are left alone. The "must enumerate 100% of the schema forever or corrupt data" failure mode doesn't apply.

To lock this in, added TestUpdateTemplatePortLabelsPreservesStartCommand: applies a label with no override and asserts the exact dockerArgs/startScript round-trip unchanged.

Re-assessment of the other majors

  • Stale "present but stale" read: given the above (omitted/partial fields aren't destructive), a stale read can't wipe data the way feared. The one real edge — reading immediately after create — is already covered by the override struct. Left as-is.
  • O(N) myself { podTemplates } scan: real but a performance nit, not a correctness bug. The single-template podTemplate(id:) query doesn't currently return the fields the save needs, so swapping it is more churn than it's worth here. Noting it rather than changing it.

Minors addressed

  • pod create now TrimSpaces --registry-auth-id, matching template create.
  • NormalizePort documented as validate-then-discard: 22/tcp and 22/http both normalize to 22, so label↔--ports matching is by number only (intentional — dashboard labels key on port number).
  • --port-labels comma limitation documented in flag help + the parser: the port=name pair form splits on ,, so a label name with a comma needs the JSON form. (A name with = is fine — only the first = splits.)

Docs regenerated for the flag-help changes. go build, go vet, and the api/template/pod test packages all pass (94 tests).

@justinwlin

Copy link
Copy Markdown
Contributor Author

Addressed the Round-2 blocker (NEW-1) — the only breaking-class item.

Fix (6f3cf1f): carry the start command through the create overrides as the backend's canonical {"cmd":[...],"entrypoint":[...]} dockerArgs encoding, so the port-label write re-asserts it regardless of whether the immediate post-create GraphQL read is stale. This also completes the set of create-time fields carried through overrides (closing the create side of the present-but-stale read gap, M3).

Tests: create-side override reconstruction, plus a stale-read guard that returns an empty dockerArgs on read and asserts the write sends the command, not "".

Verified live: template create --image X --ports 22 --docker-start-cmd "python -u app.py" --port-labels "22=ssh" now round-trips dockerArgs {"cmd":["python -u app.py"]} (survives the label write). Full suite: 191 tests pass; probe templates cleaned up.

Not changed: M1 (schema-drift guard) and M4 (O(N) fetch-all in the retry loop) — correctness-neutral / perf, as noted in the review. Happy to follow up on those separately if wanted.

templates created via the cli could not attach container registry auth,
so private-registry image pulls failed and required a manual console step
after each create. add --registry-auth-id to both create and update
(parity with pod create), surfaced on template get via graphql.

update uses a *string so passing an empty value clears the auth.

refs DR-1399
--ports created port mappings but dropped the human-readable labels shown
in the dashboard, because labels live in graphql's portsConfig field and
not the public rest template schema. add a --port-labels flag (port=name
pairs or json) to create and update.

labels are applied with a follow-up graphql saveTemplate that reconstructs
the template's full save-state (retrying briefly on stale reads after a
create) and merges in the values just written over rest, so a fresh update
is not reverted. create deletes the template if the label step fails, so
the command stays atomic. labels are validated against --ports and shown
on template get.

refs DR-1399
template get reads the rest endpoint first, which has no port-label field,
so labels set via --port-labels were saved but never echoed back. backfill
portsConfig from graphql when a user-owned template exposes ports but rest
returned no labels.

refs DR-1399
…aveats

- add TestUpdateTemplatePortLabelsPreservesStartCommand: applying a label must
  not wipe dockerArgs/startScript (where REST dockerStartCmd/dockerEntrypoint
  land), guarding the read-subset/write-whole saveTemplate path
- document NormalizePort protocol validate-then-discard (port-number matching)
- document port-labels comma limitation + json escape hatch (flags + parser)
- trim registry-auth-id in pod create to match template create
The port-label write reads the template back over GraphQL and re-sends dockerArgs.
Immediately after create that read can be stale and return an empty dockerArgs, so
'template create --docker-start-cmd … --port-labels …' could send dockerArgs:"" and
silently wipe the just-set start command.

Carry the start command through the create overrides as the backend's canonical
{"cmd":[...],"entrypoint":[...]} dockerArgs encoding, so the label write
re-asserts it regardless of read staleness. This also completes the set of
create-time fields carried through overrides, closing the create side of the
present-but-stale read gap.

Tests: create-side override reconstruction (cmd/template) and a stale-read guard
(internal/api) that returns empty dockerArgs on read and asserts the write sends the
command, not "". Verified live: create --docker-start-cmd … --port-labels … now
round-trips dockerArgs {"cmd":["python -u app.py"]}.
@justinwlin
justinwlin force-pushed the docs/release-process branch from 6f3cf1f to 07e2227 Compare July 13, 2026 19:09
@justinwlin
justinwlin merged commit e75450f into main Jul 13, 2026
1 check passed
@justinwlin
justinwlin deleted the docs/release-process branch July 13, 2026 19:11
@promptless

promptless Bot commented Jul 13, 2026

Copy link
Copy Markdown

Promptless prepared a documentation update related to this change.

Triggered by runpodctl PR #297

Refreshed the runpodctl template CLI reference to document the new --registry-auth-id and --port-labels flags on template create and template update (the port=name/JSON label syntax, --ports validation, and empty-value clear/detach behavior on update), noted that template get now shows port labels, and captured the port-label comma limitation (a label name containing a comma must use the JSON form). The README "release process" section is internal release-engineering documentation and doesn't warrant a docs change.

Review: Document runpodctl template registry auth and port label flags

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.

2 participants