Skip to content

Commit

Site: /g1t leads to /flagon-io, registries follow, and @g1t stays the agent

A page under a workspace alias or old name 301s to the same page under the workspace, keeping the query; a click's .data request is sent to the page, without _routes. Reserved names are looked up, as aliases can be reserved. A package registry request that finds nothing under an alias or old name 301s (308 for a publish) to the same path under the workspace. @g1t in markdown links to how g1t's agent works, never to /g1t.

syntaqxcommitted Parentdbe1a30Browse files
10 files+154−240/10 viewed
+11−1
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import { rehypeReferences } from "./markdown-plugins.ts";
4+import { G1T_MENTION_HREF, rehypeReferences } from "./markdown-plugins.ts";
55
66 type Node = { type: string; value?: string; tagName?: string; properties?: Record<string, unknown>; children?: Node[] };
77
1919 assert.deepEqual(links("me@example.com"), []);
2020 });
2121
22+test("@g1t is g1t's agent, never the workspace alias at /g1t", () => {
23+ assert.deepEqual(links("@g1t fix this, then ask @G1T again"), [
24+ [G1T_MENTION_HREF, "@g1t"],
25+ [G1T_MENTION_HREF, "@G1T"],
26+ ]);
27+ assert.ok(!G1T_MENTION_HREF.startsWith("/"));
28+ // Names that only start with g1t are anyone's.
29+ assert.deepEqual(links("@g1t-fans"), [["/g1t-fans", "@g1t-fans"]]);
30+});
31+
2232 test("@workspace/team links to the team", () => {
2333 assert.deepEqual(links("cc @acme/backend, @ana"), [
2434 ["/acme/-/teams/backend", "@acme/backend"],
+12−3
6060 };
6161 }
6262
63+/**
64+ * Where `@g1t` leads: g1t's own agent, which has no profile. Never `/g1t`,
65+ * which staff made an alias of Flagon, Inc.'s workspace: mentioning g1t
66+ * always means the agent.
67+ */
68+export const G1T_MENTION_HREF = "https://docs.g1t.sh/guides/working-with-g1t/";
69+
6370 /** `owner/repo#12`, `#12`, `@workspace/team`, `@name` and commit hashes, in one pass. */
6471 const REFERENCE =
6572 /(?<![\w/@#])(?:([a-z0-9][a-z0-9-]*\/[a-z0-9._-]+)#(\d+)|#(\d+)|@([a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38})\/([a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,59})|@([a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38})|([0-9a-f]{7,40}))(?![\w-])/gi;
7683 /**
7784 * Links what a forge's text refers to: `#12` to an issue or pull request
7885 * in this repository (the issue page sends a pull request's number on to
79− * it), `owner/repo#12` to one elsewhere, `@name` to a person or workspace,
80− * and a commit hash to its commit. Nothing inside code or a link changes.
86+ * it), `owner/repo#12` to one elsewhere, `@name` to a person or workspace
87+ * (`@g1t` to how g1t's agent works), and a commit hash to its commit.
88+ * Nothing inside code or a link changes.
8189 */
8290 export function rehypeReferences(options: { repo?: MarkdownRepo }) {
8391 const { repo } = options;
106114 } else if (workspace && team) {
107115 replacement = link(`/${workspace.toLowerCase()}/-/teams/${team.toLowerCase()}`, whole, "team");
108116 } else if (name) {
109− replacement = link(`/${name.toLowerCase()}`, whole, "mention");
117+ const href = name.toLowerCase() === "g1t" ? G1T_MENTION_HREF : `/${name.toLowerCase()}`;
118+ replacement = link(href, whole, "mention");
110119 } else if (hash && repo && /\d/.test(hash) && /[a-f]/i.test(hash)) {
111120 replacement = link(`/${repo.namespace}/${repo.name}/commit/${hash}`, hash.slice(0, 7), "commit");
112121 }
+24−1
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import { servicePath } from "./registry-paths.ts";
4+import { registryWorkspace, servicePath } from "./registry-paths.ts";
55
66 test("the container registry's paths go to the packages service", () => {
77 for (const path of [
119119 assert.equal(servicePath(path), null, path);
120120 }
121121 });
122+
123+test("a registry request names its workspace, and can be moved to another", () => {
124+ const cases: [string, string, string][] = [
125+ ["/-/cargo/g1t/index/config.json", "g1t", "/-/cargo/flagon-io/index/config.json"],
126+ ["/-/composer/g1t/packages.json", "g1t", "/-/composer/flagon-io/packages.json"],
127+ ["/-/maven/g1t/io/flagon/sdk/1.0/sdk-1.0.jar", "g1t", "/-/maven/flagon-io/io/flagon/sdk/1.0/sdk-1.0.jar"],
128+ ["/-/nuget/g1t/v3/index.json", "g1t", "/-/nuget/flagon-io/v3/index.json"],
129+ ["/-/rubygems/g1t/info/sdk", "g1t", "/-/rubygems/flagon-io/info/sdk"],
130+ ["/-/npm/@g1t%2fcli", "g1t", "/-/npm/@flagon-io%2fcli"],
131+ ["/-/npm/@g1t/cli/-/cli-1.0.0.tgz", "g1t", "/-/npm/@flagon-io/cli/-/cli-1.0.0.tgz"],
132+ ["/-/npm/-/package/@g1t%2Fcli/dist-tags/next", "g1t", "/-/npm/-/package/@flagon-io%2Fcli/dist-tags/next"],
133+ ["/v2/g1t/runner/manifests/latest", "g1t", "/v2/flagon-io/runner/manifests/latest"],
134+ ["/v2/G1T/runner/blobs/uploads/upl_1", "g1t", "/v2/flagon-io/runner/blobs/uploads/upl_1"],
135+ ];
136+ for (const [path, slug, moved] of cases) {
137+ const named = registryWorkspace(path);
138+ assert.equal(named?.slug, slug, path);
139+ assert.equal(named?.under("flagon-io"), moved, path);
140+ }
141+ for (const path of ["/v2", "/v2/", "/v2/token", "/-/npm", "/-/npm/-/whoami", "/-/npm/-/ping", "/-/npm/unscoped"]) {
142+ assert.equal(registryWorkspace(path), null, path);
143+ }
144+});
+24−0
2929 if (GIT_PATH.test(pathname)) return "git";
3030 return null;
3131 }
32+
33+/**
34+ * The workspace a registry request names, and `under`, the same path under
35+ * another workspace: `/-/cargo/<workspace>/…` and the other per-workspace
36+ * registries, npm's `@<workspace>` scope, and a container image's first
37+ * segment. Null for a path that names none, such as `/v2/token`.
38+ */
39+export type RegistryWorkspace = { slug: string; under: (slug: string) => string };
40+
41+const WORKSPACE_IN_PATH = [
42+ /^(\/-\/(?:composer|cargo|maven|nuget|rubygems)\/)([^/]+)(\/.*)?$/,
43+ /^(\/-\/npm\/(?:-\/package\/)?@)([^/%]+)((?:\/|%2[fF]).*)$/,
44+ /^(\/v2\/)([^/]+)(\/.+)$/,
45+];
46+
47+export function registryWorkspace(pathname: string): RegistryWorkspace | null {
48+ for (const pattern of WORKSPACE_IN_PATH) {
49+ const match = pattern.exec(pathname);
50+ if (!match) continue;
51+ const [, before, slug, after = ""] = match;
52+ return { slug: slug!.toLowerCase(), under: (to) => `${before}${to}${after}` };
53+ }
54+ return null;
55+}
+12−11
11 import { redirect } from "react-router";
22
3−import { isValidNamespace } from "@g1t/contracts";
3+import { isNamespaceShaped } from "@g1t/contracts";
44
55 import type { Viewer } from "@g1t/contracts";
66
77 import { identity, repos } from "./services.server";
8+import { underWorkspace } from "./workspace-nav";
89
910 /**
1011 * A workspace that was renamed keeps its old address as a redirect for
11− * `SLUG_HOLD_DAYS`. Call this where a page is about to 404 on its first
12− * path segment: if `slug` is an old name, it throws a 301 to the same
13− * address (path and query) under the current one. Otherwise it returns, and
14− * the caller 404s as before. Only the not-found path pays for the lookup.
12+ * `SLUG_HOLD_DAYS`, and an alias g1t's staff set (identity's aliases.rs:
13+ * `g1t` for `flagon-io`) leads to its workspace for good. Call this where a
14+ * page is about to 404 on its first path segment: if `slug` is an old name
15+ * or an alias, it throws a 301 to the same page (path and query) under the
16+ * workspace's slug now. Otherwise it returns, and the caller 404s as
17+ * before. Only the not-found path pays for the lookup.
1518 */
1619 export async function redirectIfRenamed(request: Request, slug: string): Promise<void> {
1720 const old = slug.toLowerCase();
18− // Nothing that could never have been a workspace's name is looked up.
19− if (!isValidNamespace(old)) return;
21+ // Nothing that could never have been a workspace's name or an alias is
22+ // looked up. Reserved names such as `g1t` can be aliases.
23+ if (!isNamespaceShaped(old)) return;
2024 let current: string | null = null;
2125 try {
2226 current = await identity.resolveSlug(old);
2630 }
2731 if (!current || current === old) return;
2832 const url = new URL(request.url);
29− const segments = url.pathname.split("/");
30− // segments[0] is the empty string before the leading slash.
31− segments[1] = current;
32− throw redirect(segments.join("/") + url.search, 301);
33+ throw redirect(underWorkspace(url.pathname, url.search, current), 301);
3334 }
3435
3536 /**
+16−1
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import { SETTINGS_PAGES, WORKSPACE_PAGES, pagePath, sidebarCurrent, workspacePage, workspaceRedirect } from "./workspace-nav.ts";
4+import { SETTINGS_PAGES, WORKSPACE_PAGES, pagePath, sidebarCurrent, underWorkspace, workspacePage, workspaceRedirect } from "./workspace-nav.ts";
55
66 test("a path is one of the workspace's pages, or none", () => {
77 assert.equal(workspacePage("/acme", "acme"), "overview");
7373 assert.equal(workspaceRedirect("/acme/-/members.data", "?_routes=routes%2Fworkspace%2Fmoved-members"), "/acme/-/people");
7474 assert.equal(workspaceRedirect("/acme/-/members.data", "?_routes=x&q=ada"), "/acme/-/people?q=ada");
7575 });
76+
77+test("an alias or old name leads to the same page under the workspace", () => {
78+ assert.equal(underWorkspace("/g1t", "", "flagon-io"), "/flagon-io");
79+ assert.equal(underWorkspace("/g1t/g1t/issues", "?q=is%3Aopen", "flagon-io"), "/flagon-io/g1t/issues?q=is%3Aopen");
80+ assert.equal(underWorkspace("/g1t/g1t/blob/main/README.md", "", "flagon-io"), "/flagon-io/g1t/blob/main/README.md");
81+ assert.equal(underWorkspace("/g1t/-/people/", "", "flagon-io"), "/flagon-io/-/people");
82+});
83+
84+test("a click's data request for an alias leads to the page, not its data", () => {
85+ assert.equal(underWorkspace("/g1t.data", "?_routes=routes%2Fworkspace%2Flayout", "flagon-io"), "/flagon-io");
86+ assert.equal(
87+ underWorkspace("/g1t/g1t/pulls.data", "?_routes=routes%2Frepo%2Flayout&state=closed", "flagon-io"),
88+ "/flagon-io/g1t/pulls?state=closed",
89+ );
90+});
+16−0
121121 }
122122
123123 /**
124+ * The page at `pathname` under the workspace `slug` instead of its first
125+ * segment, keeping its query: where an old name or an alias leads. A click
126+ * asks for the page's data at `<path>.data?_routes=…`; the address given is
127+ * the page's own, which the browser then asks for as usual.
128+ */
129+export function underWorkspace(pathname: string, search: string, slug: string): string {
130+ const params = new URLSearchParams(search);
131+ params.delete("_routes");
132+ const query = params.toString();
133+ const segments = pagePath(pathname).split("/");
134+ // segments[0] is the empty string before the leading slash.
135+ segments[1] = slug;
136+ return `${segments.join("/")}${query ? `?${query}` : ""}`;
137+}
138+
139+/**
124140 * Where an old address of a workspace's pages is now, keeping its query;
125141 * null when it has not moved. `/<workspace>?tab=projects` and the like
126142 * open that page.
+5−3
22
33 import type { Route } from "./+types/not-found";
44 import { redirectIfRenamed } from "../lib/renamed.server";
5+import { pagePath } from "../lib/workspace-nav";
56
67 /**
78 * Any address no other route matches. Throwing the 404 from a route, rather
89 * than leaving the router to, means the root loader still runs, so someone
910 * signed in sees the page in their own sidebar and not the public frame.
1011 *
11− * An address under a renamed workspace's old name is sent to the new one
12− * first, so deep links keep working the same as its pages do.
12+ * An address under a renamed workspace's old name, or under an alias, is
13+ * sent to the workspace's name first, so deep links keep working the same
14+ * as its pages do.
1315 */
1416 export async function loader({ request }: Route.LoaderArgs) {
15− const first = new URL(request.url).pathname.split("/")[1];
17+ const first = pagePath(new URL(request.url).pathname).split("/")[1];
1618 if (first) await redirectIfRenamed(request, first);
1719 throw data(null, { status: 404 });
1820 }
+25−3
11 import { createRequestHandler } from "react-router";
22
3+import { identityClient, isNamespaceShaped } from "@g1t/contracts";
4+
35 import { finishResponse, withRequestPerf } from "../app/lib/perf.server";
46 import { goImport } from "../app/lib/go-get";
57 import { repositoryOfPage, stillPublic } from "../app/lib/public-cache";
6−import { servicePath } from "../app/lib/registry-paths";
8+import { registryWorkspace, servicePath } from "../app/lib/registry-paths";
79
810 const requestHandler = createRequestHandler(
911 () => import("virtual:react-router/server-build"),
162164 /**
163165 * A registry request, answered by the packages service as it is: its
164166 * redirects (a large blob sent to storage) go back to the client, which
165− * follows them itself.
167+ * follows them itself. One that found nothing under a workspace's old name
168+ * or an alias staff set (`g1t` for `flagon-io`) is sent to the same path
169+ * under the workspace's name: only the not-found answer pays for the lookup.
166170 */
167171 async function proxyPackages(env: Env, request: Request): Promise<Response> {
168172 const started = Date.now();
169173 const answer = await env.PACKAGES.fetch(new Request(request, { redirect: "manual" }));
170− const response = new Response(answer.body, answer);
174+ const moved = answer.status === 404 ? await registryMoved(env, request) : null;
175+ const response = moved ?? new Response(answer.body, answer);
171176 response.headers.append("server-timing", `packages;dur=${Date.now() - started}`);
172177 return response;
173178 }
174179
180+/** Where a registry request under an alias or old name goes now, or null. */
181+async function registryMoved(env: Env, request: Request): Promise<Response | null> {
182+ const url = new URL(request.url);
183+ const named = registryWorkspace(url.pathname);
184+ if (!named || !isNamespaceShaped(named.slug)) return null;
185+ let current: string | null = null;
186+ try {
187+ current = await identityClient(env.IDENTITY).resolveSlug(named.slug);
188+ } catch {
189+ return null;
190+ }
191+ if (!current || current === named.slug) return null;
192+ const get = request.method === "GET" || request.method === "HEAD";
193+ // 308 keeps a publish a PUT, for the clients that follow it.
194+ return new Response(null, { status: get ? 301 : 308, headers: { location: named.under(current) + url.search } });
195+}
196+
175197 /**
176198 * An uploaded avatar. Its address is its hash, so it never changes and is
177199 * kept for good. It is served as nothing but an image: the stored type,
+9−1
2626 }
2727
2828 export function isValidNamespace(value: string): boolean {
29− return NAMESPACE.test(value) && !isReservedName(value);
29+ return isNamespaceShaped(value) && !isReservedName(value);
30+}
31+
32+/**
33+ * Whether `value` has a namespace's shape, reserved or not: what a
34+ * workspace's old name or an alias staff set (such as `g1t`) can be.
35+ */
36+export function isNamespaceShaped(value: string): boolean {
37+ return NAMESPACE.test(value);
3038 }
3139
3240 export function isValidRepoName(value: string): boolean {