Cloudron makes it easy to run web apps like WordPress, Nextcloud, GitLab on your server. Find out more or install now.


Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Bookmarks
  • Search
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
Brand Logo

Cloudron Forum

Offical apps | Community apps | Demo | Docs | Install
  1. Cloudron Forum
  2. Release Bell
  3. ReleaseBell: option to suppress prerelease notifications

ReleaseBell: option to suppress prerelease notifications

Scheduled Pinned Locked Moved Release Bell
11 Posts 4 Posters 189 Views 3 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • L Offline
    L Offline
    LoudLemur
    wrote last edited by
    #1

    Read against eb9d805. Last of three findings, and the one I would most like as a user.

    The prerelease column has been there since 20230926130714-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 in releasesListAllPending.

    A live example. The three newest tags on dbgate/dbgate today:

    v7.2.4-premium-beta.7
    v7.2.4-premium-beta.5
    v7.2.4-beta.6
    

    while GET /repos/dbgate/dbgate/releases/latest still 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.

    1 Reply Last reply
    0
    • fbartelsF Offline
      fbartelsF Offline
      fbartels
      App Dev
      wrote last edited by
      #2

      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

      1 Reply Last reply
      4
      • J joseph moved this topic from Apps
      • girishG Offline
        girishG Offline
        girish
        Staff
        wrote last edited by
        #3

        I think this is a good idea. @loudlemur feel free to post a patch and I can merge it.

        L 1 Reply Last reply
        0
        • girishG girish

          I think this is a good idea. @loudlemur feel free to post a patch and I can merge it.

          L Offline
          L Offline
          LoudLemur
          wrote last edited by
          #4

          @girish said:

          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 notifyPrerelease column on users defaults to 1, so existing installs keep their current behaviour after migrating and nobody has to go and re-enable anything. releasesListAllPending then joins through projects to users and drops prereleases only for users who have opted out. The setting is a checkbox in the existing Settings dialog, and Checkbox was already registered in main.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. down drops 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=FALSE forever, 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.sql declares the releases column as prereleases while the migration 20230926130714 and every code path use prerelease. 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 notifyPrerelease on projects inheriting 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.

          1 Reply Last reply
          0
          • L Offline
            L Offline
            LoudLemur
            wrote last edited by
            #5

            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 notifyPrerelease on projects, where NULL means "inherit whatever the user setting says". Existing projects all come out NULL on migration, so nothing changes for anyone until they deliberately set a project to something else. The pending query resolves the pair with COALESCE(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         suppressed
            

            Stable 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. down drops 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));
            
            1 Reply Last reply
            0
            • L Offline
              L Offline
              LoudLemur
              wrote last edited by LoudLemur
              #6

              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 notifyPrerelease flag on users defaulting to on, which is the suppression toggle asked for at the top of the thread, and a nullable notifyPrerelease on projects where NULL means "inherit the user setting". The pending release query resolves them with COALESCE(projects.notifyPrerelease, users.notifyPrerelease), project first. Existing users default to on and existing projects all migrate to NULL, 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. Checkbox and Dropdown were 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 NULL respectively, so upgrading is a no-op, and both down migrations 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         suppressed
              

              Stable 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=FALSE rather 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 corrected migrations/current_schema.sql, which declared the releases column as prereleases while the migration and all code use prerelease. 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),
              
              1 Reply Last reply
              0
              • girishG Offline
                girishG Offline
                girish
                Staff
                wrote last edited by
                #7

                Do you think you can submit a MR instead?

                L 1 Reply Last reply
                0
                • girishG girish

                  Do you think you can submit a MR instead?

                  L Offline
                  L Offline
                  LoudLemur
                  wrote last edited by
                  #8

                  @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 ReleaseBell 1.12.0 dated 2025-03-31. I took that from github.com/cloudron-io/releasebell, which turns out to be archived and to have had no push since that same date. The store is serving 1.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/packages is public but exposes only n8n-app; searching for releasebell as mostcloudron returns nothing, and packages/releasebell, packages/releasebell-app and cloudron/releasebell all 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 notifyPrerelease column on users defaulting to on, an optional nullable one on projects meaning "inherit the user setting", and a COALESCE in releasesListAllPending. 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.

                  timconsidineT 1 Reply Last reply
                  0
                  • L LoudLemur

                    @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 ReleaseBell 1.12.0 dated 2025-03-31. I took that from github.com/cloudron-io/releasebell, which turns out to be archived and to have had no push since that same date. The store is serving 1.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/packages is public but exposes only n8n-app; searching for releasebell as mostcloudron returns nothing, and packages/releasebell, packages/releasebell-app and cloudron/releasebell all 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 notifyPrerelease column on users defaulting to on, an optional nullable one on projects meaning "inherit the user setting", and a COALESCE in releasesListAllPending. 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.

                    timconsidineT Offline
                    timconsidineT Offline
                    timconsidine
                    App Dev
                    wrote last edited by
                    #9

                    @LoudLemur can you not just setup an email rule which filters/deletes inbound notifications which contain certain words?

                    Indie app dev, huge fan of Cloudron PaaS, scratching my itches : communityapps.appx.uk

                    L 1 Reply Last reply
                    0
                    • girishG Offline
                      girishG Offline
                      girish
                      Staff
                      wrote last edited by
                      #10

                      The code is here - https://git.cloudron.io/apps/releasebell

                      1 Reply Last reply
                      0
                      • timconsidineT timconsidine

                        @LoudLemur can you not just setup an email rule which filters/deletes inbound notifications which contain certain words?

                        L Offline
                        L Offline
                        LoudLemur
                        wrote last edited by
                        #11

                        @timconsidine thank you for the link, that is the piece I was missing. I had been guessing at packages/releasebell and cloudron/releasebell; the group is apps, which I never tried.

                        It does not get me all the way there, though. Signed in as LoudLemur the 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. The apps group is public but lists no public projects, and packages lists 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/releasebell for LoudLemur, 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 current master, 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 to apps/. 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 called beta.

                        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=FALSE in 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.

                        1 Reply Last reply
                        0

                        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
                        Reply
                        • Reply as topic
                        Log in to reply
                        • Oldest to Newest
                        • Newest to Oldest
                        • Most Votes


                        • Login

                        • Don't have an account? Register

                        • Login or register to search.
                        • First post
                          Last post
                        0
                        • Categories
                        • Recent
                        • Tags
                        • Popular
                        • Bookmarks
                        • Search