mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-27 07:58:45 -07:00
ci: publish client builds to a downloads page under player-facing names
Each run now stages what it built as DorkNet-<release>-PC-Full.zip, -PC-Patch.zip, -Quest.xapk and -Quest-Installer.zip, writes their sizes and SHA-256 to a manifest, and uploads them with a static page to an S3 bucket served read-only (ci/downloads). The build scripts keep their own names, which the launcher depends on; only the published copies are renamed. PUBLISH_DOWNLOADS (on by default) turns the upload off per run. Jenkins archives the same renamed files instead of the raw build output.
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>DorkNet Downloads</title>
|
||||
<link rel="icon" href="dorknet.svg" type="image/svg+xml">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<!--
|
||||
The DorkNet downloads page. Published by ci/downloads/publish.py next to
|
||||
manifest.json, which lists the files; this page only renders it. Colours
|
||||
follow DorkNet.Server/site (tailwind.config.js).
|
||||
-->
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0b0e; --surface: #131418; --surface-2: #1b1d22; --border: #23252b; --border-2: #34363d;
|
||||
--text: #ececef; --muted: #a9abb4; --faint: #7e818d;
|
||||
--brand: #0b95c7; --brand-hi: #22cfee; --brand-soft: rgba(11, 149, 199, .14); --on-brand: #fff;
|
||||
--ok: #3ecf8e; --danger: #ef5b6b;
|
||||
color-scheme: dark;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme="dark"]) {
|
||||
--bg: #f7f7f8; --surface: #ffffff; --surface-2: #f7f7f8; --border: #ececef; --border-2: #d3d4d9;
|
||||
--text: #131418; --muted: #43464f; --faint: #5d606b;
|
||||
--brand: #087aa8; --brand-hi: #0b95c7; --brand-soft: rgba(11, 149, 199, .10);
|
||||
--ok: #1f9d68; --danger: #c93a4b;
|
||||
color-scheme: light;
|
||||
}
|
||||
}
|
||||
:root[data-theme="light"] {
|
||||
--bg: #f7f7f8; --surface: #ffffff; --surface-2: #f7f7f8; --border: #ececef; --border-2: #d3d4d9;
|
||||
--text: #131418; --muted: #43464f; --faint: #5d606b;
|
||||
--brand: #087aa8; --brand-hi: #0b95c7; --brand-soft: rgba(11, 149, 199, .10);
|
||||
--ok: #1f9d68; --danger: #c93a4b;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--text);
|
||||
font: 15px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--brand-hi); }
|
||||
code, .mono { font-family: "JetBrains Mono", SFMono-Regular, Menlo, monospace; font-size: .86em; }
|
||||
.wrap { max-width: 980px; margin: 0 auto; padding: 0 16px; }
|
||||
|
||||
header { border-bottom: 1px solid var(--border); background: var(--surface); }
|
||||
header .wrap { display: flex; align-items: center; gap: 12px; height: 60px; }
|
||||
header img { width: 30px; height: 30px; }
|
||||
header .name { font-weight: 700; letter-spacing: -.01em; }
|
||||
header .name span { color: var(--faint); font-weight: 500; }
|
||||
header .spacer { flex: 1; }
|
||||
header a.signout { color: var(--muted); font-size: 13px; text-decoration: none; }
|
||||
header a.signout:hover { color: var(--text); }
|
||||
|
||||
.intro { padding: 36px 0 8px; }
|
||||
h1 { font-size: 28px; line-height: 1.2; letter-spacing: -.02em; margin: 0 0 6px; }
|
||||
.lede { color: var(--muted); margin: 0; max-width: 60ch; }
|
||||
|
||||
.release { padding: 20px 0 8px; }
|
||||
.release-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin-bottom: 14px; }
|
||||
.release-head h2 { font-size: 20px; margin: 0; letter-spacing: -.01em; }
|
||||
.release-head .sub { color: var(--faint); font-size: 13px; }
|
||||
|
||||
.platform { margin: 0 0 22px; }
|
||||
.platform h3 {
|
||||
display: flex; align-items: center; gap: 8px; font-size: 13px; text-transform: uppercase;
|
||||
letter-spacing: .06em; color: var(--muted); margin: 0 0 10px; font-weight: 600;
|
||||
}
|
||||
.platform h3 svg { width: 16px; height: 16px; }
|
||||
.platform .hint { color: var(--faint); font-size: 13px; margin: -4px 0 10px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 12px; }
|
||||
|
||||
.card {
|
||||
display: flex; flex-direction: column; gap: 10px; padding: 18px;
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||
}
|
||||
.card.primary { border-color: color-mix(in srgb, var(--brand) 55%, var(--border)); box-shadow: 0 0 0 3px var(--brand-soft); }
|
||||
.card .top { display: flex; align-items: center; gap: 8px; }
|
||||
.card h4 { margin: 0; font-size: 16px; }
|
||||
.tag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; background: var(--brand-soft); color: var(--brand-hi); }
|
||||
.card p { margin: 0; color: var(--muted); font-size: 14px; }
|
||||
.card .file { color: var(--faint); word-break: break-all; }
|
||||
.meta { display: flex; flex-wrap: wrap; gap: 4px 14px; color: var(--faint); font-size: 12.5px; }
|
||||
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: auto; padding-top: 4px; }
|
||||
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 8px; height: 38px; padding: 0 14px; border-radius: 8px;
|
||||
font: 600 14px/1 inherit; text-decoration: none; cursor: pointer; border: 1px solid transparent;
|
||||
}
|
||||
.btn svg { width: 16px; height: 16px; }
|
||||
.btn-primary { background: var(--brand); color: var(--on-brand); }
|
||||
.btn-primary:hover { background: var(--brand-hi); }
|
||||
.btn-secondary { background: var(--surface-2); color: var(--text); border-color: var(--border-2); }
|
||||
.btn-secondary:hover { border-color: var(--faint); }
|
||||
.btn:focus-visible { outline: 2px solid var(--brand-hi); outline-offset: 2px; }
|
||||
.hash {
|
||||
display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 8px; border-radius: 6px;
|
||||
background: transparent; border: 1px dashed var(--border-2); color: var(--faint); cursor: pointer; font: inherit; font-size: 12px;
|
||||
}
|
||||
.hash:hover { color: var(--text); }
|
||||
.hash.copied { color: var(--ok); border-color: var(--ok); }
|
||||
|
||||
.note { color: var(--faint); font-size: 13px; padding: 8px 0 40px; border-top: 1px solid var(--border); margin-top: 12px; }
|
||||
.note p { margin: 12px 0 0; }
|
||||
.state { padding: 48px 0; color: var(--muted); }
|
||||
.state.error { color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="wrap">
|
||||
<img src="dorknet.svg" alt="">
|
||||
<div class="name">DorkNet <span>Downloads</span></div>
|
||||
<div class="spacer"></div>
|
||||
<a class="signout" href="/cdn-cgi/access/logout">Sign out</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="intro">
|
||||
<h1>Get Rec Room for DorkNet</h1>
|
||||
<p class="lede">Pick your platform. Every file is built from the DorkNet source by our build server; the
|
||||
SHA-256 next to each one lets you check your download.</p>
|
||||
</section>
|
||||
<div id="releases"><p class="state">Loading builds…</p></div>
|
||||
<section class="note">
|
||||
<p>These builds contain Rec Room's own game files. They're for the DorkNet community only: please don't
|
||||
re-share them.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const ICONS = {
|
||||
pc: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
||||
quest: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3h-3l-2-2.5h-2L9 17H6a3 3 0 0 1-3-3z"/></svg>',
|
||||
download: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12M7 10l5 5 5-5M5 21h14"/></svg>',
|
||||
copy: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>',
|
||||
};
|
||||
const PLATFORMS = [
|
||||
{ id: 'pc', title: 'PC', hint: 'Windows, with or without a VR headset.' },
|
||||
{ id: 'quest', title: 'Meta Quest', hint: 'Turn on developer mode for your headset and allow USB debugging when asked.' },
|
||||
];
|
||||
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
||||
const size = (b) => b >= 2 ** 30 ? `${(b / 2 ** 30).toFixed(2)} GB`
|
||||
: b >= 2 ** 20 ? `${(b / 2 ** 20).toFixed(1)} MB` : `${Math.max(1, Math.round(b / 2 ** 10))} KB`;
|
||||
const date = (iso) => iso ? new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : '';
|
||||
|
||||
// What each file is for, in players' terms. Keyed by the file ids publish.py writes.
|
||||
function describe(f, rel, site) {
|
||||
const questPage = site ? `<a href="${esc(site)}/quest" target="_blank" rel="noopener">Quest install page</a>` : 'Quest install page of the DorkNet website';
|
||||
switch (f.id) {
|
||||
case 'pc-full': return {
|
||||
title: 'Full game', tag: 'Start here', primary: true,
|
||||
body: `Rec Room ${esc(rel.title)} with DorkNet already set up. Unzip it anywhere and run
|
||||
${rel.pc_exe ? `<code>${esc(rel.pc_exe)}</code>` : 'the game'}.`,
|
||||
};
|
||||
case 'pc-patch': return {
|
||||
title: 'Patch only',
|
||||
body: `Just the DorkNet part (MelonLoader and the DorkNet mod), for a copy of the game you already have.
|
||||
The DorkNet launcher installs this for you. Skip it if you got the full game.`,
|
||||
};
|
||||
case 'quest': return {
|
||||
title: 'Quest app', tag: 'Start here', primary: true,
|
||||
body: `Plug in your headset and install from the ${questPage} (Chrome or Edge), or drop the file onto SideQuest.`,
|
||||
};
|
||||
case 'quest-installer': return {
|
||||
title: 'Install scripts',
|
||||
body: `Prefer the command line? Unzip these next to the Quest app and run <code>install.cmd</code> (Windows)
|
||||
or <code>install.sh</code> (macOS / Linux). Needs adb.`,
|
||||
};
|
||||
default: return { title: f.name, body: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function card(f, rel, site) {
|
||||
const d = describe(f, rel, site);
|
||||
const build = f.build ? `<span>Build ${esc(f.build)}</span>` : '';
|
||||
return `
|
||||
<article class="card${d.primary ? ' primary' : ''}">
|
||||
<div class="top"><h4>${esc(d.title)}</h4>${d.tag ? `<span class="tag">${esc(d.tag)}</span>` : ''}</div>
|
||||
<p>${d.body}</p>
|
||||
<div class="meta"><span class="mono file">${esc(f.name)}</span></div>
|
||||
<div class="meta"><span>${size(f.size)}</span><span>Updated ${esc(date(f.built_at))}</span>${build}</div>
|
||||
<div class="actions">
|
||||
<a class="btn ${d.primary ? 'btn-primary' : 'btn-secondary'}" href="${esc(f.path)}" download>${ICONS.download}Download</a>
|
||||
<button class="hash mono" type="button" data-sha="${esc(f.sha256)}" title="Copy SHA-256: ${esc(f.sha256)}">${ICONS.copy}SHA-256 ${esc(f.sha256.slice(0, 12))}…</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function render(m) {
|
||||
const root = document.getElementById('releases');
|
||||
const releases = (m.releases || []).filter((r) => (r.files || []).length);
|
||||
if (!releases.length) { root.innerHTML = '<p class="state">No builds have been published yet.</p>'; return; }
|
||||
root.innerHTML = releases.map((rel) => {
|
||||
const sections = PLATFORMS.map((p) => {
|
||||
const files = rel.files.filter((f) => f.platform === p.id);
|
||||
if (!files.length) return '';
|
||||
return `
|
||||
<div class="platform">
|
||||
<h3>${ICONS[p.id]}${esc(p.title)}</h3>
|
||||
<p class="hint">${esc(p.hint)}</p>
|
||||
<div class="grid">${files.map((f) => card(f, rel, m.site_url)).join('')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
const game = rel.game_version ? `Game build of ${esc(date(rel.game_version + 'T12:00:00Z'))}` : '';
|
||||
return `
|
||||
<section class="release">
|
||||
<div class="release-head"><h2>Rec Room ${esc(rel.title)}</h2><span class="sub">${game}</span></div>
|
||||
${sections}
|
||||
</section>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
document.addEventListener('click', async (e) => {
|
||||
const b = e.target.closest('.hash');
|
||||
if (!b) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(b.dataset.sha);
|
||||
b.classList.add('copied');
|
||||
setTimeout(() => b.classList.remove('copied'), 1500);
|
||||
} catch { /* clipboard blocked: the full hash is in the tooltip */ }
|
||||
});
|
||||
|
||||
fetch('manifest.json', { cache: 'no-store' })
|
||||
.then((r) => { if (r.status === 404) return {}; if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||||
.then(render)
|
||||
.catch((err) => {
|
||||
document.getElementById('releases').innerHTML =
|
||||
`<p class="state error">Couldn't load the list of builds (${esc(err.message)}). Try reloading the page.</p>`;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user