Skip to main content

Git links

An issue in Crewship and the pull request that resolves it used to be two unconnected facts. Git links connect them: paste a pull-request or merge-request URL onto an issue and Crewship recognises the provider, fetches the request through that provider’s API with a credential you already store, and keeps its title, state, author and branches on the issue. It works the same way for GitHub and GitLab, on the public services and on self-hosted instances of either — and it needs no webhook and no inbound network access, which matters because most Crewship instances are not reachable from github.com.
Attaching a link never changes the issue’s status. Auto-transitioning an issue when its pull request merges, Fixes ENG-123 magic words, and branch-name generation are deliberately not part of this release.

In the issue detail

Open an issue — /issues/<identifier>, or the centre pane of /issues; they are the same screen — and its links are a Pull requests card under Description and Links. GitLab merge requests appear in the same card. Each row leads with the state, because a merged pull request, a closed one, an open draft and an open ready-to-review one are four different things and it is the thing you are scanning for. The states line down the left edge: After the state come the title, owner/repo#number, the author, the branch pair and when the state was last confirmed. The title, author and branch names come from the forge and are shown as plain text — they are never rendered as markdown or HTML, so a pull-request title cannot smuggle markup into your issue. The pull-request icon in the card header attaches a link: paste the URL, press Enter. When it fails, the reason appears under the box with your URL still in it — including the one you will hit most, “no ACTIVE GITHUB credential in this workspace can reach ghe.acme.internal…”, which names both halves of the fix. Add the credential, label it with the host, press Attach again. Hovering a row reveals two actions: refresh (the relink command — re-read the state now) and remove. Removing takes no confirmation; nothing is asserted by a link and nothing is left behind when it goes. A row whose last refresh failed keeps the state it had and says so in amber, with the reason and when the state was last true. Refreshing it again is one click; the row updates whether the refresh succeeds or fails, so the reason stays on the card rather than only in a toast.
Attaching, refreshing and removing need MANAGER or above. A reader below that sees the same card with the same links and no write affordances at all.
The response carries what the provider said:
state is normalised to one of OPEN, DRAFT, MERGED or CLOSED across both providers. A merged GitHub pull request reports itself as closed with a merged_at; Crewship files that as MERGED, not CLOSED.

The other commands

Each maps to one route under /api/v1/crews/{crewId}/issues/{identifier}/code-links (GET, POST, POST …/{linkId}/refresh, DELETE …/{linkId}). Attaching, refreshing and removing need MANAGER or above; listing needs any workspace member. State is not polled. A link shows what the last successful fetch returned, with last_synced_at saying when that was; relink is how you ask again.

Which URLs are recognised

Trailing path (/files), query strings and fragments are ignored, so a link copied from the “Files changed” tab is the same link as one copied from the conversation tab — attaching both is a 409, not two rows. Self-hosted instances are recognised without configuration. The provider is identified from the URL path, not the hostname: every GitHub deployment, Enterprise Server included, serves /pull/<n>, and no GitLab deployment does. A hostname allowlist would recognise github.com and gitlab.com and fail every self-hosted instance — the opposite of what this feature is for. Recognising it is not the same as reaching it. A self-hosted forge still needs a credential labelled with its host (see Credentials), and one on a private address also needs the operator opt-in (below). Anything else — an issue URL, a repository root, a commit — is rejected with a 400 and a message naming the shape it expected.

Credentials

Git links do not introduce a new place to keep secrets. They use the credentials already in the workspace: an ACTIVE, non-deleted credential whose provider is GITHUB or GITLAB, scoped either to the whole workspace or to the crew that owns the issue. The token is sent in a request header — never in a URL, a query string or a command line. Which credential is used:
  1. The one labelled with the host. Set the credential’s account label to the forge’s host (ghe.acme.internal, gitlab.acme.internal:8443) and it is matched by host. This is how multiple forges, or multiple accounts on one forge, are told apart.
  2. Otherwise, for github.com / gitlab.com only, the workspace’s oldest active credential for that provider.
For any other host, an unlabelled credential is not used — sending a github.com token to an arbitrary pasted host is exactly the accident this rule prevents. Add the label and the link resolves. The matched credential does more than supply the token: the account label is the address the API request is sent to. The label and the pasted host are the same string — that is how the credential was found — so this changes nothing about which forge is reached. It means the request target comes from a record your workspace stored rather than from the link somebody pasted, which is why an unlabelled credential cannot reach a self-hosted host at all.
The token needs read access to the repository: repo scope on GitHub, read_api on GitLab. Every fetch records a USE event against the credential, so a revoked key’s blast radius is a query rather than an investigation.

Self-hosted forges on a private network

By default Crewship refuses to fetch from private, loopback and link-local addresses. A pasted URL is user-supplied, and the server attaches a credential to whatever it dials, so the strict posture is the default: a link cannot be used to probe the network Crewship runs on, and DNS names that resolve to internal addresses are refused at connect time, not just at parse time. If your GitLab or GitHub Enterprise lives on an intranet address, an admin turns the opt-in on:
That reopens RFC1918, loopback and ULA addresses, and allows http:// targets. It does not reopen cloud metadata (169.254.169.254 and its IPv6 forms), multicast or reserved ranges — those stay refused regardless.

When something goes wrong

Failures come back as RFC 7807 problem documents with a stable type URI and a code, because several of them share an HTTP status and need different fixes. A refresh that fails keeps the state it already had and records the reason on the link (last_sync_error), so the UI and crewship issue links show “merged, last checked Tuesday, refreshing is failing because the token was revoked” instead of losing what was known.

What agents see

An agent reading an issue through the sidecar gets its links, with one difference from the browser view: the pull request’s title, author and branch names are wrapped in an <untrusted> block. Those strings are written by whoever opened the pull request — on a public repository, by anyone — and the fence is what tells the model to treat them as data rather than instructions. The URL, provider and state are Crewship’s own parsed values and are not fenced. See Lookout for the fence’s contract.

What’s next

The link table records the provider’s merged_at from day one, and is indexed for the reverse lookup (“which issue does this pull request belong to?”). That is the groundwork for the phase-2 behaviour — moving an issue to Done when its pull request merges — which will be additive.