@@ -43,13 +43,29 @@ export type TriggerUriScope = {
4343 slug : string ;
4444 project : { slug : string ; externalRef : string } ;
4545 organization : { slug : string } ;
46+ /**
47+ * The project's connected repository, when the caller resolved one. Only a
48+ * `source` URI needs it: the URI carries the commit and the repo-relative path,
49+ * but "which repo" lives on the project's GitHub connection
50+ * (`ConnectedGithubRepository.repository.fullName`) or on the deployment's git
51+ * metadata (`WorkerDeployment.git.remoteUrl`) — either is enough here. Omit it
52+ * and a source URI resolves to nothing, which is the honest answer for a
53+ * project with no repo connected.
54+ */
55+ repository ?: { fullName ?: string | null ; remoteUrl ?: string | null } | null ;
4656} ;
4757
4858export type ResolvedTriggerUri = {
4959 /** Short human label for the resource, e.g. a run id or a queue name. */
5060 label : string ;
51- /** Dashboard path, relative to the app origin. */
61+ /** Dashboard path, relative to the app origin — unless `external` is set . */
5262 url : string ;
63+ /**
64+ * True when `url` is an absolute link off the dashboard (today: a GitHub blob
65+ * for a `source` URI). A host must open it as a link, never hand it to the
66+ * router.
67+ */
68+ external ?: boolean ;
5369} ;
5470
5571/**
@@ -71,6 +87,44 @@ export function resolveTriggerUri(
7187 return resolveInScope ( scope , parsed . data ) ;
7288}
7389
90+ const GITHUB_ORIGIN = "https://github.com" ;
91+ /** `owner/repo` — GitHub's own character set, and nothing that could add a path. */
92+ const FULL_NAME = / ^ [ A - Z a - z 0 - 9 . _ - ] + \/ [ A - Z a - z 0 - 9 . _ - ] + $ / ;
93+
94+ /**
95+ * The repository's canonical `https://github.com/{owner}/{repo}` base, or null.
96+ *
97+ * `fullName` is the reliable input (it comes off the GitHub connection);
98+ * `remoteUrl` is the deployment's own git metadata, so it can be an SSH remote or
99+ * carry credentials and is normalized the same way the deployments UI does it
100+ * (see `BranchesPresenter`). Anything that isn't github.com is rejected rather
101+ * than guessed at — a wrong link is worse than no link.
102+ */
103+ function githubRepoBaseUrl ( repository : TriggerUriScope [ "repository" ] ) : string | null {
104+ const fullName = repository ?. fullName ?. trim ( ) ;
105+ if ( fullName && FULL_NAME . test ( fullName ) ) return `${ GITHUB_ORIGIN } /${ fullName } ` ;
106+
107+ const remoteUrl = repository ?. remoteUrl ?. trim ( ) ;
108+ if ( ! remoteUrl ) return null ;
109+
110+ const normalized = remoteUrl
111+ . replace ( / ^ g i t @ g i t h u b \. c o m : / , `${ GITHUB_ORIGIN } /` )
112+ . replace ( / ^ s s h : \/ \/ g i t @ g i t h u b \. c o m \/ / , `${ GITHUB_ORIGIN } /` )
113+ . replace ( / \. g i t $ / , "" ) ;
114+
115+ let url : URL ;
116+ try {
117+ url = new URL ( normalized ) ;
118+ } catch {
119+ return null ;
120+ }
121+ if ( url . hostname !== "github.com" ) return null ;
122+
123+ const path = url . pathname . replace ( / ^ \/ + | \/ + $ / g, "" ) ;
124+ if ( ! FULL_NAME . test ( path ) ) return null ;
125+ return `${ GITHUB_ORIGIN } /${ path } ` ;
126+ }
127+
74128/** True when the URI names this exact project and environment. */
75129function isInScope ( scope : TriggerUriScope , parsed : ParsedTriggerUri ) : boolean {
76130 return parsed . projectRef === scope . project . externalRef && parsed . environmentId === scope . id ;
@@ -128,11 +182,26 @@ function resolveInScope(
128182 label : parsed . version ,
129183 url : v3DeploymentVersionPath ( organization , project , environment , parsed . version ) ,
130184 } ;
185+ case "source" : {
186+ // A source citation is the investigation card's code grounding, so it has
187+ // to be clickable: the URI already pins the commit and the repo-relative
188+ // path, and the connected repo says where that lives. Without a repo
189+ // connection there is nothing to open — the label still renders.
190+ const base = githubRepoBaseUrl ( scope . repository ) ;
191+ const label = parsed . line === undefined ? parsed . path : `${ parsed . path } :${ parsed . line } ` ;
192+ if ( ! base ) return null ;
193+ const path = parsed . path . split ( "/" ) . map ( encodeURIComponent ) . join ( "/" ) ;
194+ const fragment = parsed . line === undefined ? "" : `#L${ parsed . line } ` ;
195+ return {
196+ label,
197+ url : `${ base } /blob/${ encodeURIComponent ( parsed . sha ) } /${ path } ${ fragment } ` ,
198+ external : true ,
199+ } ;
200+ }
131201 // No dashboard page exists for these yet. Returning null keeps the caller
132202 // honest (it renders a label with no link) instead of inventing a URL that
133203 // 404s. Add a case here the day the page ships.
134204 case "report" :
135- case "source" :
136205 case "investigation" :
137206 return null ;
138207 default : {
0 commit comments