Skip to content

Conversation

@danudey
Copy link
Contributor

@danudey danudey commented Sep 29, 2025

The Semaphore CLI tool has the ability to detect the current project, but there are a few caveats:

  1. It relies on the assumption that origin is the upstream remote; this is not a safe assumption because when creating a fork the gh cli tool will rename the upstream remote to upstream and set the new fork to be origin.
  2. This functionality is never exposed to the user directly

This means that if a user needs the current project ID (e.g. to trigger a pipeline via the API), they must get the list of all remote URLs themselves, get the list of all projects themselves, find a match between the two, and then call sem get project <project name> to get the project's ID.

This PR does a few things (updated from the previous implementation):

First, we add a new function getAllGitRemotesAndProjects(), which does the following:

  1. Gets all the configured git remotes and their URLs via git config
  2. Gets all the configured projects in the current Semaphore context
  3. Produces a list of (remoteName, remoteURL, semaphoreProject) where the project URL matches the remote URL; in other words, every configured remote for which a configured project exists.

Secondly, we also add a small helper function, getGitHubBaseRemoteName(), which checks the repository's git configuration via git config to see if the user has run gh repo set-default to choose a default remote.

Thirdly, we refactor InferProject() to call getAllGitRemotesAndProjects() and then choose one of the results with the following detection logic:

  1. If we only have one result, use that
  2. If the user has used gh repo set-default and that remote is in the list, use that
  3. If the results include an origin repo, use that; if not but there's an upstream repo, use that
  4. If the multiple non-default, non-origin, non-upstream remotes all have the same URL, return the project matching that URL
  5. At this point, we have multiple remotes with non-standard remote names and different URLs, all of which have different projects configured for them; we can't guess a correct project so give up.

The original InferProject() simply chose the origin remote and used that, so this new logic will cover those cases as well, and thus shouldn't cause issues with the rest of the codebase.

Lastly, we add a new sem get current_project (alias sem get cur) which simply prints out the project's metadata:

sem get current_project
04ebc08e-437f-45e0-9abd-b5d1d66bb126 projectname [email protected]:thisorg/project.git

Originally I was planning on highlighting the current project when a user did sem get project without an argument, but that ended up feeling messier than just having a separate command for it which a user could parse.

Edit: Added a --json flag to current_project to format the output as JSON (which includes far more data).

Edit 2: Bumped golang version from 1.20 to 1.21 to get the slices package (should go to 1.25 IMHO but this is a minimal change), and added -buildvcs=false to work around git permissions warnings in docker container when running make build

Edit 3: Completely rewrote almost everything into a vastly simplified and yet more complicated implementation.

@hamir-suspect hamir-suspect self-requested a review October 2, 2025 09:46
@hamir-suspect
Copy link
Contributor

Respect remote order when inferring project — cmd/utils/project.go:45-63

With the new InferProject()/getProjectFromUrls() flow we now pick the first project returned by ListProjects() whose URL matches any of the repository remotes (slices.Contains(urls, p.Spec.Repository.Url)).

When a repo has multiple remotes that each point to different Semaphore projects (e.g. origin for the main project and staging for an internal clone), the API’s ordering determines which project we pick.

There is no guarantee that the API returns the origin project (or the user’s intended default) before the others, so we can end up targeting the wrong project for every sem get … command that relies on InferProjectName(), as well as for the new current_project command.

We should iterate in remote order (preferably checking origin first) instead of project-order to avoid returning the wrong project.

In previous implementations, we would infer the current project by
getting a list of all remote URLs and then choosing the first project
that matched one of them, which isn't necessarily the correct one.

In this commit, we add some logic to try to make the best choice
possible, using the following logic:

1. Get a list of all remotes, with their names and URLs
2. Get a list of all projects from Semaphore
3. Make a list of all remotes where there is a project configured
   for that remote's URL (so all remotes wihch have an associated
   project in Semaphore)

Once we have that, we choose one based on the following logic:
1. If there's only one, choose that
2. If the user has run `gh repo set-default` in this repository,
   use the repository they chose at that point.
3. If there's one named "origin" choose that one. This is git's
   default remote name when you clone.
4. If there's one named "upstream" choose that one. This is what
   the `gh` CLI will rename your "origin" repo to when you create
   a fork for a new PR.
5. If there's more than one remote but they all have the same URL,
   then just use whichever project matches that URL.
6. If we still can't decide, print an error to the user; they're
   probably doing something weird.
@danudey
Copy link
Contributor Author

danudey commented Oct 22, 2025

@hamir-suspect Alright, two updates:

  1. I put the buildvcs param back and made another change to test; it does require the docker container to be rebuilt, though, which docker compose doesn't seem to want to do automatically. I think docker-compose build will do this though?
  2. I rewrote almost everything else I wrote to be both more and less complicated, but to be a little smarter about what project it picks if there's more than one.

One decision I'd like input on: currently, if the user has run gh repo set-default in their repository to choose a default remote for creating PRs against, we infer that remote to be the current project. This would only be a breaking change if:

  1. The user had an origin remote pointing to a repo associated with semaphore project A; and
  2. Another remote pointing to a repo associated with a different semaphore project B which they had chosen as their default via gh repo set-default.

In other words, they're creating PRs against project B but origin points to project A.

We can flip this logic so that if they have a matching origin or upstream repo we use that (which guarantees we'd match the old logic if applicable), and then we fall back to the gh-configured default repo. For me the current behavior makes sense, but y'all have more experience with users so I'm happy to flip it if it makes more sense this way.

@danudey danudey requested a review from hamir-suspect October 22, 2025 22:40
@hamir-suspect
Copy link
Contributor

hamir-suspect commented Oct 29, 2025

Hey @danudey totally agree with the current logic—keeping the gh repo set-default remote as the first choice. Just a small nit-pick and we can merge this

Co-authored-by: Amir Hasanbasic <[email protected]>
@danudey danudey requested a review from hamir-suspect October 31, 2025 18:10
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.

2 participants