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.
Attach a link
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
/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 isGITHUB 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:
- 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. - Otherwise, for
github.com/gitlab.comonly, the workspace’s oldest active credential for that provider.
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: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 stabletype 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’smerged_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.