Skip to content

feat(ci): build a dev image automatically on a labelled PR - #677

Open
balajinvda wants to merge 2 commits into
mainfrom
feat/dev-image-on-labeled-pr
Open

feat(ci): build a dev image automatically on a labelled PR#677
balajinvda wants to merge 2 commits into
mainfrom
feat/dev-image-on-labeled-pr

Conversation

@balajinvda

@balajinvda balajinvda commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Why

Automatic dev image creation for developers, so a change can be deployed to
staging and tested before the PR merges.

What changed

Label a PR deploy-to-stg and every push to it builds and pushes a dev image.
Remove the label to stop. The image lands in ncp-dev as gh.<run>-<sha8>, the
same convention workflow_dispatch already uses, so nothing downstream changes.

A PR event carries no service_path input, so a new resolve job derives it
from the changed files: take the src/<plane>/<service> prefix of everything
changed and require exactly one. Two services skips with a notice rather than
guessing. workflow_dispatch still passes the path explicitly and behaves
exactly as before.

Customer Release Notes

Not customer visible.

Plan Summary

Not applicable. No release machinery is touched and no git tag is created, so
this cannot reach the production promotion path.

Usage

Label the PR, then deploy the image.

In the GitHub UI: open the PR, click the gear next to Labels in the right
sidebar, and select deploy-to-stg.

From the CLI:

gh pr edit <PR-NUMBER> --repo NVIDIA/nvcf --add-label deploy-to-stg

The build starts as soon as the label is applied, and again on every push to
that PR. Find the resulting tag under the image-push (manual) run, or derive
it: gh.<run-number>-<first 8 of the head SHA>. Pull it from ncp-dev and deploy
that tag on staging.

To stop the builds, remove the label:

gh pr edit <PR-NUMBER> --repo NVIDIA/nvcf --remove-label deploy-to-stg

Manual dispatch is unchanged for the cases this does not cover, such as a PR
touching two services or building from a branch with no PR.

Testing

The resolution logic was exercised against six cases before pushing: one
service, two services, docs only, a root file only, a service plus a root file,
and a top-level src file. Only the single-service case builds; the rest skip
with a notice.

Not yet exercised end to end on a real labelled PR. The label does not exist in
the repository yet, so it needs creating before the trigger can fire, and the
first labelled PR is the real test.

Notes

Security: fork PRs receive no secrets, so the push fails closed rather than
leaking the registry token. Same-repo PRs get them, matching how the bazel
matrix already handles the cache token. pull_request is used rather than
pull_request_target, so untrusted code never runs with secrets in scope.

The concurrency group previously keyed on github.event.inputs.service_path,
which is empty on a PR event and would have collapsed every PR into one group.
It now falls back to the ref.

Open question for the reviewer: deploy-to-stg is my choice of label name and
is easy to change.

References

None

Related Merge Requests/Pull Requests

None

Dependencies

None

Github commit:
feat(ci): build a dev image automatically on a labelled PR

Co-authored-by: Balaji Ganesan bganesan@nvidia.com

Summary by CodeRabbit

  • Chores
    • Improved automation for publishing staging container images from eligible pull request updates.
    • Added validation to identify the correct service changes and prevent publishing when changes are ambiguous or not buildable.
    • Improved consistency for build caching and image publishing across supported services.

Getting a dev image to test on staging currently means opening Actions,
selecting the workflow, typing the service subtree by hand and clicking run,
once per attempt. That friction is why people ask for dev tags instead, but a
dev tag does not solve it: publish_dev_prerelease runs on a main push, so the
tag only exists after the merge, which is too late to test before merging.

Label a PR with deploy-to-stg and every push to it builds and pushes a dev
image. Removing the label stops it. The image lands in ncp-dev as
gh.<run>-<sha8>, the same convention workflow_dispatch already uses, so nothing
downstream changes.

A PR event carries no service_path input, so a resolve job derives it from the
changed files: take the src/<plane>/<service> prefix of everything that changed
and require exactly one. Changing two services skips with a notice rather than
guessing, since a dev image targets one service and picking for the user would
be wrong. workflow_dispatch still passes the path explicitly and is unchanged.

No release machinery is touched and no git tag is created, so this cannot reach
the production promotion path. Fork PRs receive no secrets and fail closed
rather than leaking the registry token; same-repo PRs get them, matching how
the bazel matrix already handles the cache token.

Resolution logic verified against six cases before pushing: one service, two
services, docs only, a root file only, a service plus a root file, and a
top-level src file. Only the single-service case builds.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda
balajinvda requested a review from a team as a code owner August 5, 2026 01:28
@balajinvda
balajinvda requested a review from mikeyrcamp August 5, 2026 01:28
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The workflow supports labeled pull request image builds. A resolve job selects one service subtree from workflow input or changed files. The push job uses that subtree for caching, Bazel layout, and image publishing.

Changes

Image push workflow

