Skip to content

feat: sync secrets to GitLab CI environment scopes - #1044

Merged
rohan-chaturvedi merged 21 commits into
mainfrom
feat--gitlab-environment-scopes
Nov 2, 2026
Merged

rohan-chaturvedi merged 21 commits into
mainfrom
feat--gitlab-environment-scopes

Conversation

@nimish-ks

@nimish-ks nimish-ks commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

🔍 Overview

Closes #1043.

GitLab CI/CD variables can be limited to an environment with an environment scope, and the same key can exist once per scope. Until now, GitLab syncs always wrote to the default * scope. There was no way to sync e.g. Staging and Production secrets into the matching GitLab environments of one project, as GitHub Actions syncs can with GitHub environments.

Existing syncs also broke as soon as a synced key existed in more than one scope: updates failed with There are multiple variables with provided parameters. Please use 'filter[environment_scope]', which stopped the whole sync. They now sync all other keys and explain the fix: recreate the sync with a scope.

💡 Proposed Changes

Environment scope on GitLab syncs

  • The GitLab sync form has a new GitLab Environment Scope picker. New syncs default to All environments (*), and any scope GitLab accepts can be typed, including wildcards like review/*.
    • For projects, it lists the project's GitLab environments, at most 50 at a time. Typing searches all of them, with an exact name listed first. Projects with review apps can have thousands, and rendering them all froze the page. The selected scope is always listed, so reopening the list and pressing Enter or Tab keeps it.
    • Groups have no environments, so for groups it suggests the scopes already used by the group's variables, as GitLab's own form does.
    • What's shown is what's used: a typed scope is kept when the picker closes without picking an option (Escape discards it). An invalid scope is kept too, shown as an error, and blocks creating the sync instead of falling back to the previous scope.
    • Picking another project or group resets the scope; a scope chosen before the first pick is kept.
  • New gitlabEnvironments(credentialId, projectId) query. It is paginated and uses the same credential permission check as the sibling GitLab queries. Projects with Environments disabled return an empty list instead of an error.
  • New gitlabGroupEnvironmentScopes(credentialId, groupPath) query, using GitLab's GraphQL group.environmentScopes. It only fetches scope names, never values, and returns an empty list on GitLab versions without that field.
  • createGitlabCiSync takes an optional environmentScope, validated against GitLab's own rules (charset, 255 chars) and stored in the sync options.
  • The sync card shows the scope, e.g. phase/backend (production).
  • Pressing Enter in the GitLab sync form no longer goes back to step 1. The Back button had no type, so it was the form's default submit button.

Scope-aware sync

  • A sync with a scope only creates, updates and deletes variables in that scope. Updates and deletes select the scope with filter[environment_scope], so keys that exist in several scopes no longer break the sync.
  • Two syncs in an App can't write the same GitLab project or group and scope, since they would overwrite each other's variables. Different masked/protected settings no longer make such a sync count as distinct.
  • This is checked when a sync is created, when a sync's credentials change, and when a credential's GitLab host changes. Using a new token for the same GitLab instance is never blocked. http:// and https:// URLs of a host count as the same instance. Errors only name syncs of Apps the member can access.

Existing syncs (verified with an upgrade test, see Testing)

  • Syncs created before this change have no stored scope and keep their previous behaviour: they sync to all environments (*).
  • As before, they also update and delete a variable that a user moved to another scope in GitLab, the workaround for missing scope support. This applies as long as it's the key's only variable outside the scopes of other syncs to the same project or group.
  • They never write one value into several scopes.
  • They follow one rule for every key, as before, with or without a * variable. When a key has variables in several scopes, e.g. a per-environment override of a synced key, they compare the secret with the variable listed last, and if it changed, update the key without a scope filter: GitLab refuses that for projects (409) and updates a variable of its choice for groups. GitLab also refuses to delete such a key (409). So they write what they wrote before. Now:
    • The sync never deletes a variable the old sync wouldn't have: wherever the old sync stopped, it still creates and updates the other keys, but deletes nothing, and fails naming the keys and what to do. Pipelines may rely on variables the old sync never got to delete.
    • Creating the sync again with an environment scope, e.g. All environments (*), makes it manage only that scope and leave overrides alone.
  • Syncs created before July 2024 only stored the project or group path. They are matched by path, and store the ID on their next run, so they're recognised after a rename too. While the org is pending deletion, they pause like other syncs, without calling GitLab.
  • A new scoped sync to a project or group that still has such a sync, in any App, is rejected with a message to recreate that sync with a scope first. Otherwise the old sync could copy a variable moved to the new sync's scope into *.

Safety

  • GitLab tiers without scoped group variables (Free) silently drop environment_scope and create the variable for all environments.
    • Before writing anything, group syncs with a scope create a throwaway PHASE_SCOPE_CHECK_* variable holding a random value. If GitLab dropped the scope, they abort with a clear error, so no secret is ever written there.
    • Every create also verifies the scope GitLab returned and removes the variable if it landed elsewhere.
  • GitLab's group PUT falls back to any variable with the same key when the scope filter matches nothing, e.g. when a concurrent run deleted the variable after it was listed, and GitLab ≤ 16.7 ignores the filter on group PUT entirely.
    • Group updates first check that the variable still exists (GET doesn't fall back) and create it again if not. Otherwise GitLab would update another variable with the key, e.g. a manual one in another scope.
    • Updates also send the scope in the body, so a fallback in the remaining window moves the other variable into the sync's scope instead of exposing the secret to its environments.
    • If GitLab still updated another scope's variable, it is restored from the listing and the sync fails.
  • All GitLab API calls go through one helper that sets a timeout. Redirects are only followed on the same host: to the same port, or from http:// to https:// on any port, keeping the method and body. A redirect to another host fails with an error that names that address (scheme, host and port only), so users know which GitLab host to use. The host is read with the parser requests connects with, redirects with credentials are refused, and in cloud each hop goes through the SSRF check again. In cloud, GitLab hosts that urllib.parse and requests read differently (e.g. with a backslash) are refused. Before, requests followed any redirect, which:
    • sent the Private-Token header (and, on 307/308, the secret payload) to wherever the GitLab host redirected, bypassing the cloud SSRF check on the configured host;
    • turned creates behind an http:// → https:// redirect into GETs: the sync reported success without ever creating a variable. GitLab also redirects the old path of a renamed project, so path-only syncs to renamed projects had the same problem.
  • Tokens and hosts are trimmed. A token containing characters that are invalid in a header is no longer included in the error; previously requests echoed the header value into sync logs and GraphQL errors.

🖼️ Screenshots or Demo

Screenshots of the scope picker, the configured sync and the sync card are in the docs PR, under public/assets/images/platform-integrations/gitlab/: https://github.com/phasehq/docs/pull/261/files

📝 Release Notes

  • New: GitLab CI syncs can target a GitLab environment scope (e.g. production, staging, review/*), so each Phase Environment can be synced to the matching GitLab environment of the same project or group.
  • Fixed: GitLab syncs using an http:// host that redirects to https:// (or the old path of a renamed project) reported success without creating new variables.
  • Fixed: pressing Enter in the GitLab sync form went back to the credentials step.
  • Changed: existing GitLab syncs keep syncing as before, and leave other syncs' scopes alone. A key with variables in several environment scopes, e.g. a per-environment override, no longer stops the whole sync: the other secrets are synced, and the error names the key and how to fix it. To sync such keys, or to add environment-scoped syncs to the same project or group, recreate the existing sync with a scope; with All environments (*), it leaves overrides alone.
  • Changed: GitLab syncs only follow redirects on the same host, such as from http:// to https://. If your GitLab URL redirects to another host, use the address named in the sync error as the GitLab host in your credentials. Before, syncs sent the GitLab token, and on some redirects the secrets, to whatever host the redirect named.
  • Environment scopes for group variables require GitLab Premium or Ultimate. On other tiers the sync fails with an explanation instead of exposing scoped secrets to all environments.

❓ Open Questions

  • Duplicate detection is per App, as for the other providers, so two Apps can still sync to the same GitLab project and scope. In QA, a second App's sync to edge/svc staging replaced all of the first App's staging variables, and the two syncs would keep overwriting each other. Should GitLab (or all providers) check across the organisation?

🧪 Testing

Automated

  • backend/tests/utils/syncing/test_gitlab.py: sync behaviour against an in-memory fake of the GitLab variables API.
    • The fake models per-scope keys, the group PUT fallback (and that GET/DELETE don't fall back) and Free-tier scope dropping.
    • Covers scoped, unscoped and legacy syncs (including when the old sync used to stop, for projects and groups), pagination, the scope check, rollback and its failure, the group update check, redirects (same host followed, including http:// to https:// on another port; other hosts, other ports on the same scheme, downgrades, credentials and backslash tricks refused, and the error names the address), timeouts, environment and group scope listing, path-only syncs, resolving their ID and host normalisation.
  • backend/tests/graphene/mutations/test_gitlab_sync_mutations.py: scope validation and conflict detection on create, credential switch and credential host change (legacy and path-only syncs, other Apps, other GitLab instances, token rotation, syncs without credentials, errors for Apps the member can't access).
  • backend/tests/tasks/test_syncing.py: dispatch for scoped vs. legacy syncs, and storing the ID of path-only syncs (not while the org is pending deletion).
  • New cases in the existing query/mutation permission matrices.
  • frontend/tests/utils/gitlabEnvironmentScope.test.ts: scope validation and picker options.
  • frontend/tests/components/GitLabEnvironmentScopePicker.test.tsx: reopening the picker and pressing Enter or Tab keeps a selected scope that isn't among the first 50, or that was typed (e.g. review/*).

Upgrade test: syncs created with main, then run with this branch (self-hosted GitLab CE 19.4)

  • Ten syncs in the format main creates (confirmed against main's own mutation), each on its own project or group, covering:
    • variables that already existed in GitLab (in and outside *, file type, raw: false, comments);
    • a variable moved to production by hand, and per-environment overrides on a project and a group;
    • a key not in Phase in two scopes;
    • masked + protected (with an unmaskable value), and a "masked and hidden" variable;
    • a path-only sync (pre-July 2024), a group sync, a paused sync, and a host that redirects to another path.
  • In place: ran the scenario with main's backend, worker and frontend, switched the same database to this branch, and re-ran every sync with nothing changed. No sync that passed on main failed, nothing main kept was deleted, and the only changes were the fixes below. The path-only sync stored its ID.
  • Side by side: ran the same five steps (first sync; moves and overrides in GitLab; edits, renames and deletes in Phase; deletes; no-op) from the same starting state on main and on this branch, and diffed every variable and sync result after each step. Everything is identical except:
    • project override: both fail at the overridden key and leave its variables as they are. This branch still syncs the other keys, and the error says which key and how to fix it.
    • group override: no difference. Both send the same update, and GitLab chooses the variable.
    • redirecting host: main reported "synchronized" on every run but never created a variable. Now they're created, and unchanged runs report "No changes needed".
    • key in two scopes: both fail and leave the variables as they are; the error now says which key and how to fix it.
    • No sync passes on main and fails here, at any step. The only such case found later is a redirect to another host, see below.
  • After the upgrade: in the UI and the API, creating a scoped sync next to these syncs (including the path-only and the group sync, and from another App) is refused with the "recreate that sync" message. Their credentials can be switched to a new token, and their credential's host edited. Recreating one with * changes no variables, after which scoped syncs can be added.

Follow-up commits (2b5a261, 45cb501, 1ef2237), checked against main's sync code (v2.77.2) and self-hosted GitLab CE 19.4:

  • Existing syncs now follow main's rule for every key with variables in several scopes, for projects and groups. Differential test of main's and this branch's sync code on about 39,000 random project and group states: the same pass or fail in every state, no extra deletes, and the same variables wherever main passed. Side by side against GitLab: keys with a * variable and an override, and keys in several scopes without one, in each order and state, end with the same variables on both.
  • Scope picker with 2,000 environments (Chromium): opens in 84 ms instead of 11 s, and closes in 0.4 s instead of 69 s. An environment past the first 50 can be found by typing and picked, and is sent with the create request.

After merging main (v2.78): follow-up commits f378dd9, 7637705, 16dd5fc and 4acadd0, checked against self-hosted GitLab CE 19.4.1 with a GitLab Runner:

  • Upgrade in place from main's code (v2.78.1 images, same code as main): 10 syncs created through main's UI and run on main, then run on this branch with the same database. Nothing changed in GitLab, and every sync gave the same result, except the redirect cases below.
  • Side by side after the upgrade (edits, renames, deletes, GitLab drift, no-op), from the same GitLab state: this branch never deleted a variable that main kept. Two differences, by design: where main stops at a key in several scopes, this branch still syncs the other keys; when a delete is ambiguous, it deletes nothing, while main deleted the keys it reached first.
  • Redirects: a host on http://…:8080 behind a TLS proxy that redirects (308) to https://…:8443 syncs, as on main, also through the UI. Redirects to another host fail, and the error names that host. On main, a 301 redirect to another host reported success without creating variables.
  • Scoped syncs created through the UI (*, review/*, and production masked and protected on a project; * and production on a group): 10 pipeline jobs got exactly the expected values (200 values checked). Edits in Phase changed only the matching scope, and re-runs changed nothing.
  • Picker: with 2,000 environments, or a typed review/*, reopening the list and pressing Tab or Enter keeps the chosen scope, and the create request sends it (Chromium). Before, it switched to *.
  • Org pending deletion (main's new lock): the new GitLab queries and sync triggers are refused, and GitLab sync jobs pause without calling GitLab, including path-only syncs.

Manual QA against self-hosted GitLab CE 19.4 and GitLab EE 19.4 on the Free and Ultimate tiers, with a GitLab Runner executing real pipelines.

Sync behaviour

  • Synced Development → review/*, Staging → staging and Production → production, plus an existing unscoped sync, into one project. Checked every variable's value, description (comment), masked/protected/raw flags and scope through the API.
  • Ran a pipeline with jobs for production, staging, development, review/feature-login and no environment. Each job received exactly the expected values: scoped values override *, review/* matches review/feature-login, and $ in values is not expanded.
  • Edited, re-commented and deleted secrets in Phase; only the matching scope changed in GitLab. Re-syncs report "No changes needed".
  • 150 variables in another scope, a stale key on page 2 and 122 environments: pagination works and other scopes are untouched.
  • GitLab EE Free: a scoped group sync fails with the tier explanation and leaves no variable behind; scoped project syncs work.
  • Duplicate syncs, invalid scopes, path-traversal project IDs and foreign credentials are rejected through the API.

Several syncs on one project and group with existing variables (scripted against real GitLab, checked through its API and 3 pipelines):

  • Variables that already existed in GitLab before the syncs:
    • in a sync's scope: overwritten with the Phase value and the sync's masked/protected settings, or deleted when not in Phase;
    • in other scopes: untouched;
    • a file variable keeps its type;
    • a "masked and hidden" variable is updated by a masked sync.
  • Three scoped syncs plus a path-filtered sync (/api → development) on one project, and * and staging group syncs on its group. Pipelines confirm that project variables beat group variables, and that review/feature-login beats review/*.
  • Phase edits (value, comment, rename, delete) change only the sync's own scope. Drift made in GitLab (edited, deleted, re-flagged or extra variables) is reverted on the next sync.
  • A * variable moved to production by hand: the * sync recreates it, and the production sync deletes the moved copy.
  • Second syncs to the same scope are rejected, including from another Phase Environment, with another path, or without a scope (defaults to *). The UI shows the error and keeps the form.
  • Pausing a sync stops updates; resuming applies them. Deleting a sync leaves its variables, and it can be recreated.
  • A masked secret GitLab can't mask fails the sync with GitLab's error. Updates before it are applied, and fixing the secret in Phase completes the sync.
  • Empty, multi-line and unicode values; group scope suggestions; no PHASE_SCOPE_CHECK_* variable left behind.

GitLab EE Ultimate

  • Scoped group syncs (production masked and protected, staging, *, review/*) on a group, a scoped sync on a nested subgroup and a project sync below it, with pre-existing group variables in and outside the synced scopes.
  • Pipelines confirm that the subgroup beats the parent group and the project beats both, and that production-only group secrets only reach production jobs.
  • Group scope suggestions, for a top-level and a nested group, in the API and the UI. A group sync created through the UI with a typed scope.
  • The group PUT race, reproduced against real GitLab by deleting the variable after the listing: the sync created it again, and the other scopes' variables (including a manual one) were untouched.
  • Removing the license: existing scoped group variables keep their scopes, and * group syncs keep working. Scoped group syncs fail with the tier explanation without writing anything, and catch up once the license is back.

Form state (31 checks, Playwright, payload captured from the request):

  • Switching between the project and group tabs after choosing a resource and scope, in either order; picking another project or group.
  • Switching credentials, Back → Next, and closing and reopening the dialog.
  • Typed, invalid (by click and by Enter), cleared, Escape-discarded and filtered-then-picked scopes; a scope chosen before the first project or group; Enter in each input.
  • Environments from the second page; a double-click on Create sends one request.

Redirects: besides the unit tests, 18,468 Location values (userinfo and backslash tricks, percent-encoding, IDNA, IPv6 and zone IDs, control characters, scheme-relative and urljoin oddities) against 8 starting URLs: every redirect that is followed connects to the same host and port as checked, and a backslash proof of concept that reached another server before the fix is refused.

yarn build, tsc, ESLint and yarn test pass. The backend suite passes except test_file_read_permission_error, an unrelated test that relies on chmod 000 and so fails whenever tests run as root.

🎯 Reviewer Focus

  1. backend/api/utils/syncing/gitlab/main.py: sync_gitlab_secrets, gitlab_sync_conflict, same_gitlab_resource, get_environment_scopes_of_other_syncs, gitlab_request and _connection_target.
  2. backend/backend/graphene/mutations/syncing.py: CreateGitLabCISync, check_gitlab_host_change (used by UpdateSyncAuthentication and UpdateProviderCredentials).
  3. backend/api/tasks/syncing.py: perform_gitlab_sync (storing the ID of path-only syncs).
  4. frontend/components/syncing/GitLab/.

➕ Additional Context

Docs: phasehq/docs#261

Known limitations of this change:

  • A sync created before this change can't tell a variable that was moved to another scope from a per-environment override. If a key's * variable is removed from Phase and the key is later added back, the remaining override is treated as moved and gets updated.
  • When such a sync is recreated with a scope, variables that were moved to other scopes by hand are no longer updated. Their jobs keep the old value until a sync for that scope is created or the variables are deleted. The docs say so.
  • A path-only sync (before July 2024) that hasn't run since this change, e.g. a paused one, is matched by path until it runs: if its project was renamed, a scoped sync can be created next to it.
  • Two credentials for the same GitLab instance under different host names (or non-default ports) count as different instances.
  • If a group variable is deleted in the moment between the check and the update, GitLab's fallback can still move another variable with that key into the sync's scope. Nothing is exposed.

Pre-existing issues noticed while working on this, not changed here:

  • validate_url_is_safe resolves the host once and requests resolves it again (DNS rebinding). It also parses URLs differently from requests (e.g. a backslash before @), so a URL can pass it and connect elsewhere. GitLab now refuses such hosts itself; the OIDC issuer and AWS STS endpoint checks still use it as is.
  • The GitLab project list uses min_access_level=30 (Developer), but managing CI/CD variables needs Maintainer, so some listed projects can't be synced.
  • Variables created as "masked and hidden" in GitLab return no value, so they are rewritten on every sync. With an unmasked sync, every run fails: GitLab won't change the visibility of a hidden variable.
  • A variable that existed in GitLab before the sync, with raw: false and the same value, keeps variable expansion enabled.
  • When GitLab can't mask a value, the sync error is GitLab's {"message":{"value":["is invalid"]}}, which doesn't say why.
  • The timeout on requests applies per socket read, not to the whole request, and the project/group listings have no page cap.
  • Overlapping runs of the same sync are possible: cancel_sync_tasks removes queued jobs but doesn't stop running ones.
  • Any member who can update integration credentials can repoint another App's sync credential to a host they control. UpdateSyncAuthentication also accepts a credential of another provider.
  • The Back button of the other sync forms has the same missing type, so Enter in their text fields goes back to step 1.
  • Sync cards don't show the GitLab host, so syncs to the same path on two instances look identical.
  • trigger_sync_tasks queues the job before it creates the sync event. A job that starts at once finds no event, fails at the end and leaves the sync "in progress" (seen once in QA; affects all providers).

✨ How to Test the Changes Locally

  1. Run GitLab locally, e.g. docker run -p 8929:8929 -e GITLAB_OMNIBUS_CONFIG="external_url 'http://localhost:8929'" gitlab/gitlab-ce. Create a project with a few environments (production, staging, review/feature-1) and a personal access token with the api scope.
  2. In Phase, add GitLab credentials with host http://localhost:8929, enable SSE for an App and create a GitLab CI sync. Pick an environment scope in the new picker, or type review/*.
  3. Check the project's Settings → CI/CD → Variables in GitLab: the variables carry the chosen environment scope, and variables in other scopes are untouched.

💚 Did You...

  • Ensure linting passes (code style checks)?
  • Update dependencies and lockfiles (if required)
  • Update migrations (if required)
  • Regenerate graphql schema and types (if required)
  • Verify the app builds locally?
  • Manually test the changes on different browsers/devices? (Chromium only)

GitLab CI/CD variables can be scoped to an environment, and the same key
can exist once per scope. GitLab syncs can now target an environment
scope, like GitHub Actions syncs can target a GitHub environment.

- Add an optional environment scope to GitLab syncs. Pick one of the
  project's GitLab environments or type any scope GitLab accepts,
  including wildcards like review/*. New syncs default to all
  environments (*).
- A sync with a scope only creates, updates and deletes variables in
  that scope. Updates and deletes select the scope explicitly, so keys
  that exist in several scopes no longer fail with "There are multiple
  variables with provided parameters".
- Syncs created before this change have no scope and keep syncing to
  all environments. As before, they also update and delete a variable
  that was moved to another scope in GitLab, as long as its key has no
  variable for all environments and the scope doesn't belong to another
  sync. To add scoped syncs to the same project or group in an App,
  such a sync must be recreated with a scope first.
- GitLab tiers without scoped group variables silently create the
  variable for all environments. Group syncs with a scope check this
  with a throwaway variable before writing anything, and every create
  and update verifies the scope GitLab used, so a scoped secret is never
  left exposed to every environment.
- Reject a second sync in the same App to the same GitLab project or
  group and environment scope, as the two would overwrite each other's
  variables.
- GitLab API requests time out and no longer follow redirects, which
  could forward the token or a secret to another host. Tokens are
  trimmed and never included in errors.
- Add a gitlabEnvironments query to list a project's environments, and
  show the scope on the sync card.
- Keep a typed environment scope when the picker closes without a pick,
  so the scope that's shown is the one that's used instead of silently
  falling back to all environments. Escape discards it.
- Pressing Enter in the form no longer goes back to step 1: the Back
  button was the form's default submit button.
- Suggest the environment scopes already used by a group's variables for
  group syncs, as GitLab does (groups have no environments). Only scope
  names are fetched, and older GitLab versions get no suggestions.
- Detect syncs created before July 2024, which only stored the project
  or group path, when checking for conflicting syncs. A scoped sync could
  be created next to one, and the older sync then deleted its variables.
- Check for conflicting syncs when a sync's credentials change, or a
  credential's GitLab host changes, so two syncs can't end up writing the
  same project or group and scope. Staying on the same GitLab instance,
  e.g. to use a new token, is never blocked.
- Group variable PUTs fall back to another variable with the same key
  when the targeted one was deleted after it was listed, moving it into
  the sync's scope. Recreate variables of other scopes taken this way.
- Syncs created before environment scopes now report keys they can't
  update or delete because they exist in several scopes, instead of
  skipping deletions silently, after making all other changes.
- Follow redirects on the same GitLab host, e.g. http to https, as
  requests used to, so syncs using such hosts keep working. Redirects to
  other hosts are still refused, with a clearer error.
- An invalid typed environment scope is kept and blocks creating the
  sync, instead of falling back to the previous scope. Picking another
  group resets the scope, as picking another project does.
…https hosts

- Syncs created before environment scopes stopped at keys GitLab refused
  to change because they exist in several scopes, so variables were
  never deleted past them. Delete nothing while such a key exists, and
  name it in the error, so the upgrade can't remove variables pipelines
  rely on. New and changed secrets are still synced.
- Treat http:// and https:// (and default ports) of the same host as the
  same GitLab instance when checking for conflicting syncs.
- Decide whether a redirect stays on the GitLab host with the URL parser
  requests connects with: urllib.parse and urllib3 disagree on
  backslashes, which let a redirect pass the check and send the token and
  secrets elsewhere. Redirects with credentials, to other ports (except
  http to https on 443) or downgrading to http are refused, and each hop
  is validated in cloud. In cloud, GitLab hosts the two parsers read
  differently are refused too.
- Before updating a group variable, check that it still exists, and
  create it if not: GitLab's group PUT would otherwise update another
  variable with the key. This replaces recreating variables afterwards,
  which could bring back variables deleted in the meantime.
- Syncs created before environment scopes delete nothing exactly when
  they used to stop: a project key whose last-listed variable differs,
  or a removed key with variables in several scopes. Group updates never
  stopped, so group syncs keep deleting next to overrides.
- Store the ID of syncs that only stored a project or group path on
  their next run, so they're recognised after a rename.
- Syncs created before environment scopes conflict with scoped syncs to
  the same project or group in any App of the organisation.
- Keep a scope chosen before the first project or group is picked.
- Don't name syncs of Apps a member can't access in credential errors.
@nimish-ks
nimish-ks marked this pull request as draft October 4, 2026 10:05
rohan-chaturvedi and others added 13 commits October 14, 2026 11:11
Syncs created before environment scopes compared each secret with the
variable listed last. For a group key with variables in several scopes and
none for all environments, they then updated the key without a scope
filter, and GitLab chose the variable, e.g. the synced one moved to another
scope by hand. These syncs now failed whenever any of the key's variables
differed, and stopped updating the moved variable.

Send the same unfiltered update, in the same cases, so they behave as
before. A key with a variable in another sync's scope is still reported:
GitLab could choose that variable.
Projects with review apps can have thousands of GitLab environments. The
picker rendered all of them, and the combobox re-sorts its options each
time one is added or removed, so opening and closing the list froze the
page (about 11 s and 69 s with 2,000 environments).

List at most 50 scopes, with an exact match first, and say how many more
match. Typing still searches all environments, so any of them can be
picked.
Syncs created before environment scopes followed one rule for every key:
compare the secret with the variable listed last, and if it is changed,
update the key without a scope filter. For a key with a variable for all
environments and an override, they now compared and updated the "*"
variable instead. That overwrote "*" where they used to change nothing,
e.g. with the override's production value, which then reached every job.

Use the old rule for every key again. Projects still fail at such a key,
as before, but now sync all other keys, delete nothing and name the key.
To sync only "*" and leave overrides alone, create the sync again with the
all environments scope.
Headless UI only keeps the selected option active if it is rendered. A
scope past the first 50 environments, or a typed scope like review/*,
wasn't rendered, so Enter or Tab after reopening the list switched the
sync to all environments (*). List the selected scope when there is no
query.

Also list an exact match with the same case first, so typing production
picks production rather than Production.
…eletion

perform_gitlab_sync looked up the ID of syncs that only stored a path
before handle_sync_event checked the org deletion lock, so a job queued
before the lock still called GitLab.
A GitLab host entered as http:// behind a proxy that redirects with
307/308 to https:// on another port worked before this change and then
failed. Redirects to another host are still refused.
When a redirect to another host is refused, say where GitLab redirected
to (scheme, host and port only), so users know which GitLab host to
use in their credentials.
@rohan-chaturvedi
rohan-chaturvedi marked this pull request as ready for review November 2, 2026 12:16
@rohan-chaturvedi
rohan-chaturvedi self-requested a review November 2, 2026 12:16
@rohan-chaturvedi
rohan-chaturvedi merged commit 19707b7 into main Nov 2, 2026
15 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.

Sync to specific GitLab environments

2 participants