feat(release): stage GitWorkshop and next node updates

This commit is contained in:
archipelago
2026-09-09 18:15:21 -04:00
parent 973356df16
commit f5c0ba85cd
97 changed files with 5716 additions and 1327 deletions
+36
View File
@@ -0,0 +1,36 @@
FROM docker.io/library/node:24-alpine AS build
# GitWorkshop has no release artifacts, so pin the audited source revision.
# The fetch verifies that the exact requested commit was checked out before any
# dependency or build command runs.
ARG GITWORKSHOP_COMMIT=dc36db64f6a2cca29d109829eabaf0a49d4bf4da
RUN apk add --no-cache git
WORKDIR /src
RUN git init \
&& git remote add origin https://github.com/DanConwayDev/gitworkshop.git \
&& git fetch --depth=1 origin "${GITWORKSHOP_COMMIT}" \
&& git checkout --detach FETCH_HEAD \
&& test "$(git rev-parse HEAD)" = "${GITWORKSHOP_COMMIT}"
COPY gitworkshop-archipelago.patch /tmp/gitworkshop-archipelago.patch
RUN git apply --check /tmp/gitworkshop-archipelago.patch \
&& git apply /tmp/gitworkshop-archipelago.patch
RUN npm ci
RUN APP_BASE_PATH=/app/archipelago-source/ \
APP_RELEASE_VERSION="archipelago-${GITWORKSHOP_COMMIT}" \
npm run build
FROM docker.io/library/nginx:1.27.4-alpine
COPY --from=build /src/dist/ /usr/share/nginx/html/
COPY nginx-main.conf /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY UPSTREAM.md /usr/share/doc/archipelago-source/UPSTREAM.md
# Run both nginx master and workers as the packaged unprivileged user. Writable
# runtime paths live on the manifest's small mode-1777 `/tmp` tmpfs, so the
# container needs neither Linux capabilities nor a writable root filesystem.
EXPOSE 8337
ENTRYPOINT []
USER nginx
CMD ["nginx", "-g", "daemon off;"]
+23
View File
@@ -0,0 +1,23 @@
# GitWorkshop upstream
This image packages the GitWorkshop NIP-34 web client from:
- Source: https://github.com/DanConwayDev/gitworkshop
- Pinned commit: `dc36db64f6a2cca29d109829eabaf0a49d4bf4da`
- Upstream project: https://gitworkshop.dev/
- App icon: `public/icons/icon.svg` from the same pinned revision (the artwork
is only inset onto Archipelago's standard icon safe area).
The small build patch only makes the upstream Vite/React application work
below Archipelago's `/app/archipelago-source/` mount, injects the existing
consent-gated Archipelago NIP-07 provider, disables the development-only
`localhost:4869` cache-relay probe, and removes two unreachable lookup relays
from the defaults. It does not replace GitWorkshop's NIP-34, GRASP, repository
browser, issue, pull-request, or review interfaces.
The pinned revision and current upstream `main` do not contain a license file,
the package metadata declares no license, and GitHub reports no detected
license. This development image is for local evaluation only. Do not publish
it to the production app registry until upstream adds an explicit license (the
preferred auditable resolution) or its maintainer provides a written grant
covering compilation, modification, and redistribution.
@@ -0,0 +1,418 @@
diff --git a/index.html b/index.html
index 6894507..a917f5d 100644
--- a/index.html
+++ b/index.html
@@ -14,7 +14,7 @@
property="og:description"
content="Decentralized GitHub alternative over Nostr"
/>
- <meta property="og:image" content="/og-image.png" />
+ <meta property="og:image" content="%BASE_URL%og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
@@ -22,15 +22,19 @@
content="%APP_NAME% — git collaboration without the platform"
/>
<meta name="twitter:card" content="summary_large_image" />
- <meta name="twitter:image" content="/og-image.png" />
+ <meta name="twitter:image" content="%BASE_URL%og-image.png" />
<meta
http-equiv="content-security-policy"
- content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-src 'self' https:; font-src 'self'; base-uri 'self'; manifest-src 'self'; connect-src 'self' blob: https: wss:; img-src 'self' data: blob: https:; media-src 'self' https:"
+ content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; frame-src 'self' http: https:; font-src 'self'; base-uri 'self'; manifest-src 'self'; connect-src 'self' blob: https: wss:; img-src 'self' data: blob: https:; media-src 'self' https:"
/>
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
- <link rel="icon" type="image/png" href="/favicon.png" />
- <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
- <link rel="manifest" href="/manifest.webmanifest" />
+ <link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" />
+ <link rel="icon" type="image/png" href="%BASE_URL%favicon.png" />
+ <link rel="apple-touch-icon" href="%BASE_URL%icons/apple-touch-icon.png" />
+ <link
+ rel="manifest"
+ href="/manifest.webmanifest"
+ crossorigin="use-credentials"
+ />
<style>
body {
margin: 0;
@@ -41,7 +45,9 @@
background: #16171e;
}
</style>
- <script src="/theme-init.js"></script>
+ <script src="%BASE_URL%theme-init.js"></script>
+ <script src="%BASE_URL%archipelago-nostrdb-config.js"></script>
+ <script data-no-nip98 src="/nostr-provider.js?v=tab-signer-v4"></script>
</head>
<body>
<div id="root">
@@ -119,7 +125,7 @@
<div id="splash-content">
<img
class="splash-logo"
- src="/icons/icon-192x192.png"
+ src="%BASE_URL%icons/icon-192x192.png"
width="64"
height="64"
alt=""
diff --git a/public/archipelago-nostrdb-config.js b/public/archipelago-nostrdb-config.js
new file mode 100644
index 0000000..8f598c4
--- /dev/null
+++ b/public/archipelago-nostrdb-config.js
@@ -0,0 +1,7 @@
+// window.nostrdb.js probes a developer-only relay at localhost:4869 unless
+// configured before the app module graph loads. On an installed node that
+// address means the user's own device, can never be the app's cache relay,
+// and is correctly blocked by GitWorkshop's production CSP.
+window.nostrdbConfig = Object.assign({}, window.nostrdbConfig || {}, {
+ localRelays: [],
+});
diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
index 0f681e7..16dff02 100644
--- a/src/AppRouter.tsx
+++ b/src/AppRouter.tsx
@@ -20,6 +20,8 @@ import { MaintainerAcceptanceMonitor } from "./components/MaintainerAcceptanceMo
import { useRepoPath } from "./hooks/useRepoPath";
import { REPO_KIND } from "./lib/nip34";
import { getGitWorkshopPath } from "./lib/gitworkshopUrl";
+import { useLoginActions } from "./hooks/useLoginActions";
+import { accounts } from "./services/accounts";
/**
* Handles public GitWorkshop links in native builds. This stays inside the
@@ -365,9 +367,46 @@ function LegacyRedirect() {
return <RepoLayout />;
}
+/** Turn an eager node identity choice into GitWorkshop's extension login. */
+function ArchipelagoIdentityLogin() {
+ const login = useLoginActions();
+ const loginRef = useRef(login.extension);
+ const loginRunning = useRef(false);
+ loginRef.current = login.extension;
+
+ useEffect(() => {
+ const bridge = (
+ window as Window & {
+ archipelagoNostr?: {
+ onIdentitySelected?: (
+ callback: (identity: { nostr_pubkey: string }) => void,
+ ) => () => void;
+ };
+ }
+ ).archipelagoNostr;
+ if (!bridge?.onIdentitySelected) return;
+
+ return bridge.onIdentitySelected(() => {
+ if (accounts.getActive() || loginRunning.current) return;
+ loginRunning.current = true;
+ void loginRef
+ .current()
+ .catch((error) => {
+ console.error("Archipelago automatic login failed:", error);
+ })
+ .finally(() => {
+ loginRunning.current = false;
+ });
+ });
+ }, []);
+
+ return null;
+}
+
function AppRouter() {
return (
- <BrowserRouter>
+ <BrowserRouter basename={import.meta.env.BASE_URL}>
+ <ArchipelagoIdentityLogin />
<NativeGitWorkshopLinks />
<NativeAndroidBackButton />
<ScrollToTop />
diff --git a/src/components/AppFooter.tsx b/src/components/AppFooter.tsx
index 3eb76d2..22b079b 100644
--- a/src/components/AppFooter.tsx
+++ b/src/components/AppFooter.tsx
@@ -62,7 +62,7 @@ export function AppFooter() {
className="flex items-center gap-2 hover:opacity-80 transition-opacity w-fit"
>
<img
- src="/icons/icon.svg"
+ src={`${import.meta.env.BASE_URL}icons/icon.svg`}
alt="GitWorkshop"
className="h-6 w-6"
/>
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx
index b31aa79..cec9d39 100644
--- a/src/components/AppHeader.tsx
+++ b/src/components/AppHeader.tsx
@@ -155,7 +155,11 @@ export function AppHeader() {
to="/"
className="group transition-opacity hover:opacity-80 shrink-0"
>
- <img src="/icons/icon.svg" alt="GitWorkshop" className="h-8 w-8" />
+ <img
+ src={`${import.meta.env.BASE_URL}icons/icon.svg`}
+ alt="GitWorkshop"
+ className="h-8 w-8"
+ />
</Link>
<div className="flex items-center gap-2 ml-auto">
diff --git a/src/main.tsx b/src/main.tsx
index 1e4fead..ae3a045 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -10,9 +10,11 @@ import "@fontsource-variable/inter";
// itself, so subsequent loads are fully uncontrolled. Capacitor packages local
// assets and does not use this web-deployment cleanup worker.
if (!Capacitor.isNativePlatform() && "serviceWorker" in navigator) {
- navigator.serviceWorker.register("/sw.js").catch(() => {
- /* ignore — browser may block in certain envs */
- });
+ navigator.serviceWorker
+ .register(`${import.meta.env.BASE_URL}sw.js`)
+ .catch(() => {
+ /* ignore — browser may block in certain envs */
+ });
}
createRoot(document.getElementById("root")!).render(
diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx
index 18e3593..685c422 100644
--- a/src/pages/NotFound.tsx
+++ b/src/pages/NotFound.tsx
@@ -28,7 +28,7 @@ const NotFound = () => {
Oops! Page not found
</p>
<a
- href="/"
+ href={import.meta.env.BASE_URL}
className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline"
>
Return to Home
diff --git a/src/services/settings.ts b/src/services/settings.ts
index 8f9a1a7..5438a72 100644
--- a/src/services/settings.ts
+++ b/src/services/settings.ts
@@ -124,8 +124,6 @@ export const fallbackRelaysCustomised$ = isCustomised$(
* These are used by the event loaders to find events more efficiently.
*/
export const DEFAULT_LOOKUP_RELAYS = normalizeRelayList([
- "wss://purplepag.es",
- "wss://index.hzrd149.com",
"wss://indexer.coracle.social",
]);
diff --git a/vite.config.ts b/vite.config.ts
index 0534fb9..d94fc70 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -39,37 +39,38 @@ function htmlAppNamePlugin(): Plugin {
*/
function manifestPlugin(): Plugin {
const virtualId = "/manifest.webmanifest";
+ const appBase = process.env.APP_BASE_PATH ?? "/";
const manifest = JSON.stringify(
{
name: "GitWorkshop.dev",
short_name: "GitWorkshop",
description: "Decentralized GitHub alternative over Nostr",
- start_url: "/",
+ start_url: appBase,
display: "standalone",
background_color: "#16171e",
theme_color: "#16171e",
categories: ["development", "productivity", "utilities"],
icons: [
{
- src: "/icons/icon-192x192.png",
+ src: `${appBase}icons/icon-192x192.png`,
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
- src: "/icons/icon-512x512.png",
+ src: `${appBase}icons/icon-512x512.png`,
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
- src: "/icons/pwa-maskable-192x192.png",
+ src: `${appBase}icons/pwa-maskable-192x192.png`,
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
- src: "/icons/pwa-maskable-512x512.png",
+ src: `${appBase}icons/pwa-maskable-512x512.png`,
sizes: "512x512",
type: "image/png",
purpose: "maskable",
@@ -82,6 +83,12 @@ function manifestPlugin(): Plugin {
return {
name: "manifest",
+ transformIndexHtml(html) {
+ return html.replace(
+ 'href="/manifest.webmanifest"',
+ `href="${appBase}manifest.webmanifest"`,
+ );
+ },
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === virtualId) {
@@ -104,6 +111,10 @@ function manifestPlugin(): Plugin {
// https://vitejs.dev/config/
export default defineConfig(() => ({
+ // Archipelago serves GitWorkshop behind the dashboard origin. Vite's base
+ // controls emitted asset URLs while BrowserRouter consumes the same value
+ // below, so repository routes remain valid below that mount point.
+ base: process.env.APP_BASE_PATH ?? "/",
define: {
__APP_NAME__: JSON.stringify(name),
__APP_RELEASE_VERSION__: JSON.stringify(
diff --git a/src/components/auth/AccountSwitcher.tsx b/src/components/auth/AccountSwitcher.tsx
index f59a7d2..3168910 100644
--- a/src/components/auth/AccountSwitcher.tsx
+++ b/src/components/auth/AccountSwitcher.tsx
@@ -46,7 +46,7 @@ function SignerTypeBadge({ account }: { account: IAccount }) {
return (
<span className="flex items-center gap-1 text-[10px] text-muted-foreground">
<Puzzle className="w-3 h-3" />
- Extension
+ Extension / Archipelago
</span>
);
if (account instanceof NostrConnectAccount)
diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx
index 11f6716..0bba6a2 100644
--- a/src/components/auth/LoginDialog.tsx
+++ b/src/components/auth/LoginDialog.tsx
@@ -239,9 +239,15 @@ const LoginDialog: React.FC<LoginDialogProps> = ({
try {
if (!("nostr" in window)) {
throw new Error(
- "Nostr extension not found. Please install a NIP-07 extension.",
+ "No NIP-07 signer found. Open GitWorkshop through Archipelago or install a browser extension.",
);
}
+ const archipelago = (
+ window as Window & {
+ archipelagoNostr?: { selectIdentity?: () => Promise<unknown> };
+ }
+ ).archipelagoNostr;
+ if (archipelago?.selectIdentity) await archipelago.selectIdentity();
await login.extension();
onLogin();
onClose();
@@ -437,7 +443,9 @@ const LoginDialog: React.FC<LoginDialogProps> = ({
disabled={isLoading}
>
<Puzzle className="w-4 h-4" />
- {isLoading ? "Logging in..." : "Log in with Extension"}
+ {isLoading
+ ? "Logging in..."
+ : "Log in with Extension / Archipelago"}
</Button>
)}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index b8de377..7f72f31 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -23,6 +23,7 @@ import {
ChevronUp,
Pin,
Search,
+ Globe2,
} from "lucide-react";
import { CreateRepoDialog } from "@/components/CreateRepoDialog";
import { Button } from "@/components/ui/button";
@@ -36,6 +37,7 @@ import { useUserActivity } from "@/hooks/useUserActivity";
import { useUserRepositories } from "@/hooks/useUserRepositories";
import { useUserFollowedRepos } from "@/hooks/useUserFollowedRepos";
import { useUserPinnedCoords } from "@/hooks/useUserPinnedRepos";
+import { useRepositorySearch } from "@/hooks/useRepositorySearch";
import { useNotifications } from "@/hooks/useNotifications";
import { useUserProfileSubscription } from "@/hooks/useUserProfileSubscription";
import { useUserPath } from "@/hooks/useUserPath";
@@ -409,6 +411,63 @@ function FollowedReposPanel({ pubkey }: { pubkey: string }) {
);
}
+// ---------------------------------------------------------------------------
+// Recent repositories from the wider Nostr network
+// ---------------------------------------------------------------------------
+
+function NetworkRepositoriesPanel() {
+ const { repos, isLoading } = useRepositorySearch("");
+ const recent = repos?.slice(0, 6);
+
+ return (
+ <div className="h-fit">
+ <div className="pb-3 flex items-center justify-between gap-3">
+ <h3 className="text-base font-semibold flex items-center gap-2">
+ <Globe2 className="h-4 w-4 text-muted-foreground" />
+ Nostr network
+ </h3>
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
+ asChild
+ >
+ <Link to="/search">
+ Browse all
+ <ArrowRight className="h-3 w-3 ml-1" />
+ </Link>
+ </Button>
+ </div>
+
+ {recent === undefined || (isLoading && recent.length === 0) ? (
+ <div className="space-y-1">
+ {Array.from({ length: 5 }).map((_, i) => (
+ <RepoRowSkeleton key={i} />
+ ))}
+ </div>
+ ) : recent.length > 0 ? (
+ <div className="space-y-0.5">
+ {recent.map((repo) => (
+ <RepoListItem
+ key={`${repo.selectedMaintainer}:${repo.dTag}`}
+ repo={repo}
+ />
+ ))}
+ </div>
+ ) : (
+ <div className="py-6 text-center">
+ <p className="text-sm text-muted-foreground">
+ No network repositories available
+ </p>
+ <p className="text-xs text-muted-foreground/60 mt-1">
+ Check the git index relay in Settings
+ </p>
+ </div>
+ )}
+ </div>
+ );
+}
+
// ---------------------------------------------------------------------------
// Embedded notifications panel (compact, inbox only, max 5)
// ---------------------------------------------------------------------------
@@ -591,6 +650,8 @@ export function Dashboard() {
<MyRepositoriesPanel pubkey={pubkey} />
<Separator className="opacity-40" />
<FollowedReposPanel pubkey={pubkey} />
+ <Separator className="opacity-40" />
+ <NetworkRepositoriesPanel />
</div>
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr notice;
events {
worker_connections 256;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
keepalive_timeout 65;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/conf.d/*.conf;
}
+42
View File
@@ -0,0 +1,42 @@
server {
# Host networking is required for the loopback-only Archipelago RPC.
# Keep nginx itself on loopback so the authenticated app gate owns every
# externally reachable listener.
listen 127.0.0.1:8337;
server_name _;
root /usr/share/nginx/html;
index index.html;
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location = /manifest.webmanifest {
default_type application/manifest+json;
try_files $uri =404;
}
location = /app/archipelago-source/manifest.webmanifest {
default_type application/manifest+json;
rewrite ^/app/archipelago-source/(.*)$ /$1 break;
try_files $uri =404;
}
# The normal dashboard proxy strips this prefix before forwarding, while
# direct app-gate access preserves it. Supporting both keeps health/debug
# access useful without making launch depend on any particular interface.
location ^~ /app/archipelago-source/ {
rewrite ^/app/archipelago-source/(.*)$ /$1 break;
try_files $uri $uri/ /index.html;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
location / {
try_files $uri $uri/ /index.html;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
}