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;
pid?: number;
sqid?: string;
uuid: string;
}
interface User {
@@ -105,6 +106,10 @@ class DB {
pid: {
type: DataTypes.INTEGER,
allowNull: true
},
uuid: {
type: DataTypes.STRING,
unique: true
}
});
@@ -182,13 +187,14 @@ class DB {
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({
repo,
commit: commit || null,
patch: patch || null,
distro,
dependencies,
uuid,
userId: author || '-1'
});
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[]> {
return await this.build.findAll({
attributes: SELECT,

View File

@@ -92,24 +92,70 @@ class Web {
});
});
const showBuild = async (req: express.Request, res: express.Response, build: Build) => {
if (!build) {
res.sendStatus(404);
return;
}
build.sqid = sqids.encode([build.id]);
const log = splitLines(await this.db.getLog(build.id));
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) {
res.sendStatus(404);
return;
}
build.sqid = sqids.encode([build.id]);
const log = splitLines(await this.db.getLog(build.id));
res.render('build', {
page: {
title: 'Archery',
titlesuffix: `Build #${build.id}`,
description: `Building ${build.repo} on ${build.distro}`,
},
user: req?.user,
build,
log,
ended: build.status !== 'queued' && build.status !== 'running'
if (req?.user) {
res.locals.shareable = `${req.protocol}://${req.host}/b/${build.uuid}/`;
}
res.render('build', {
page: {
title: 'Archery',
titlesuffix: `Build #${build.id}`,
description: `Building ${build.repo} on ${build.distro}`,
},
user: req?.user,
build,
log,
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);
});
});
}
@@ -138,7 +184,7 @@ class Web {
app.use(passport.initialize());
app.use(passport.session());
app.get('/login', (req, res) => {
if(req?.user) {
if (req?.user) {
return res.redirect('/');
}
res.append('X-Robots-Tag', 'none');
@@ -164,6 +210,7 @@ class Web {
res.redirect('/login');
});
});
createBuildPages('b', (id) => this.db.getBuildByUuid(id));
app.use((req, res, next) => {
if (!req?.user) {
res.redirect('/login');
@@ -216,16 +263,14 @@ class Web {
req.body.patch || null,
req.body.distro || 'arch',
req.body.dependencies || 'stable',
req?.user?.['id']
req?.user?.['id'],
crypto.randomUUID()
);
res.redirect(`/build/${sqids.encode([buildId])}/`);
this.buildController.triggerBuild();
});
app.get('/build/:id/', async (req, res) => {
const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]);
showBuild(req, res, build);
});
createBuildPages('build', (id) => this.db.getBuild(sqids.decode(id)?.[0]));
app.get('/build/:id/cancel', async (req, res) => {
const build = await this.db.getBuild(sqids.decode(req.params.id)?.[0]);
@@ -239,46 +284,12 @@ class Web {
catch (ex) {
console.error(ex);
}
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);
res.redirect(`/build/${req.params.id}/`);
});
app.get('/healthcheck', (_, res) => {
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 = () => {

View File

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

View File

@@ -12,18 +12,21 @@
<h2 id="buildStatus"><%= build.status %></h2>
<div class="overflow-x">
<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>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>Dependencies</label> <span><%= build.dependencies %></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>
<% } %>
<% if (locals.shareable) { %>
<label>Shareable link</label> <span><a href="<%= shareable %>"><%= shareable %></a> <button class="copybtn">Copy</button></span>
<% } %>
</div>
</div>
<% if (build.sqid) { %>
<% if (build.sqid && !public) { %>
<div>
<a href="/build?id=<%= build.sqid %>" class="button">Clone</a>
</div>
@@ -44,6 +47,7 @@
</div>
<%- include("footer", locals) %>
<script src="/assets/js/timezone.js?v1" nonce="<%= cspNonce %>"></script>
<script src="/assets/js/copy.js?v1" nonce="<%= cspNonce %>"></script>
<% if (!ended) { %>
<script src="/assets/js/build.js?v3" nonce="<%= cspNonce %>"></script>
<% } %>