create public build endpoint
All checks were successful
App Image CI / Build app image (pull_request) Successful in -1m26s
NPM Audit Check / Check NPM audit (pull_request) Successful in -2m14s

This commit is contained in:
2025-09-23 19:15:19 -05:00
parent b20bbf40fc
commit 77441fe7ed
5 changed files with 108 additions and 62 deletions

7
scripts/copy.js Normal file
View File

@@ -0,0 +1,7 @@
document.addEventListener('DOMContentLoaded', function () {
for (let btn of document.getElementsByClassName('copybtn')) {
btn.addEventListener('click', e => {
navigator.clipboard.writeText(e.target.previousElementSibling.innerText);
});
}
});

View File

@@ -25,6 +25,7 @@ interface Build {
status: Status; status: Status;
pid?: number; pid?: number;
sqid?: string; sqid?: string;
uuid: string;
} }
interface User { interface User {
@@ -105,6 +106,10 @@ class DB {
pid: { pid: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true
},
uuid: {
type: DataTypes.STRING,
unique: true
} }
}); });
@@ -182,13 +187,14 @@ class DB {
return user.id; return user.id;
} }
public async createBuild(repo: string, commit: string, patch: string, distro: string, dependencies: string, author: string): Promise<number> { public async createBuild(repo: string, commit: string, patch: string, distro: string, dependencies: string, author: string, uuid: string): Promise<number> {
const buildRec = await this.build.create({ const buildRec = await this.build.create({
repo, repo,
commit: commit || null, commit: commit || null,
patch: patch || null, patch: patch || null,
distro, distro,
dependencies, dependencies,
uuid,
userId: author || '-1' userId: author || '-1'
}); });
return buildRec.id; return buildRec.id;
@@ -241,6 +247,15 @@ class DB {
}); });
} }
public async getBuildByUuid(uuid: string): Promise<Build> {
return await this.build.findOne({
where: {
uuid
},
include: this.user
});
}
public async getBuilds(): Promise<Build[]> { public async getBuilds(): Promise<Build[]> {
return await this.build.findAll({ return await this.build.findAll({
attributes: SELECT, attributes: SELECT,

View File

@@ -92,7 +92,9 @@ class Web {
}); });
}); });
const showBuild = async (req: express.Request, res: express.Response, build: Build) => { const createBuildPages = (slug: string, getBuildFn: (str: string) => Promise<Build>) => {
app.get(`/${slug}/:id/`, async (req, res) => {
const build = await getBuildFn(req.params.id);
if (!build) { if (!build) {
res.sendStatus(404); res.sendStatus(404);
return; return;
@@ -100,6 +102,10 @@ class Web {
build.sqid = sqids.encode([build.id]); build.sqid = sqids.encode([build.id]);
const log = splitLines(await this.db.getLog(build.id)); const log = splitLines(await this.db.getLog(build.id));
if (req?.user) {
res.locals.shareable = `${req.protocol}://${req.host}/b/${build.uuid}/`;
}
res.render('build', { res.render('build', {
page: { page: {
title: 'Archery', title: 'Archery',
@@ -109,7 +115,47 @@ class Web {
user: req?.user, user: req?.user,
build, build,
log, log,
ended: build.status !== 'queued' && build.status !== 'running' ended: build.status !== 'queued' && build.status !== 'running',
public: !!oidc && !req?.user
});
});
app.get(`/${slug}/:id/logs{/}`, async (req, res) => {
const build = await getBuildFn(req.params.id);
if (!build) {
res.sendStatus(404);
return;
}
const log = (await this.db.getLog(build.id)).map(logChunk => logChunk.chunk).join('\n');
res.set('Content-Type', 'text/plain').send(log);
});
app.get(`/${slug}/:id/patch{/}`, async (req, res) => {
const build = await getBuildFn(req.params.id);
if (!build || !build.patch) {
res.sendStatus(404);
return;
}
res.set('Content-Type', 'text/plain').send(build.patch);
});
wsApp.ws(`/${slug}/:id/ws`, async (ws, req) => {
const build = await getBuildFn(req.params.id);
if (! build || (build.status !== 'queued' && build.status !== 'running')) {
return ws.close();
}
console.log('WS Opened');
const eventListener = (be: BuildEvent) => {
if (be.id === build.id) {
ws.send(JSON.stringify(be));
}
};
this.buildController.on('log', eventListener);
ws.on('close', () => {
console.log('WS Closed');
this.buildController.removeListener('log', eventListener);
});
}); });
} }
@@ -164,6 +210,7 @@ class Web {
res.redirect('/login'); res.redirect('/login');
}); });
}); });
createBuildPages('b', (id) => this.db.getBuildByUuid(id));
app.use((req, res, next) => { app.use((req, res, next) => {
if (!req?.user) { if (!req?.user) {
res.redirect('/login'); res.redirect('/login');
@@ -216,16 +263,14 @@ class Web {
req.body.patch || null, req.body.patch || null,
req.body.distro || 'arch', req.body.distro || 'arch',
req.body.dependencies || 'stable', req.body.dependencies || 'stable',
req?.user?.['id'] req?.user?.['id'],
crypto.randomUUID()
); );
res.redirect(`/build/${sqids.encode([buildId])}/`); res.redirect(`/build/${sqids.encode([buildId])}/`);
this.buildController.triggerBuild(); this.buildController.triggerBuild();
}); });
app.get('/build/:id/', async (req, res) => { createBuildPages('build', (id) => this.db.getBuild(sqids.decode(id)?.[0]));
const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]);
showBuild(req, res, build);
});
app.get('/build/:id/cancel', async (req, res) => { app.get('/build/:id/cancel', async (req, res) => {
const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]); const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]);
@@ -239,46 +284,12 @@ class Web {
catch (ex) { catch (ex) {
console.error(ex); console.error(ex);
} }
res.redirect(`/build/${req.params.id}`); res.redirect(`/build/${req.params.id}/`);
});
app.get('/build/:id/logs{/}', async (req, res) => {
const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]);
if (!build) {
res.sendStatus(404);
return;
}
const log = (await this.db.getLog(build.id)).map(logChunk => logChunk.chunk).join('\n');
res.set('Content-Type', 'text/plain').send(log);
});
app.get('/build/:id/patch{/}', async (req, res) => {
const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]);
if (!build || !build.patch) {
res.sendStatus(404);
return;
}
res.set('Content-Type', 'text/plain').send(build.patch);
}); });
app.get('/healthcheck', (_, res) => { app.get('/healthcheck', (_, res) => {
res.send('Healthy'); res.send('Healthy');
}); });
wsApp.ws('/build/:id/ws', (ws, req) => {
console.log('WS Opened');
const eventListener = (be: BuildEvent) => {
if (be.id === sqids.decode(req.params.id)?.[0]) {
ws.send(JSON.stringify(be));
}
};
this.buildController.on('log', eventListener);
ws.on('close', () => {
console.log('WS Closed');
this.buildController.removeListener('log', eventListener);
});
});
} }
close = () => { close = () => {

View File

@@ -118,6 +118,15 @@ input[type="submit"] {
} }
} }
span,
a {
&:has(+ button.copybtn) {
float: left;
margin-right: .75em;
}
}
.sidebar { .sidebar {
background: var(--primary); background: var(--primary);

View File

@@ -12,18 +12,21 @@
<h2 id="buildStatus"><%= build.status %></h2> <h2 id="buildStatus"><%= build.status %></h2>
<div class="overflow-x"> <div class="overflow-x">
<div class="grid-2col"> <div class="grid-2col">
<label>Repo</label> <span><%= build.repo %></span> <label>Repo</label> <span><span><%= build.repo %></span> <button class="copybtn">Copy</button></span>
<label>Commit</label> <span><% if (build.commit) { %><%= build.commit %><% } else { %>latest<% } %></span> <label>Commit</label> <span><% if (build.commit) { %><%= build.commit %><% } else { %>latest<% } %></span>
<label>Patch</label> <span><% if (build.patch) { %><a href="/build/<%= build.sqid %>/patch">patch file</a><% } else { %>none<% } %></span> <label>Patch</label> <span><% if (build.patch) { %><a href="/build/<%= build.sqid %>/patch">patch file</a><% } else { %>none<% } %></span>
<label>Distro</label> <span><%= build.distro %></span> <label>Distro</label> <span><%= build.distro %></span>
<label>Dependencies</label> <span><%= build.dependencies %></span> <label>Dependencies</label> <span><%= build.dependencies %></span>
<label>Start time</label> <span class="to-local-time"><%= build.startTime %></span> <label>Start time</label> <span class="to-local-time"><%= build.startTime %></span>
<% if (build.userId !== '-1') { %> <% if (build.userId && build.userId !== '-1') { %>
<label>Triggered by</label> <span><%= build.user.displayName %> (<%= build.user.username %>)</span> <label>Triggered by</label> <span><%= build.user.displayName %> (<%= build.user.username %>)</span>
<% } %> <% } %>
<% if (locals.shareable) { %>
<label>Shareable link</label> <span><a href="<%= shareable %>"><%= shareable %></a> <button class="copybtn">Copy</button></span>
<% } %>
</div> </div>
</div> </div>
<% if (build.sqid) { %> <% if (build.sqid && !public) { %>
<div> <div>
<a href="/build?id=<%= build.sqid %>" class="button">Clone</a> <a href="/build?id=<%= build.sqid %>" class="button">Clone</a>
</div> </div>
@@ -44,6 +47,7 @@
</div> </div>
<%- include("footer", locals) %> <%- include("footer", locals) %>
<script src="/assets/js/timezone.js?v1" nonce="<%= cspNonce %>"></script> <script src="/assets/js/timezone.js?v1" nonce="<%= cspNonce %>"></script>
<script src="/assets/js/copy.js?v1" nonce="<%= cspNonce %>"></script>
<% if (!ended) { %> <% if (!ended) { %>
<script src="/assets/js/build.js?v3" nonce="<%= cspNonce %>"></script> <script src="/assets/js/build.js?v3" nonce="<%= cspNonce %>"></script>
<% } %> <% } %>