gitlab icon indicating copy to clipboard operation
gitlab copied to clipboard

Semantic release authentication precedence prevents pipeline triggers when GitLab CI_JOB_TOKEN has push permissions

Open tachyons opened this issue 4 months ago • 16 comments

Summary

When GitLab's "push with CI Job token" feature is enabled, semantic-release pushes fail to trigger pipelines even when configured with a Personal Access Token (PAT). This breaks continuous deployment workflows that depend on semantic-release triggering subsequent pipelines.

Background

GitLab recently introduced the ability to push using CI_JOB_TOKEN. As a security measure, commits pushed with CI_JOB_TOKEN intentionally don't trigger new pipelines (for now) to prevent infinite loops. However, this creates an issue with how semantic-release/gitlab handles authentication.

The Problem

semantic-release/gitlab appears to use CI_REPOSITORY_URL for git operations, which includes embedded CI_JOB_TOKEN credentials:

https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/group/project.git

Even when semantic-release is configured with a PAT (via GITLAB_TOKEN), semantic-release prioritizes the credentials embedded in the remote URL over other authentication methods. This means:

  1. All pushes authenticate with CI_JOB_TOKEN instead of the provided PAT
  2. GitLab correctly identifies these as CI_JOB_TOKEN pushes and skips pipeline triggers
  3. Release workflows break when they depend on subsequent pipeline triggers

Current Workaround

Users can manually override the remote URL before running semantic-release:

release:
  script:
    - git remote set-url origin https://gitlab.com/${CI_PROJECT_PATH}.git
    - npx semantic-release

Impact

