fix(gitlab): preserve private flag when webhook payload omits project visibility#6544
Merged
6543 merged 2 commits intoMay 5, 2026
Conversation
GitLab webhook payloads for push, tag, and merge-request events do not include `project.visibility` (string); they only carry `project.visibility_level` (numeric), which the go-gitlab library does not expose on the corresponding event types. As a result, `hook.Project.Visibility` is always empty for these events, the switch in convertPushHook/convertTagHook never matches, and IsSCMPrivate is left at its zero value (false). `(*Repo).Update` then unconditionally copied that false value into the stored repo, overwriting the correct value previously synced via the forge API during repo activation or repair. Once stored, downstream code (server/pipeline/items.go) skipped attaching the netrc to the clone step on the assumption the repo was public. For genuinely private GitLab repos this manifested as `fatal: could not read Username for 'https://gitlab.com'` on every clone after the next push, requiring a manual repair to re-sync the API value, which then survived only until the next push webhook clobbered it again. Fix: - model.Repo.Update: only propagate Visibility/IsSCMPrivate when the source actually supplies visibility info. Empty `from.Visibility` now signals "no information" and leaves the stored fields untouched. The API path (convertGitLabRepo) always populates Visibility, so repo activation/repair behaviour is unchanged. - gitlab convertReleaseHook: the previous logic (`VisibilityLevel > VisibilityLevelInternal`) was inverted — it marked private/internal repos as public and public repos as private. Replace with a switch over the three numeric levels and set both Visibility and IsSCMPrivate so the new gate in Update propagates the value. - gitlab convertPushHook/convertTagHook: also set Visibility alongside IsSCMPrivate so any future GitLab payload (or self-hosted variant) that does include `project.visibility` is propagated correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lock in the regression fix from the previous commit: webhook payloads that omit project visibility (empty from.Visibility) must not overwrite the stored value, while API payloads that supply visibility — including the "internal" GitLab variant — must propagate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6544 +/- ##
==========================================
+ Coverage 41.45% 41.60% +0.14%
==========================================
Files 431 431
Lines 28696 28725 +29
==========================================
+ Hits 11896 11950 +54
+ Misses 15729 15703 -26
- Partials 1071 1072 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
6543
approved these changes
May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
On every push (or tag/MR) webhook from GitLab to a Woodpecker server hosting a private GitLab repo, the clone step starts failing with:
Clicking Repair Repository in the UI fixes it temporarily — until the next push, when the cycle starts again.
WOODPECKER_AUTHENTICATE_PUBLIC_REPOS=trueis an effective workaround.Root cause
GitLab's push, tag, and merge-request webhook payloads include
project.visibility_level(numeric: 0 / 10 / 20) but notproject.visibility(string). The fixtures in this repo confirm this —server/forge/gitlab/fixtures/HookPush.json,HookTag.json, andHookPullRequestOpened.jsonall carryvisibility_leveland novisibility.The go-gitlab library (
gitlab.com/gitlab-org/api/client-go/v2) mapsPushEventProject.Visibilityto JSON keyvisibilityonly and does not exposevisibility_levelfor these event types. Sohook.Project.Visibilityis""for real GitLab payloads, theswitchinconvertPushHook/convertTagHookmatches no case, and the returnedRepo.IsSCMPrivateis left at its Go zero valuefalse.(*Repo).Updatethen unconditionally copied thatfalseover the stored value previously set byconvertGitLabRepoduring repo activation or repair — flipping the stored repo toprivate=0, visibility=public. Downstream,server/pipeline/items.goskips attaching the netrc to the clone step whenrepo.IsSCMPrivateis false (andAuthenticatePublicReposis unset), so the next clone runs without credentials and fails with the error above.The Repair button hits the GitLab API (where
visibilityis populated), routes throughconvertGitLabRepo, and re-syncs the correct value — until the next webhook clobbers it again.I traced this end-to-end on a 3.14.0 deployment by:
private=0, visibility=publicwhile the GitLab API returnedvisibility=privatefor the same project.git clone https://oauth2:$TOKEN@gitlab.com/...— it is not a token / scope problem.parsePipeline→compiler.WithNetrc(..., repo.IsSCMPrivate || AuthenticatePublicRepos)and identifying the gate.WOODPECKER_AUTHENTICATE_PUBLIC_REPOS=truemade clones succeed without any other change.private=1, visibility=privateafter a fresh push event.Fix
model.Repo.Update: only propagateVisibility/IsSCMPrivatewhen the source actually supplies visibility info. An emptyfrom.Visibilitynow signals "no information" and leaves the stored fields untouched. The API path (convertGitLabRepo) always populatesVisibility, so repo activation/repair is unchanged. Side benefit: "internal" is now preserved instead of being collapsed to "private" by the oldIsSCMPrivate-based recomputation.gitlab.convertReleaseHook: the previous expressionwas inverted: with
VisibilityLevelInternal == 10, onlyPublic (20)satisfied> 10, so it marked public projects as private and private/internal projects as public. Replaced with an explicit switch over the three numeric levels that sets bothVisibilityandIsSCMPrivate, so the newUpdategate propagates the value.gitlab.convertPushHook/convertTagHook: also setVisibilityalongsideIsSCMPrivate, mirroringconvertGitLabRepo. Today this is effectively dead code (because theVisibilitystring is empty in real payloads), but it is symmetric and forward-compatible if GitLab ever populates the field.Tests
server/model/repo_test.go(new) covers:from.Visibilitypreserves the stored private value (the regression);from.Visibilitypreserves the stored public value;internalis preserved instead of being collapsed toprivate.Existing
server/forge/gitlabandserver/modeltests continue to pass.Verification
Built from this branch on top of a 3.14.0 base and deployed against a private GitLab project that previously failed on every push. After the patch:
private=1, visibility=privateacross multiple webhook-triggered pipelines.WOODPECKER_AUTHENTICATE_PUBLIC_REPOS.