Layer / File(s) Summary
Resolve the service subtree
.github/workflows/image-push-manual.yml
The workflow adds pull request triggers, same-repository and label filters, concurrency cancellation, and service subtree resolution before pushing.
Use the resolved path for publishing
.github/workflows/image-push-manual.yml
Cache keys, Bazel module layout, and image publishing now use the resolved service subtree.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: mikeyrcamp

Sequence Diagram(s)

sequenceDiagram
  participant PullRequestEvent
  participant resolve
  participant push
  PullRequestEvent->>resolve: Start on labeled, synchronize, or reopened event
  resolve->>resolve: Check deploy-to-stg label and resolve service subtree
  resolve->>push: Emit resolved service path
  push->>push: Use path for cache, Bazel layout, and image publishing
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits syntax and accurately describes the new automatic dev image build workflow for labelled pull requests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dev-image-on-labeled-pr

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/image-push-manual.yml:
- Around line 80-85: Update the subtree detection command in the workflow to use
the merge-base diff form between BASE_SHA and HEAD_SHA, so only changes
introduced by the PR populate subtrees; leave the subsequent single-subtree
BUILD.bazel resolution logic unchanged.
- Around line 18-25: Update the workflow trigger/resolve logic so a labeled
event proceeds only when github.event.label.name is deploy-to-stg. Preserve the
existing label-presence check for synchronize and reopened events in the resolve
job condition, without changing their behavior.
- Around line 42-43: Update the concurrency group expression in the workflow to
use github.run_id for pull request runs while retaining
github.event.inputs.service_path for manual dispatches; preserve the existing
github.ref fallback where applicable and leave cancel-in-progress unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 820e2b07-a354-4ab7-ab9a-a27819a4ac60

📥 Commits

Reviewing files that changed from the base of the PR and between efc98c7 and 105d5fa.

📒 Files selected for processing (1)
  • .github/workflows/image-push-manual.yml

Comment thread .github/workflows/image-push-manual.yml
Comment thread .github/workflows/image-push-manual.yml Outdated
Comment on lines 42 to 43
group: image-push-${{ github.event.inputs.service_path || github.ref }}
cancel-in-progress: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/image-push-manual.yml
printf '%s\n' '--- concurrency references ---'
rg -n -C 3 'concurrency|github\.ref|github\.run_id|workflow_dispatch|pull_request|labeled' .github/workflows

Repository: NVIDIA/nvcf

Length of output: 50367


🌐 Web query:

site:docs.github.com/en/actions control workflow concurrency one pending run group cancel-in-progress false github.run_id concurrency expression

💡 Result:

In GitHub Actions, the concurrency key is used to limit the number of workflow runs or jobs that execute simultaneously within a defined concurrency group [1][2]. When a new workflow run is triggered, if another run within the same concurrency group is already in progress, the new run is placed in a pending state [1][3]. Regarding your specific scenario: 1. cancel-in-progress: false: This is the default behavior [1][3]. When set to false (or omitted), any existing pending runs within the same concurrency group are canceled, and the new run becomes the single pending run [1][3]. The currently running job or workflow is not canceled; it completes its execution [1][3][4]. 2. github.run_id as a concurrency group: If you use the expression group: ${{ github.run_id }}, you are effectively assigning a unique concurrency group to every individual workflow run [5]. Since every run has a unique ID, the concurrency group will never overlap with another run. Consequently, this configuration will have no effect on concurrency control, as no two runs will ever share the same group, and no run will ever be placed in a pending state or cause another to be canceled [1][2]. If your goal is to manage concurrency while preventing the cancellation of in-progress runs, you should omit cancel-in-progress or explicitly set it to false and ensure your concurrency group identifier is shared across the runs you intend to throttle (e.g., based on branch name or workflow name rather than a unique run ID) [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
expr = "github.event_name == 'workflow_dispatch' && github.event.inputs.service_path || github.run_id"
cases = [
    ("workflow_dispatch", "src/a/b", "refs/heads/main", 101),
    ("workflow_dispatch", "src/a/b", "refs/heads/feature", 102),
    ("pull_request", "", "refs/pull/7/merge", 103),
    ("pull_request", "", "refs/pull/7/merge", 104),
]
for event, path, ref, run_id in cases:
    # Evaluate the equivalent of GitHub's &&/|| conditional idiom.
    value = path if event == "workflow_dispatch" and path else run_id
    print(f"{event:18} path={path or '-':12} ref={ref:24} run_id={run_id} -> image-push-{value}")
PY

Repository: NVIDIA/nvcf

Length of output: 537


Use a unique concurrency group for pull request runs.

cancel-in-progress: false keeps the running run but allows a newer run to replace the existing pending run. Use github.run_id for pull request runs and keep github.event.inputs.service_path for manual dispatches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/image-push-manual.yml around lines 42 - 43, Update the
concurrency group expression in the workflow to use github.run_id for pull
request runs while retaining github.event.inputs.service_path for manual
dispatches; preserve the existing github.ref fallback where applicable and leave
cancel-in-progress unchanged.

Comment on lines +80 to +85
mapfile -t subtrees < <(
git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" \
| grep -E '^src/[^/]+/[^/]+/' \
| cut -d/ -f1-3 | sort -u
)
if [ "${#subtrees[@]}" -eq 1 ] && [ -f "${subtrees[0]}/BUILD.bazel" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Diff from the PR merge base.

git diff "${BASE_SHA}" "${HEAD_SHA}" compares two endpoint trees. If the base branch advances in another service after the PR branches, that service enters subtrees. The resolver then treats a single-service PR as multi-service and skips its image.

Use the merge-base form. git diff A...B compares the merge base of A and B with B. (git-scm.com)

Proposed fix
-            git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" \
+            git diff --name-only "${BASE_SHA}...${HEAD_SHA}" \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mapfile -t subtrees < <(
git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" \
| grep -E '^src/[^/]+/[^/]+/' \
| cut -d/ -f1-3 | sort -u
)
if [ "${#subtrees[@]}" -eq 1 ] && [ -f "${subtrees[0]}/BUILD.bazel" ]; then
mapfile -t subtrees < <(
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" \
| grep -E '^src/[^/]+/[^/]+/' \
| cut -d/ -f1-3 | sort -u
)
if [ "${`#subtrees`[@]}" -eq 1 ] && [ -f "${subtrees[0]}/BUILD.bazel" ]; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/image-push-manual.yml around lines 80 - 85, Update the
subtree detection command in the workflow to use the merge-base diff form
between BASE_SHA and HEAD_SHA, so only changes introduced by the PR populate
subtrees; leave the subsequent single-subtree BUILD.bazel resolution logic
unchanged.

Four problems found by reviewing and exercising the first version rather than
assuming it worked.

Fork PRs fired the trigger, took a runner and died at the push step with an
auth error, because secrets are withheld from forks. They are now filtered in
the job condition, so they never start.

`labeled` fires for every label. Adding an unrelated label to a PR that already
carried deploy-to-stg triggered a full rebuild. The condition now also requires
that the label being added is the one we care about; on `synchronize` and
`reopened` there is no github.event.label, so that clause is true and the
label-presence check governs.

Pushing twice to a PR queued two builds when the first was already obsolete.
PR builds now cancel superseded runs. Dispatch runs still queue, since each is
deliberate and someone is waiting on its output.

The skip message was wrong for a subtree that resolves but has no BUILD.bazel:
it reported the multiple-subtree case and pointed at workflow_dispatch, which
would not have helped. A change under src/libraries/go/lib resolves to
src/libraries/go, which is not a buildable service. All three skip reasons now
say what actually happened.

Verified by extracting the resolve script from the workflow and running it
against dispatch, one service, two services, docs only, and a library path.
actionlint is clean across every workflow in the repository.

Co-authored-by: Balaji Ganesan <bganesan@nvidia.com>

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/image-push-manual.yml (1)

95-100: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Resolve the canonical service root.

When a PR changes only src/libraries/rust/stargate, line 98 resolves it to src/libraries/rust. That directory has no BUILD.bazel, so the workflow sets found=false and skips the image push. Use the service-root contract from .github/workflows/bazel.yml before the BUILD.bazel check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/image-push-manual.yml around lines 95 - 100, Update the
subtree resolution using the canonical service-root contract from bazel.yml
before the BUILD.bazel check. Ensure changes under src/libraries/rust/stargate
resolve to their service root rather than src/libraries/rust, then use that
resolved root in the existing subtrees and BUILD.bazel validation so the image
push is detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.github/workflows/image-push-manual.yml:
- Around line 95-100: Update the subtree resolution using the canonical
service-root contract from bazel.yml before the BUILD.bazel check. Ensure
changes under src/libraries/rust/stargate resolve to their service root rather
than src/libraries/rust, then use that resolved root in the existing subtrees
and BUILD.bazel validation so the image push is detected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 543becb8-e777-4b14-959a-f20b27e9ecd3

📥 Commits

Reviewing files that changed from the base of the PR and between 105d5fa and 3bef6f5.

📒 Files selected for processing (1)
  • .github/workflows/image-push-manual.yml

@balajinvda
balajinvda marked this pull request as draft August 5, 2026 23:44
@balajinvda

Copy link
Copy Markdown
Contributor Author

Moving to draft until the NGC token scope is confirmed.

This PR auto-fires image-push (manual) from a PR label. The body claims the
push "cannot reach the production promotion path" — that was inferred from the
secret name NGC_NCP_DEV_KEY, not from the key's actual NGC authorization. The
registry value is masked in run logs, and a recent dispatch landed at
nvidia/nvcf-byoc on stg rather than an ncp-dev path, so the name and the
destination do not obviously agree.

Auto-triggering on a label is only safe if that key is stg-scoped. Holding until
someone with NGC admin access confirms the scope.

@balajinvda
balajinvda marked this pull request as ready for review August 5, 2026 23:48
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.

3 participants