This affects all GitLab users who:

  • Use semantic-release for automated releases
  • Have "CI_JOB_TOKEN can push" enabled (increasingly common as it's a recommended security practice)
  • Depend on release commits/tags triggering deployment pipelines

Additional Context

  • GitLab Issue: [#560654](https://gitlab.com/gitlab-org/gitlab/-/issues/560654)
  • The issue occurs because Git's credential precedence favors URL-embedded credentials over credential helpers

Testing

I'm happy to help test any proposed solutions. As a GitLab engineer, I can also provide additional context about GitLab's authentication behavior if needed.

tachyons avatar Aug 21 '25 08:08 tachyons

Thanks for reaching out proactively and offering your help here @tachyons! Do you have a sample project that shows this behavior?

Git auth is handled in semantic-release core at https://github.com/semantic-release/semantic-release/blob/master/lib/get-git-auth-url.js.

As you have already noticed the logic there does not append user-provided credentials to the remote URL if authentication works without them. I'm looping in @semantic-release/maintainers for their opinions here.

fgreinacher avatar Aug 24 '25 11:08 fgreinacher

@fgreinacher Here is a repo with this bug https://gitlab.com/tachyons-gitlab/semantic-bug

tachyons avatar Aug 25 '25 16:08 tachyons

Current Workaround

Users can manually override the remote URL before running semantic-release

based on this workaround, you're most of the way to understanding how this is working.

if your project does not have a package.json file, which appears to be the case in your example, semantic-release discovers the repository url from the defined remote origin. based on that discovered url, this is the logic applied to inject auth appropriately.

CI_REPOSITORY_URL for git operations, which includes embedded CI_JOB_TOKEN credentials:

https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/group/project.git

i haven't navigated through all of the logic, but i expect that the presence of a token that has push access already existing in the defined origin url results in skipping the logic for injecting alternate credentials. it seems a bit strange to me to include the token in the remote url like that. is that still something that could be changed?

i'm not a gitlab user, so i am not very familiar with the specifics, but with github actions, there are options that can be provided to avoid the workflow credentials being used beyond checkout, like persist-credentials, or allowing a custom token to be injected. if the remote url needs the ci-job-token for checkout, it feels like there may be a need to provide a similar option to either prevent the token from existing beyond checkout, or to allow injecting the PAT at that stage rather than relying on semantic-release to have logic to override for the gitlab specific scenario.

i'd really like to avoid having custom logic for gitlab handling of standard git logic. do any of the thoughts above trigger thoughts around options that might be possible to make this scenario less unique to gitlab?

travi avatar Aug 25 '25 20:08 travi

it seems a bit strange to me to include the token in the remote url like that. is that still something that could be changed?

This is an existing behaviour and we use this to enable git clone to work on pipeline without any additional authorization. Changing this behaviour can be a breaking change from our end.

tachyons avatar Aug 26 '25 06:08 tachyons

i assumed that would likely be the case. is the suggestion to provide ways to override that default behavior more possible, at least allowing a PAT to be used instead? i really dont think custom auth logic should be a responsibility of semantic-release here

travi avatar Aug 26 '25 14:08 travi

@travi I'm checking internally to see if there anything we can do from Gitlab's end without breaking changes.

2025-08-21T01:41:43.490Z semantic-release:get-git-auth-url Verifying ssh auth by attempting to push to https://gitlab-ci-token:[secure]@gitlab.com/tachyons-gitlab/semantic-bug.git 2025-08-21T01:41:44.063Z semantic-release:get-git-auth-url SSH key auth successful.

https://github.com/semantic-release/semantic-release/blob/d12fd098ae1f600c117146953a00c4badf7ec468/lib/get-git-auth-url.js#L90

// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
  try {
    debug("Verifying ssh auth by attempting to push to  %s", repositoryUrl);
    await verifyAuth(repositoryUrl, branch.name, { cwd, env });
  } catch {
    debug("SSH key auth failed, falling back to https.");
    const envVars = Object.keys(GIT_TOKENS).filter((envVar) => !isNil(env[envVar]));

As of now semantic-release interpret existing behaviour as pushing with SSH key as git push dry run ran successfully. So It skipped injecting auth token given through GL_TOKEN env var

tachyons avatar Aug 26 '25 16:08 tachyons

correct. while the debug logs and the code comments are slightly inaccurate, the check is whether push is possible with existing auth. since the token is already wired into the remote definition, existing auth does enable the push to happen, so there should be no need to configure other methods.

this is why i suggested the options above, since we either need to remove the existing auth before this check happens or enable that existing auth to use the desired PAT instead. i dont believe this should be a step that semantic-release does, but could instead be enabled as part of the workflow definition similar to how github actions enables these options.

thank you for initiating the internal conversations. very interested in the result

travi avatar Aug 26 '25 17:08 travi

If we do this on semantic release, solution would be something like

/**
 * Get the repository remote URL with any authentication credentials stripped.
 *
 * @param {Object} [execaOpts] Options to pass to `execa`.
 *
 * @return {string} The value of the remote git URL without auth credentials.
 */
export async function repoUrl(execaOptions) {
  try {
    const url = (await execa("git", ["config", "--get", "remote.origin.url"], execaOptions)).stdout;
    
    if (url.startsWith('https://')) {
      try {
        const parsedUrl = new URL(url);
        // Clear any auth information
        parsedUrl.username = '';
        parsedUrl.password = '';
        return parsedUrl.toString();
      } catch (urlError) {
        // Fallback to regex if URL parsing fails
        return url.replace(/^(https:\/\/)([^@]+@)(.+)$/, '$1$3');
      }
    }
    
    // Return all non-HTTPS URLs unchanged (SSH, HTTP, etc.)
    return url;
  } catch (error) {
    debug(error);
  }
}

Alternative is to set GIT_STRATEGY variable to none. So that gitlab CI won't download the repo by default. The users can download the repo manually with the authentication they set.

tachyons avatar Sep 01 '25 08:09 tachyons

If we do this on semantic release, solution would be something like

it isnt safe for us to always assume that existing credentials are not desired. if those credentials enable all that is needed for a workflow to be able to release, that may be desired by our users in other contexts than this one.

Alternative is to set GIT_STRATEGY variable to none. So that gitlab CI won't download the repo by default. The users can download the repo manually with the authentication they set.

this seems more in line with solving for the uniqueness of this situation withing the context of gitlab. it seems like a slightly less desirable experience, but that may be the necessary approach in order to account to the specific design decisions made on the gitlab side

travi avatar Sep 01 '25 15:09 travi

Is there any progress on this?

marchermans avatar Sep 16 '25 06:09 marchermans

Alternative is to set GIT_STRATEGY variable to none. So that gitlab CI won't download the repo by default. The users can download the repo manually with the authentication they set.

this seems more in line with solving for the uniqueness of this situation withing the context of gitlab. it seems like a slightly less desirable experience, but that may be the necessary approach in order to account to the specific design decisions made on the gitlab side

I don't think this is a good idea as it really degrades the developer experience for a standard use case. I'd prefer if we could somehow find a solution that "just works". I agree semantic-release core should not have to care about this GitLab specific situation. Could we somehow allow the GitLab plugin help core tell the "correct URL" (without credentials)?

fgreinacher avatar Sep 17 '25 06:09 fgreinacher

I don't think this is a good idea as it really degrades the developer experience for a standard use case. I'd prefer if we could somehow find a solution that "just works".

i'm fully behind providing as good of a user experience as possible and i'd love to get to a point where this could "just work", but semantic-release isnt the only part of that puzzle. in this case, i'm suggesting that the gitlab default behavior is what is causing that degradation. my point of reference is github actions, where the checkout action provides two options for overriding the default behavior in order to avoid conflicting with semantic-release's later steps. even in this case, the default behavior can cause problems, but having the simple config options makes it easy enough to overcome. please help me see better options if they exist.

Could we somehow allow the GitLab plugin help core tell the "correct URL" (without credentials)?

i could use help seeing a path to this that wouldnt be only for gitlab. as mentioned above, a similar situation exists with github actions that did not require special behavior in core or overrides in a plugin because the action provided by github provides the necessary config options

travi avatar Sep 19 '25 20:09 travi

The code around https://github.com/semantic-release/semantic-release/blob/d12fd098ae1f600c117146953a00c4badf7ec468/lib/get-git-auth-url.js#L90-L93 only mentions the SSH scenario which makes total sense to me. I'm wondering though whether there is a real use case for embedding credentials in an HTTPS clone URL. As far as I understand semantic-release core as well as the plugins require explicitly configured credentials, see e.g. https://semantic-release.gitbook.io/semantic-release/usage/ci-configuration#push-access-to-the-remote-repository. Maybe we can limit the Git push check to SSH URLs only?

fgreinacher avatar Oct 05 '25 11:10 fgreinacher

Hi @tachyons, I recently noticed the new "push with CI Job token" feature and was trying to test it out on one of my projects (historically I've always set up various Access Tokens to do this). I've run in to an issue which I think is distinct from the above but I thought I'd check here before raising a new issue. The error I'm receiving is:

TypeError: Cannot read properties of undefined (reading 'project_access')
        at default (file:///usr/local/lib/node_modules/@semantic-release/gitlab/lib/verify.js:64:40)
        at async verifyConditions (file:///usr/local/lib/node_modules/@semantic-release/gitlab/index.js:11:3)
        at async validator (file:///usr/local/lib/node_modules/semantic-release/lib/plugins/normalize.js:36:24)
        at async file:///usr/local/lib/node_modules/semantic-release/lib/plugins/pipeline.js:38:36
        at async Promise.all (index 0)
        at async next (file:///usr/local/lib/node_modules/semantic-release/node_modules/p-reduce/index.js:15:44)
    at file:///usr/local/lib/node_modules/semantic-release/lib/plugins/pipeline.js:55:13
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async pluginsConfigAccumulator.<computed> [as verifyConditions] (file:///usr/local/lib/node_modules/semantic-release/lib/plugins/index.js:87:11)
    at async run (file:///usr/local/lib/node_modules/semantic-release/index.js:106:3)
    at async Module.default (file:///usr/local/lib/node_modules/semantic-release/index.js:278:22)

I've set my GITLAB_TOKEN to the CI_JOB_TOKEN and I believe the above error is happening because a CI_JOB_TOKEN doesn't have access to the https://gitlab.com/api/v4/projects/:id endpoint and so the verify phase of the plugin is failing to determine that it has write access to the repository, even though the main semantic release process has already confirmed access via the remote url:

[1:47:14 PM] [semantic-release] › ✔  Allowed to push to the Git repository

Do you think I should raise a separate issue for this?

mrhornsby avatar Oct 12 '25 19:10 mrhornsby

Having investigated further I've realised that the semantic-release-gitlab plugin is really only to add issues and release entries in Gitlab and semantic-release without this plugin is working as expected with the CI_JOB_TOKEN. I'll look to raise a feature request with Gitlab to consider whether the CI_JOB_TOKEN could be given the additional API access required to support this plugin.

mrhornsby avatar Oct 13 '25 06:10 mrhornsby

@mrhornsby That is still an open issue

tachyons avatar Oct 13 '25 13:10 tachyons