fix(deploy): make GitHub/GitLab app deploy work (Source Config, private repos, token re-validate) - #5707
Conversation
…hild init The deploy-application wizard's Source Config step (step2_1, the git commit picker) is rendered with [hidden], so Angular instantiates it up-front. The step2_1Ref @ViewChild setter called v.onEnter() at that instantiation time — before step 2's submit had saved any git source into applicationSource. The commit-list wrapper therefore resolved an empty project/branch, never fetched commits, and the step stayed invalid: Source Config showed no options and the Next button was disabled. This is the same ViewChild-fires-once trap already fixed for step2_2/step3 (#5600). Move step2_1's activation work off the ViewChild setter and onto step2_1Handle.onEnter, which the stepper fires when navigating into the step — after step 2's submit has populated the source. Applies to both GitHub and GitLab source types. Add a regression guard asserting onEnter is not called at ViewChild init and is called on handle activation. Signed-off-by: Chris Weibel <christopher.weibel@gsa.gov>
…t repos
When a github.com/gitlab.com endpoint is separately registered, the deploy
wizard tags the GitHub/GitLab source type with that endpoint's guid. In
Private/Enterprise access mode the user supplies a Personal Access Token
directly in the form, but project-exists validation and repo/branch/commit
lookups were still built against the registered endpoint guid — so requests
proxied through the endpoint's stored credentials (/api/v1/proxy/<guid>/...)
and ignored the typed token. A private repo the token could see returned 404
("Project does not exist"), even though the suggestions dropdown (a separate
path) correctly showed it as Private.
Only use the registered endpoint guid in Public mode. In Private/Enterprise
mode talk to the SCM API directly with the typed token:
- add projectExistsEndpointGuid getter ('' unless Public) for the validator
- build/rebuild the component SCM with no endpoint guid in Private/Enterprise
(setSourceTypeModel$ and setGitMode)
- onNext requires an endpoint guid only in Public mode; Private/Enterprise
deploy carries the token instead
Add regression tests covering the guid selection per mode and the SCM rebuild
on tab switch.
Signed-off-by: Chris Weibel <christopher.weibel@gsa.gov>
The project-exists async validator only re-runs when its own control (the
project name) changes. When a Personal Access Token is added AFTER the
project name is entered, the auth context changes but the name does not, so:
- the earlier unauthenticated /repos/<owner>/<repo> lookup 404s a private
repo and caches projectExists={name, exists:false}, and
- the token change never re-triggers validation; even if it did, the
checkProjectExists guard (name !== value) skips the re-check.
Result: a private repo the typed token can see stays stuck on
"Project does not exist".
Make the directive implement OnChanges + registerOnValidatorChange: when the
auth context (scm,guid,token) in appGithubProjectExists changes, clear the
cached project-exists result and ask Angular to re-run validation so the repo
is re-checked with the new token. Also preserve commas in the token when
splitting the auth context.
Add regression tests for the re-validate-on-token-change behavior and the
comma-safe token parse.
Signed-off-by: Chris Weibel <christopher.weibel@gsa.gov>
A GitHub/GitLab GET /repos/<owner>/<repo> returns 404 both when a repo truly does not exist AND when the supplied access token is not authorized for a private repo (GitHub deliberately 404s rather than 403s to avoid leaking existence). The wizard flatly showed "Project does not exist", which is misleading in the common private-repo/token case. Reword to point at the likely causes: exact (case-sensitive) owner/name and, for private repositories, an access token authorized for that repo. Signed-off-by: Chris Weibel <christopher.weibel@gsa.gov>
norman-abramovitz
left a comment
There was a problem hiding this comment.
Went through all four commits and verified the mechanisms rather than taking the commit messages at their word. The diagnoses hold up well, and each fix is aimed at the right layer:
GitSCMService.getSCMreturns a fresh instance per call (scm.service.ts:27) andBaseSCM.getEndpointshort-circuits toof(undefined)on an empty guid, falling back togetPublicApi(). So passing''in Private/Enterprise really does bypass the proxy and use the typed token, and there's no shared-instance leakage between modes.- Relaxing the endpoint-guid requirement in
onNextis safe on the backend:deploy.go:442only resolves a CNSI recordif len(info.EndpointGUID) != 0, and the clone picks upAccessTokenindependently atdeploy.go:667. The frontend was gating a path jetstream already supported. - The
step2_1change matches the existingstep2_2Refprecedent in the same file, andsteppers.component.tsroutes everysetActivethroughpOnEnter→signalHandle.onEnter, not just the first step. Right hook. registerOnValidatorChangeis genuinely wired up for async validators (@angular/forms,setUpValidators), so the re-validate trigger works as described.
The one thing I'd like changed
ngOnChanges calls deployData.projectDoesntExist('') unconditionally, including on the very first change. On the redeploy path that discards state the wizard cannot rebuild.
gitscm-tab.component.ts:332seedscheckProjectExists(scm, projectName)just before navigating to/applications/deploy.CfDeployAppDataServiceisprovidedIn: 'root', so that result carries into the wizard.- The directive's first
ngOnChangesalways fires withlastAuthContext === ''against something like'github,<guid>,'— never equal — so it resetsprojectExiststo{ name: '', exists: false }. - On redeploy the project input is
[disabled]="isRedeploy", and a disabled control skips its async validator (updateValueAndValidityonly runs validators whenenabled). Nothing re-checks, so the reset is permanent.
Worth noting this fires even when step 2 is not the active step. I assumed at first that step content was instantiated lazily — the comment above setActive in steppers.component.ts says as much — but that isn't what happens. <ng-content> inside <ng-template> doesn't defer construction in Ivy. I checked with a throwaway spec putting a directive with an @Input inside a nested @if in a non-active step, and its ngOnChanges ran on first render. So the reset lands during the initial wizard render regardless of where the stepper starts. (Independent of this PR, that steppers.component.ts comment is misleading and its accompanying spec builds StepComponent objects by hand, so it never actually exercises projection. The deferred onEnter delivery is still correct, just not for the stated reason.)
The visible effect is confined: projectInfo$ maps to null and the repository info card — avatar, full name, description — disappears for the rest of the redeploy, on the step whose own heading is "Review source details". The deploy itself still completes; repositoryBranches$ filters on exists upstream of its switchMap, so an in-flight getBranches isn't cancelled and repositoryBranch survives. Whether the card is lost at all depends on which resolves first, the seeded repo lookup or canDeployType$, so it will look intermittent.
An isFirstChange() guard covers it without weakening the fix — the case you're targeting, a token entered after the project name, is by definition never the first change:
ngOnChanges(changes: SimpleChanges): void {
const change = changes.appGithubProjectExists;
if (!change) { return; }
const auth = this.appGithubProjectExists ?? '';
if (change.isFirstChange()) { this.lastAuthContext = auth; return; }
if (auth === this.lastAuthContext) { return; }
this.lastAuthContext = auth;
this.deployData.projectDoesntExist('');
this.onChange?.();
}The spec currently pins the opposite behaviour (expect(reRuns).toBe(1) for the first-change block), so that expectation would become 0. Worth deciding whether that assertion was describing intended behaviour or just recording what the code happened to do.
Smaller points
The new not-found copy also shows while the project name is still being typed. validate() returns githubProjectDoesNotExist: true for anything failing isValidProjectName, and the template renders the error with no dirty/touched gate, so "Repository not found. Check the owner/name is exact (it is case-sensitive), and for a private repository make sure your access token is authorized for it." appears from the first keystroke and stays until the name is complete — owner/re still trips it, since the check wants more than two characters after the slash. The conflation predates this PR, but the longer text makes it far more prominent. A distinct error key for "not a complete owner/repo yet" would be the proper fix; short of that, wording that doesn't imply a lookup already happened would read better.
res.slice(2).join(',') is reasonable hardening, though for what it's worth GitHub (ghp_, github_pat_) and GitLab tokens are base62 plus -/_, so a comma shouldn't arise in practice.
endpointGuid: endpointGuid ?? '' in the saveAppDetails call is now a no-op — the public branch is already guarded non-empty just above, and the other branch is a literal ''.
The two setGitMode specs assert with toHaveBeenCalledWith, which matches any call, while the comment says "Last getSCM call should target...". Fine as it stands, but the comment promises something stronger than the assertion does.
… review) Address review feedback from norman-abramovitz on PR #5707. github-project-exists.directive's ngOnChanges cleared projectExists unconditionally on the very first change. Because the directive is constructed on first wizard render even in a non-active step (Ivy does not defer <ng-content> inside <ng-template>), and the redeploy path pre-seeds checkProjectExists() before navigating to the wizard (CfDeployAppDataService is providedIn:'root'), that first-change reset discarded the seeded repo info. On redeploy the project input is [disabled] so its async validator never re-runs to rebuild it — the repository info card (avatar/name/description) disappeared permanently. Track lastAuthContext as undefined until the first ngOnChanges records the baseline, and only clear/re-validate on a *subsequent* genuine auth-context change (e.g. a PAT typed after the project name). Add a regression test that the first change does not clear projectExists, and correct the earlier test that wrongly expected a re-run on the first change. Also correct the misleading comment in steppers.component.ts: projected step content is not lazily instantiated; the afterNextRender deferral is about ordering relative to the active-step transition, not lazy construction. Signed-off-by: Chris Weibel <christopher.weibel@gsa.gov>
|
Thanks for the thorough review — and especially for verifying the mechanisms directly rather than trusting the commit messages. Fixed the
You're also right about the
|
|
The first-change guard does what's needed, and the I checked the destroy/recreate path as well, since a fresh directive also starts with The new test earns its place — spying on One correction on the Three small things are still open from the earlier review, all in files this PR already touches — no objection to folding them in here rather than tracking them separately:
The remaining point I'd rather see as its own change than bolted onto this PR: And yes, please do file the follow-up for the stepper spec. It builds To be clear on status: the blocking issue is resolved, so the outstanding "changes requested" is only about the three one-liners above. Push those and I'll approve — no need to open anything separate for them, we'll keep it on this PR. |
Three loose ends raised in review that merged before they were picked up. No behaviour change. - onNext coalesced the endpoint guid at the save site with `?? ''`. SourceType.endpointGuid is optional, so that operator was carrying the type rather than ever changing a value: the public branch is already guarded non-empty and the other is a literal ''. Narrow at the declaration instead and drop it from the object literal. - The two setGitMode specs asserted with toHaveBeenCalledWith, which matches any call, while their comment described the last one. Use toHaveBeenLastCalledWith so the assertion states what the comment claims. - The deferral comment in setActive said a synchronous pOnEnter would run "before the entering step is marked active", but the step is marked active a few lines above it. The actual constraint is that change detection has not yet re-rendered the content outlet for the new currentIndex.
Problem
Several issues in the Deploy Application wizard's git (GitHub/GitLab) path made it unusable end-to-end:
git clonefailed with exit 128 (wrong branch).github.com/gitlab.comendpoint was also registered.Root-caused and fixed each below. All changes have unit tests;
eslintis clean.Changes
1. Build the commit list on step activation, not
@ViewChildinitThe Source Config step (
step2_1, the commit picker) is rendered via[hidden], so Angular instantiates it up-front. Thestep2_1Ref@ViewChildsetter calledonEnter()at that instant — before step 2's submit had saved a git source — so the commit-list wrapper resolved an emptyapplicationSource, never fetched commits, and the step stayed invalid (no options, Next disabled). Moved the activation work tostep2_1Handle.onEnter, which the stepper fires on real navigation into the step, after step 2 submits. This is the same@ViewChild-fires-once trap already fixed forstep2_2/step3(#5600).This also resolves the exit 128 clone: the wizard had been carrying a wrong branch (e.g.
mainon amaster-default repo →Remote branch main not found) because the branch was never resolved from the repo's real default.2. Use the typed token, not a registered endpoint, for private repos
A separately-registered
github.com/gitlab.comendpoint tags the source type with its guid. In Private/Enterprise access mode the user supplies a Personal Access Token directly in the form, but project-exists validation + repo/branch/commit lookups were still built against the registered endpoint guid — so requests proxied through the endpoint's stored creds (/api/v1/proxy/<guid>/...) and ignored the typed token. A private repo the token could see returned 404 ("Project does not exist"). Now only Public mode uses the registered endpoint; Private/Enterprise talk to the SCM API directly with the typed token.3. Re-validate when the access token changes
An async validator only re-runs when its own control (the project name) changes. Adding a PAT after the name was entered didn't re-check (and the cache was keyed on name only), leaving a stale unauthenticated "does not exist". The directive now implements
OnChanges+registerOnValidatorChange: an auth-context (scm/guid/token) change clears the cached result and re-triggers validation. Token parsing is also comma-safe.4. Clearer repository-not-found message
GET /repos/<owner>/<repo>404s both for "does not exist" and for a private repo the token isn't authorized for (GitHub 404s rather than 403s to avoid leaking existence). Reworded the flat "Project does not exist" to point at exact (case-sensitive) owner/name and, for private repos, token authorization.Verification
deploy-application.component,deploy-application-step2.component, andgithub-project-exists.directive; all pass undervitest --project=cloud-foundry.eslintclean.developand is resolved by this branch.Implemented with AI assistance (OpenCode); reviewed by a human contributor. Commits are DCO signed-off.