ReleaseBell: option to suppress prerelease notifications
-
Read against
eb9d805. Last of three findings, and the one I would most like as a user.The
prereleasecolumn has been there since20230926130714-add-release-prerelease.js. It is fetched (github.js:130), stored (database.js:174), and labelled in both the subject line and the email template. But nothing filters on it — there is no user setting and no condition inreleasesListAllPending.A live example. The three newest tags on
dbgate/dbgatetoday:v7.2.4-premium-beta.7 v7.2.4-premium-beta.5 v7.2.4-beta.6while
GET /repos/dbgate/dbgate/releases/lateststill returns v7.2.3. So starring that repository currently sends three emails announcing versions that are not released, two of them for a premium build that is not the open-source product. Any project running a beta channel behaves the same way, so this is the normal case rather than an edge one.A sketch, defaulting to on so nothing changes for existing users:
ALTER TABLE users ADD COLUMN notifyPrerelease BOOLEAN DEFAULT 1;Then in
releasesListAllPending, join through to users — the flag is per-user while the pending list is release-scoped:SELECT releases.* FROM releases JOIN projects ON projects.id = releases.projectId JOIN users ON users.id = projects.userId WHERE releases.notified = 0 AND (users.notifyPrerelease = 1 OR releases.prerelease = 0)Plus one checkbox beside the existing email setting.
A per-project override would be better long term, since people tend to want stable-only for tools they merely use and prereleases for the one thing they develop against. But the per-user flag covers most of the value and is a smaller change.
Happy to send this as a patch if that is useful.
-
A better thing to look into would be https://docs.renovatebot.com/. I think i have seen renovate config files in the cloudron app repositories as well in the past. With renovate you can get automatic merge request for the changed dependencies.
A working example can be found in this repo as well https://git.9wd.eu/apps/cloudron-cypht
-
J joseph moved this topic from Apps
-
I think this is a good idea. @loudlemur feel free to post a patch and I can merge it.
I think this is a good idea. @loudlemur feel free to post a patch and I can merge it.
Thanks. Patch below, against
1.12.0(eb9d805).It does the per-user flag only, as described above. A new
notifyPrereleasecolumn onusersdefaults to1, so existing installs keep their current behaviour after migrating and nobody has to go and re-enable anything.releasesListAllPendingthen joins throughprojectstousersand drops prereleases only for users who have opted out. The setting is a checkbox in the existing Settings dialog, andCheckboxwas already registered inmain.js, so this adds no new dependency.Two things I checked rather than assumed, since the query is the part that can silently do the wrong thing.
The migration is safe on an existing database. I ran it against MariaDB 11 with users created before the column existed, and they all came out with
notifyPrerelease=1, so an upgrade is a no-op until someone unticks the box.downdrops the column cleanly with no row loss.The filter selects what it should. With two users, one opted in and one opted out, each starring a project with one stable release and one prerelease pending, the new query returns the opted-in user's stable release and prerelease, and only the stable release for the opted-out user. An already-notified release stays excluded as before.
One behaviour worth flagging because it is a design choice and not an accident. A suppressed prerelease keeps
notified=FALSEforever, so if a user later ticks the box back on, the prereleases published while it was off will be delivered at that point. Leaving them pending is what makes the setting reversible without losing anything, but if you would rather they were marked notified and silently dropped, that is a one-line change and I am happy to redo it either way.I also corrected a small inconsistency in passing.
migrations/current_schema.sqldeclares the releases column asprereleaseswhile the migration20230926130714and every code path useprerelease. The file is marked "Only for reference" so nothing was broken by it, but it is exactly the file someone reads before writing a patch like this one, so it seemed worth fixing while I was there. Say the word if you would prefer that split into its own commit.diff --git a/backend/database.js b/backend/database.js index 57e6918..8960c5c 100644 --- a/backend/database.js +++ b/backend/database.js @@ -123,16 +123,23 @@ async function projectsRemove(projectId) { await db.query('DELETE FROM projects WHERE id=?', [ projectId ]); } +function userPostprocess(u) { + u.notifyPrerelease = !!u.notifyPrerelease; + return u; +} + async function usersList() { const [result] = await db.query('SELECT * FROM users', []); - return result; + return result.map(userPostprocess); } async function usersAdd(user) { assert.strictEqual(typeof user, 'object'); - await db.query('INSERT INTO users (id, email, githubToken) VALUES (?, ?, ?)', - [ user.id, user.email, user.githubToken ]); + if (typeof user.notifyPrerelease !== 'boolean') user.notifyPrerelease = true; + + await db.query('INSERT INTO users (id, email, githubToken, notifyPrerelease) VALUES (?, ?, ?, ?)', + [ user.id, user.email, user.githubToken, user.notifyPrerelease ]); return user; } @@ -143,16 +150,17 @@ async function usersGet(userId) { const [result] = await db.query('SELECT * FROM users WHERE id=?', [ userId ]); if (!result.length) throw new Error('no such user'); - return result[0]; + return userPostprocess(result[0]); } -async function usersUpdate(userId, githubToken, email) { +async function usersUpdate(userId, githubToken, email, notifyPrerelease) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof githubToken, 'string'); assert.strictEqual(typeof email, 'string'); + assert.strictEqual(typeof notifyPrerelease, 'boolean'); - let args = [ githubToken, email, userId ]; - let query = 'UPDATE users SET githubToken=?,email=? WHERE id=?'; + let args = [ githubToken, email, notifyPrerelease, userId ]; + let query = 'UPDATE users SET githubToken=?,email=?,notifyPrerelease=? WHERE id=?'; await db.query(query, args); } @@ -186,6 +194,10 @@ async function releasesUpdate(releaseId, data) { } async function releasesListAllPending() { - const [result] = await db.query('SELECT * FROM releases WHERE notified=FALSE', []); + // a prerelease is only pending for users who have not opted out of prerelease notifications + const [result] = await db.query('SELECT releases.* FROM releases' + + ' JOIN projects ON projects.id=releases.projectId' + + ' JOIN users ON users.id=projects.userId' + + ' WHERE releases.notified=FALSE AND (releases.prerelease=FALSE OR users.notifyPrerelease=TRUE)', []); return result; } diff --git a/backend/routes.js b/backend/routes.js index 90c87ce..f21ee85 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -61,7 +61,7 @@ async function auth(req, res, next) { // update email if changed if (user.email !== req.oidc.user.email) { try { - await database.users.update(user.id, user.githubToken, req.oidc.user.email); + await database.users.update(user.id, user.githubToken, req.oidc.user.email, user.notifyPrerelease); user.email = req.oidc.user.email; } catch (e) { console.error('Failed to update email for user.', user, e); @@ -84,6 +84,9 @@ async function profileUpdate(req, res, next) { const githubToken = req.body.githubToken || ''; + // absent means "leave unchanged", so a client that does not know about this field cannot reset it + const notifyPrerelease = 'notifyPrerelease' in req.body ? !!req.body.notifyPrerelease : req.user.notifyPrerelease; + try { await github.verifyToken(githubToken); } catch (error) { @@ -91,11 +94,12 @@ async function profileUpdate(req, res, next) { } try { - await database.users.update(req.user.id, githubToken, req.user.email); + await database.users.update(req.user.id, githubToken, req.user.email, notifyPrerelease); } catch (error) { return next(new HttpError(500, error)); } req.user.githubToken = githubToken; + req.user.notifyPrerelease = notifyPrerelease; next(new HttpSuccess(202, {})); diff --git a/frontend/App.vue b/frontend/App.vue index c259601..f200eba 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -40,6 +40,10 @@ <br/> <a href="https://github.com/settings/tokens/new?description=ReleaseBell" target="_blank" style="margin-top: 10px;">Generate a GitHub API token</a> </div> + <div class="form-field"> + <Checkbox id="notifyPrereleaseInput" v-model="settingsDialog.notifyPrerelease" :binary="true"/> + <label for="notifyPrereleaseInput" style="margin-left: 10px;">Notify me about prereleases</label> + </div> </div> </form> <template #footer> @@ -151,6 +155,7 @@ export default { visible: false, busy: false, error: '', + notifyPrerelease: true, data: {} }, }; @@ -198,6 +203,7 @@ export default { }, onShowSettingsDialog() { this.settingsDialog.githubToken = this.user.githubToken; + this.settingsDialog.notifyPrerelease = this.user.notifyPrerelease !== false; this.settingsDialog.error = ''; this.settingsDialog.visible = true; }, @@ -228,7 +234,7 @@ export default { this.settingsDialog.error = ''; try { - await superagent.post(`${API_ORIGIN}/api/v1/profile`).send({ githubToken: this.settingsDialog.githubToken }); + await superagent.post(`${API_ORIGIN}/api/v1/profile`).send({ githubToken: this.settingsDialog.githubToken, notifyPrerelease: this.settingsDialog.notifyPrerelease }); } catch (error) { if (error.status === 402) { document.getElementById('githubTokenInput').focus(); @@ -241,6 +247,8 @@ export default { return; } + this.user.notifyPrerelease = this.settingsDialog.notifyPrerelease; + this.settingsDialog.busy = false; this.settingsDialog.visible = false; } diff --git a/migrations/20260804120000-add-user-notify-prerelease.js b/migrations/20260804120000-add-user-notify-prerelease.js new file mode 100644 index 0000000..28f2ef2 --- /dev/null +++ b/migrations/20260804120000-add-user-notify-prerelease.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE users ADD COLUMN notifyPrerelease BOOLEAN DEFAULT 1', callback); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE users DROP COLUMN notifyPrerelease', callback); +}; diff --git a/migrations/current_schema.sql b/migrations/current_schema.sql index 76bfcd6..8e2ecb6 100644 --- a/migrations/current_schema.sql +++ b/migrations/current_schema.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS users( id VARCHAR(128) NOT NULL UNIQUE, email VARCHAR(512) NOT NULL, githubToken VARCHAR(512) NOT NULL DEFAULT "", + notifyPrerelease BOOLEAN DEFAULT true, PRIMARY KEY(id)); @@ -25,7 +26,7 @@ CREATE TABLE IF NOT EXISTS releases( version VARCHAR(512) NOT NULL, body TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, notified BOOLEAN DEFAULT false, - prereleases BOOLEAN DEFAULT false, + prerelease BOOLEAN DEFAULT false, createdAt BIGINT NOT NULL, FOREIGN KEY(projectId) REFERENCES projects(id),On the per-project override I mentioned earlier: I have deliberately left it out of this patch to keep the diff to the thing you agreed to. It is still the better shape long term, because the common case is really "I want betas for this one project I am actually testing, and stable only for the other thirty". That would want a nullable
notifyPrereleaseonprojectsinheriting from the user setting when unset, which is a bigger change to the project list UI than to the backend. Happy to follow up with it as a second patch once this one is in, if you want it. -
Follow-up to the patch above, for anyone who wants the opposite of suppression: betas for one specific project while staying on stable everywhere else. This applies on top of the previous patch rather than replacing it, so apply that one first.
The shape is a nullable
notifyPrereleaseonprojects, whereNULLmeans "inherit whatever the user setting says". Existing projects all come outNULLon migration, so nothing changes for anyone until they deliberately set a project to something else. The pending query resolves the pair withCOALESCE(projects.notifyPrerelease, users.notifyPrerelease), project first. In the UI it is a three-way dropdown per row in the project table, reading Default, Always, Never.That gives the case the per-user flag on its own cannot express, and which I think is the one most people actually want. Turn prereleases off globally, then set Always on the one project you are testing, and you get its betas and nobody else's.
I ran the same MariaDB check over all six combinations of user setting against project override, using a pending prerelease per project.
project user_pref project_pref outcome always on Always notified inherit on Default notified never on Never suppressed always off Always notified inherit off Default suppressed never off Never suppressedStable releases are untouched throughout, including for a user who has prereleases off everywhere. Before the migration ran, all six existing projects came out
NULL, so an upgrade is a no-op.downdrops the column cleanly.One note on where the setting lives, in case you would rather it went elsewhere. I put the dropdown in the project table next to Track, because that is where the per-project state already is and it needs no new dialog. It does add a column to a table that is already fairly wide, so if you would prefer it tucked into a per-project menu instead, that is a UI-only change and the backend stands as is.
diff --git a/backend/database.js b/backend/database.js index 8960c5c..afc0aab 100644 --- a/backend/database.js +++ b/backend/database.js @@ -60,6 +60,8 @@ function init() { function projectPostprocess(p) { if (p.lastSuccessfulSyncAt === '0000-00-00 00:00:00') p.lastSuccessfulSyncAt = 0; p.enabled = !!p.enabled; + // null is meaningful here: it means "inherit the user setting", so only coerce an actual value + if (p.notifyPrerelease !== null && p.notifyPrerelease !== undefined) p.notifyPrerelease = !!p.notifyPrerelease; return p; } @@ -194,10 +196,12 @@ async function releasesUpdate(releaseId, data) { } async function releasesListAllPending() { - // a prerelease is only pending for users who have not opted out of prerelease notifications + // a prerelease is only pending if prerelease notifications are on for it. the project setting + // wins when set, and falls back to the user setting when NULL const [result] = await db.query('SELECT releases.* FROM releases' + ' JOIN projects ON projects.id=releases.projectId' + ' JOIN users ON users.id=projects.userId' + - ' WHERE releases.notified=FALSE AND (releases.prerelease=FALSE OR users.notifyPrerelease=TRUE)', []); + ' WHERE releases.notified=FALSE AND (releases.prerelease=FALSE' + + ' OR COALESCE(projects.notifyPrerelease, users.notifyPrerelease)=TRUE)', []); return result; } diff --git a/frontend/App.vue b/frontend/App.vue index f200eba..fb4cb67 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -96,6 +96,11 @@ {{ prettyDate(slotProps.data.createdAt) }} </template> </Column> + <Column field="notifyPrerelease" header="Prereleases" sortable> + <template #body="slotProps"> + <Dropdown v-model="slotProps.data.notifyPrerelease" :options="prereleaseOptions" optionLabel="name" optionValue="value" @change="onPrereleaseStateChanged(slotProps.data)"/> + </template> + </Column> <Column field="enabled" header="Track" sortable> <template #body="slotProps"> <InputSwitch v-model="slotProps.data.enabled" @change="onTrackStateChanged(slotProps.data)" /> @@ -146,6 +151,16 @@ export default { type: 'gitlab', name: 'GitLab' }], + prereleaseOptions: [{ + value: null, + name: 'Default' + }, { + value: true, + name: 'Always' + }, { + value: false, + name: 'Never' + }], addProjectDialog: { visible: false, busy: false, @@ -185,6 +200,9 @@ export default { async onTrackStateChanged(project) { await superagent.post(`${API_ORIGIN}/api/v1/projects/${project.id}`).send({ enabled: project.enabled }); }, + async onPrereleaseStateChanged(project) { + await superagent.post(`${API_ORIGIN}/api/v1/projects/${project.id}`).send({ notifyPrerelease: project.notifyPrerelease }); + }, async onLogout() { await superagent.get(`${API_ORIGIN}/api/v1/logout?return_to=${location.origin}`); this.user = null; diff --git a/migrations/20260804130000-add-project-notify-prerelease.js b/migrations/20260804130000-add-project-notify-prerelease.js new file mode 100644 index 0000000..28b572f --- /dev/null +++ b/migrations/20260804130000-add-project-notify-prerelease.js @@ -0,0 +1,10 @@ +'use strict'; + +// NULL means "inherit the user's notifyPrerelease setting" +exports.up = function(db, callback) { + db.runSql('ALTER TABLE projects ADD COLUMN notifyPrerelease BOOLEAN DEFAULT NULL', callback); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE projects DROP COLUMN notifyPrerelease', callback); +}; diff --git a/migrations/current_schema.sql b/migrations/current_schema.sql index 8e2ecb6..7af7b3a 100644 --- a/migrations/current_schema.sql +++ b/migrations/current_schema.sql @@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS projects( enabled BOOLEAN DEFAULT true, lastSuccessfulSyncAt BIGINT DEFAULT 0, type VARCHAR(32) NOT NULL DEFAULT "github", + notifyPrerelease BOOLEAN DEFAULT NULL, -- NULL means inherit the user's setting FOREIGN KEY(userId) REFERENCES users(id), PRIMARY KEY(id)); -
Instead of (NOT as well as) the second patch, you could alternatively do this: the whole thing as a single patch against
1.12.0(eb9d805), in case that is easier to take than the two separate ones. This replaces both of the diffs above rather than adding to them, so apply this or those, not both.It does two levels of setting. A
notifyPrereleaseflag onusersdefaulting to on, which is the suppression toggle asked for at the top of the thread, and a nullablenotifyPrereleaseonprojectswhereNULLmeans "inherit the user setting". The pending release query resolves them withCOALESCE(projects.notifyPrerelease, users.notifyPrerelease), project first. Existing users default to on and existing projects all migrate toNULL, so an upgrade changes nobody's mail until they touch a setting.The two levels together cover both directions people want. Untick the box in Settings and prereleases stop everywhere, which is the original request. Or leave it ticked and set a noisy project to Never. Or untick it globally and set Always on the one project you are actually testing, which is the case I suspect is most common and which the user flag alone cannot express.
In the UI that is a checkbox in the Settings dialog and a three-way dropdown per row in the project table, reading Default, Always, Never.
CheckboxandDropdownwere both already registered, so this adds no new dependency.I checked the migration and the query against MariaDB 11 rather than reasoning about them. Users and projects created before the columns existed come out on and
NULLrespectively, so upgrading is a no-op, and bothdownmigrations drop cleanly with no row loss. Across all six combinations of user setting against project override the resolution comes out as intended.project user_pref project_pref outcome always on Always notified inherit on Default notified never on Never suppressed always off Always notified inherit off Default suppressed never off Never suppressedStable releases are unaffected throughout, including for a user who has prereleases off everywhere, and an already-notified release stays excluded as before.
Two things worth knowing before you take it. A suppressed prerelease keeps
notified=FALSErather than being marked notified, so if someone turns the setting back on they will receive the prereleases published while it was off. That is what makes the setting reversible without losing anything, but say if you would rather they were silently dropped, which is a one-line change. And I correctedmigrations/current_schema.sql, which declared the releases column asprereleaseswhile the migration and all code useprerelease. That file is marked "Only for reference" so nothing was broken, but it is the file someone reads before writing exactly this patch.diff --git a/backend/database.js b/backend/database.js index 57e6918..afc0aab 100644 --- a/backend/database.js +++ b/backend/database.js @@ -60,6 +60,8 @@ function init() { function projectPostprocess(p) { if (p.lastSuccessfulSyncAt === '0000-00-00 00:00:00') p.lastSuccessfulSyncAt = 0; p.enabled = !!p.enabled; + // null is meaningful here: it means "inherit the user setting", so only coerce an actual value + if (p.notifyPrerelease !== null && p.notifyPrerelease !== undefined) p.notifyPrerelease = !!p.notifyPrerelease; return p; } @@ -123,16 +125,23 @@ async function projectsRemove(projectId) { await db.query('DELETE FROM projects WHERE id=?', [ projectId ]); } +function userPostprocess(u) { + u.notifyPrerelease = !!u.notifyPrerelease; + return u; +} + async function usersList() { const [result] = await db.query('SELECT * FROM users', []); - return result; + return result.map(userPostprocess); } async function usersAdd(user) { assert.strictEqual(typeof user, 'object'); - await db.query('INSERT INTO users (id, email, githubToken) VALUES (?, ?, ?)', - [ user.id, user.email, user.githubToken ]); + if (typeof user.notifyPrerelease !== 'boolean') user.notifyPrerelease = true; + + await db.query('INSERT INTO users (id, email, githubToken, notifyPrerelease) VALUES (?, ?, ?, ?)', + [ user.id, user.email, user.githubToken, user.notifyPrerelease ]); return user; } @@ -143,16 +152,17 @@ async function usersGet(userId) { const [result] = await db.query('SELECT * FROM users WHERE id=?', [ userId ]); if (!result.length) throw new Error('no such user'); - return result[0]; + return userPostprocess(result[0]); } -async function usersUpdate(userId, githubToken, email) { +async function usersUpdate(userId, githubToken, email, notifyPrerelease) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof githubToken, 'string'); assert.strictEqual(typeof email, 'string'); + assert.strictEqual(typeof notifyPrerelease, 'boolean'); - let args = [ githubToken, email, userId ]; - let query = 'UPDATE users SET githubToken=?,email=? WHERE id=?'; + let args = [ githubToken, email, notifyPrerelease, userId ]; + let query = 'UPDATE users SET githubToken=?,email=?,notifyPrerelease=? WHERE id=?'; await db.query(query, args); } @@ -186,6 +196,12 @@ async function releasesUpdate(releaseId, data) { } async function releasesListAllPending() { - const [result] = await db.query('SELECT * FROM releases WHERE notified=FALSE', []); + // a prerelease is only pending if prerelease notifications are on for it. the project setting + // wins when set, and falls back to the user setting when NULL + const [result] = await db.query('SELECT releases.* FROM releases' + + ' JOIN projects ON projects.id=releases.projectId' + + ' JOIN users ON users.id=projects.userId' + + ' WHERE releases.notified=FALSE AND (releases.prerelease=FALSE' + + ' OR COALESCE(projects.notifyPrerelease, users.notifyPrerelease)=TRUE)', []); return result; } diff --git a/backend/routes.js b/backend/routes.js index 90c87ce..f21ee85 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -61,7 +61,7 @@ async function auth(req, res, next) { // update email if changed if (user.email !== req.oidc.user.email) { try { - await database.users.update(user.id, user.githubToken, req.oidc.user.email); + await database.users.update(user.id, user.githubToken, req.oidc.user.email, user.notifyPrerelease); user.email = req.oidc.user.email; } catch (e) { console.error('Failed to update email for user.', user, e); @@ -84,6 +84,9 @@ async function profileUpdate(req, res, next) { const githubToken = req.body.githubToken || ''; + // absent means "leave unchanged", so a client that does not know about this field cannot reset it + const notifyPrerelease = 'notifyPrerelease' in req.body ? !!req.body.notifyPrerelease : req.user.notifyPrerelease; + try { await github.verifyToken(githubToken); } catch (error) { @@ -91,11 +94,12 @@ async function profileUpdate(req, res, next) { } try { - await database.users.update(req.user.id, githubToken, req.user.email); + await database.users.update(req.user.id, githubToken, req.user.email, notifyPrerelease); } catch (error) { return next(new HttpError(500, error)); } req.user.githubToken = githubToken; + req.user.notifyPrerelease = notifyPrerelease; next(new HttpSuccess(202, {})); diff --git a/frontend/App.vue b/frontend/App.vue index c259601..fb4cb67 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -40,6 +40,10 @@ <br/> <a href="https://github.com/settings/tokens/new?description=ReleaseBell" target="_blank" style="margin-top: 10px;">Generate a GitHub API token</a> </div> + <div class="form-field"> + <Checkbox id="notifyPrereleaseInput" v-model="settingsDialog.notifyPrerelease" :binary="true"/> + <label for="notifyPrereleaseInput" style="margin-left: 10px;">Notify me about prereleases</label> + </div> </div> </form> <template #footer> @@ -92,6 +96,11 @@ {{ prettyDate(slotProps.data.createdAt) }} </template> </Column> + <Column field="notifyPrerelease" header="Prereleases" sortable> + <template #body="slotProps"> + <Dropdown v-model="slotProps.data.notifyPrerelease" :options="prereleaseOptions" optionLabel="name" optionValue="value" @change="onPrereleaseStateChanged(slotProps.data)"/> + </template> + </Column> <Column field="enabled" header="Track" sortable> <template #body="slotProps"> <InputSwitch v-model="slotProps.data.enabled" @change="onTrackStateChanged(slotProps.data)" /> @@ -142,6 +151,16 @@ export default { type: 'gitlab', name: 'GitLab' }], + prereleaseOptions: [{ + value: null, + name: 'Default' + }, { + value: true, + name: 'Always' + }, { + value: false, + name: 'Never' + }], addProjectDialog: { visible: false, busy: false, @@ -151,6 +170,7 @@ export default { visible: false, busy: false, error: '', + notifyPrerelease: true, data: {} }, }; @@ -180,6 +200,9 @@ export default { async onTrackStateChanged(project) { await superagent.post(`${API_ORIGIN}/api/v1/projects/${project.id}`).send({ enabled: project.enabled }); }, + async onPrereleaseStateChanged(project) { + await superagent.post(`${API_ORIGIN}/api/v1/projects/${project.id}`).send({ notifyPrerelease: project.notifyPrerelease }); + }, async onLogout() { await superagent.get(`${API_ORIGIN}/api/v1/logout?return_to=${location.origin}`); this.user = null; @@ -198,6 +221,7 @@ export default { }, onShowSettingsDialog() { this.settingsDialog.githubToken = this.user.githubToken; + this.settingsDialog.notifyPrerelease = this.user.notifyPrerelease !== false; this.settingsDialog.error = ''; this.settingsDialog.visible = true; }, @@ -228,7 +252,7 @@ export default { this.settingsDialog.error = ''; try { - await superagent.post(`${API_ORIGIN}/api/v1/profile`).send({ githubToken: this.settingsDialog.githubToken }); + await superagent.post(`${API_ORIGIN}/api/v1/profile`).send({ githubToken: this.settingsDialog.githubToken, notifyPrerelease: this.settingsDialog.notifyPrerelease }); } catch (error) { if (error.status === 402) { document.getElementById('githubTokenInput').focus(); @@ -241,6 +265,8 @@ export default { return; } + this.user.notifyPrerelease = this.settingsDialog.notifyPrerelease; + this.settingsDialog.busy = false; this.settingsDialog.visible = false; } diff --git a/migrations/20260804120000-add-user-notify-prerelease.js b/migrations/20260804120000-add-user-notify-prerelease.js new file mode 100644 index 0000000..28f2ef2 --- /dev/null +++ b/migrations/20260804120000-add-user-notify-prerelease.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE users ADD COLUMN notifyPrerelease BOOLEAN DEFAULT 1', callback); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE users DROP COLUMN notifyPrerelease', callback); +}; diff --git a/migrations/20260804130000-add-project-notify-prerelease.js b/migrations/20260804130000-add-project-notify-prerelease.js new file mode 100644 index 0000000..28b572f --- /dev/null +++ b/migrations/20260804130000-add-project-notify-prerelease.js @@ -0,0 +1,10 @@ +'use strict'; + +// NULL means "inherit the user's notifyPrerelease setting" +exports.up = function(db, callback) { + db.runSql('ALTER TABLE projects ADD COLUMN notifyPrerelease BOOLEAN DEFAULT NULL', callback); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE projects DROP COLUMN notifyPrerelease', callback); +}; diff --git a/migrations/current_schema.sql b/migrations/current_schema.sql index 76bfcd6..7af7b3a 100644 --- a/migrations/current_schema.sql +++ b/migrations/current_schema.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS users( id VARCHAR(128) NOT NULL UNIQUE, email VARCHAR(512) NOT NULL, githubToken VARCHAR(512) NOT NULL DEFAULT "", + notifyPrerelease BOOLEAN DEFAULT true, PRIMARY KEY(id)); @@ -15,6 +16,7 @@ CREATE TABLE IF NOT EXISTS projects( enabled BOOLEAN DEFAULT true, lastSuccessfulSyncAt BIGINT DEFAULT 0, type VARCHAR(32) NOT NULL DEFAULT "github", + notifyPrerelease BOOLEAN DEFAULT NULL, -- NULL means inherit the user's setting FOREIGN KEY(userId) REFERENCES users(id), PRIMARY KEY(id)); @@ -25,7 +27,7 @@ CREATE TABLE IF NOT EXISTS releases( version VARCHAR(512) NOT NULL, body TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, notified BOOLEAN DEFAULT false, - prereleases BOOLEAN DEFAULT false, + prerelease BOOLEAN DEFAULT false, createdAt BIGINT NOT NULL, FOREIGN KEY(projectId) REFERENCES projects(id), -
@girish Yes, happy to. Two things in the way, and one of them is my own mistake, so let me get that out first.
The diffs above are against
eb9d805, which is ReleaseBell1.12.0dated 2025-03-31. I took that fromgithub.com/cloudron-io/releasebell, which turns out to be archived and to have had no push since that same date. The store is serving1.13.1. So I patched a mirror that stopped sixteen months ago and did not think to check it against the live version, which is entirely on me. I would not want you applying those diffs as posted.The second thing is access. I cannot find the repository to fork.
git.cloudron.io/packagesis public but exposes onlyn8n-app; searching for releasebell asmostcloudronreturns nothing, andpackages/releasebell,packages/releasebell-appandcloudron/releasebellall come back not-found rather than forbidden, so I cannot tell whether I am guessing the name wrong or simply cannot see it.Could you point me at the right path, and grant whatever access a fork needs? Once I can see it I will rebase onto current
master, re-run the checks against that base rather than the stale one, and open the MR.For what it is worth the change itself should be unaffected. It adds a
notifyPrereleasecolumn onusersdefaulting to on, an optional nullable one onprojectsmeaning "inherit the user setting", and aCOALESCEinreleasesListAllPending. Unless that query or the settings dialog moved between 1.12.0 and 1.13.1, it is the same patch with different line numbers. I will confirm rather than assume once I can read the real tree. -
@girish Yes, happy to. Two things in the way, and one of them is my own mistake, so let me get that out first.
The diffs above are against
eb9d805, which is ReleaseBell1.12.0dated 2025-03-31. I took that fromgithub.com/cloudron-io/releasebell, which turns out to be archived and to have had no push since that same date. The store is serving1.13.1. So I patched a mirror that stopped sixteen months ago and did not think to check it against the live version, which is entirely on me. I would not want you applying those diffs as posted.The second thing is access. I cannot find the repository to fork.
git.cloudron.io/packagesis public but exposes onlyn8n-app; searching for releasebell asmostcloudronreturns nothing, andpackages/releasebell,packages/releasebell-appandcloudron/releasebellall come back not-found rather than forbidden, so I cannot tell whether I am guessing the name wrong or simply cannot see it.Could you point me at the right path, and grant whatever access a fork needs? Once I can see it I will rebase onto current
master, re-run the checks against that base rather than the stale one, and open the MR.For what it is worth the change itself should be unaffected. It adds a
notifyPrereleasecolumn onusersdefaulting to on, an optional nullable one onprojectsmeaning "inherit the user setting", and aCOALESCEinreleasesListAllPending. Unless that query or the settings dialog moved between 1.12.0 and 1.13.1, it is the same patch with different line numbers. I will confirm rather than assume once I can read the real tree.@LoudLemur can you not just setup an email rule which filters/deletes inbound notifications which contain certain words?
-
@LoudLemur can you not just setup an email rule which filters/deletes inbound notifications which contain certain words?
@timconsidine thank you for the link, that is the piece I was missing. I had been guessing at
packages/releasebellandcloudron/releasebell; the group isapps, which I never tried.It does not get me all the way there, though. Signed in as
LoudLemurthe path 404s, and GitLab answers 404 rather than 403 for a private project you are not a member of, so I read that as the repository being there and my account not being on it. Theappsgroup is public but lists no public projects, andpackageslists exactly one, so I assume that is simply how app repos are kept rather than anything to do with me.@girish that narrows what I asked for in my last post. I no longer need the path, only access to
apps/releasebellforLoudLemur, or a fork pushed somewhere I can reach. Everything else on my side is unchanged: once I can read the real tree I will rebase onto currentmaster, re-run my checks against that base instead of the archived 1.12.0 mirror, and open the MR.Incidentally the footer of the notification email points at
git.cloudron.io/cloudron/releasebell, which is one of the paths that comes back not-found. Small thing, but the in-app link looks stale relative toapps/. Happy to fix it in the same MR.On the email rule: you are right that it works, and more cleanly than I would have guessed before looking. The subject is built as
<project> <version> (prerelease) released, and that(prerelease)comes straight from the release API flag rather than from pattern-matching the tag, so a filter on that literal string catches exactly the prerelease mails and nothing else. No false hits on a project that happens to ship a version calledbeta.So it is a good workaround and worth having posted for anyone who needs one today. Three things it cannot do, which is why I still think the setting earns its place.
It is all-or-nothing. The reason I ended up adding a per-project override on top of the per-user flag is that the useful setting is not "no prereleases" but "prereleases for the four projects I actually test against, and not for the other forty". Expressing that as mail rules means one rule per project, edited by hand every time you follow something new.
It deletes rather than defers. Because a suppressed release stays
notified=FALSEin the database, turning the setting back on delivers the prereleases you missed while it was off. That is deliberate, and it is what makes the setting safe to flip. A filtered email is gone.And it depends on a presentation detail. The subject string is template text, not API surface. If someone rewords it, the rule quietly stops matching and you start receiving prereleases again, or a reworded rule quietly starts eating real releases. Filters that fail in the delete direction fail silently.
None of which is an argument against your rule in the meantime.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login