From dfe8e7bd5d42c2533d87f9b56fed755c2c5a2d51 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 2 Dec 2025 18:10:49 +0100 Subject: [PATCH 1/9] Scope general broadcasts by account Because DoS ourselves is not fun --- app/models/board/broadcastable.rb | 1 + app/views/cards/_broadcasts.html.erb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/board/broadcastable.rb b/app/models/board/broadcastable.rb index facb23ee4..f5481a9ff 100644 --- a/app/models/board/broadcastable.rb +++ b/app/models/board/broadcastable.rb @@ -3,5 +3,6 @@ module Board::Broadcastable included do broadcasts_refreshes + broadcasts_refreshes_to ->(board) { [ board.account, :all_boards ] } end end diff --git a/app/views/cards/_broadcasts.html.erb b/app/views/cards/_broadcasts.html.erb index 87c21e05d..04114b1f4 100644 --- a/app/views/cards/_broadcasts.html.erb +++ b/app/views/cards/_broadcasts.html.erb @@ -3,5 +3,5 @@ <%= turbo_stream_from board %> <% end %> <% else %> - <%= turbo_stream_from :all_boards %> + <%= turbo_stream_from [ Current.account, :all_boards ] %> <% end %> From da59bafa5cbfd4a75d31505c4195b7e35bc6ad24 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 2 Dec 2025 11:36:06 -0600 Subject: [PATCH 2/9] Make the jump nav hotkeys look like the others --- app/assets/stylesheets/nav.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/nav.css b/app/assets/stylesheets/nav.css index 511336977..bcbe83088 100644 --- a/app/assets/stylesheets/nav.css +++ b/app/assets/stylesheets/nav.css @@ -189,9 +189,8 @@ text-align: center; kbd { - border: none; - box-shadow: none; inset: 0.66em 0.33em auto auto; + line-height: 1.4; opacity: 0.5; position: absolute; From 2d0110963a41c1e3ff7102b07553f8ffdb059a83 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 2 Dec 2025 18:42:51 +0100 Subject: [PATCH 3/9] Add automated Docker image builds --- .github/workflows/publish-image.yml | 202 ++++++++++++++++++++++++++++ Dockerfile | 7 + 2 files changed, 209 insertions(+) create mode 100644 .github/workflows/publish-image.yml diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 000000000..0c02245c2 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,202 @@ +name: Build and publish container image to GHCR + +on: + push: + branches: + - main + tags: + - 'v*' + pull_request: + workflow_dispatch: + +concurrency: + group: publish-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +env: + IMAGE_DESCRIPTION: Fizzy is Kanban as it should be. Not as it has been. + SOURCE_URL: https://github.com/${{ github.repository }} + +jobs: + build: + name: Build and push image (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + platform: linux/amd64 + arch: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + arch: arm64 + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.11.1 + + - name: Log in to GHCR + uses: docker/login-action@v3.5.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute canonical image name (lowercase) + id: vars + shell: bash + run: | + set -eu + IMAGE_REF="${IMAGE_NAME:-$GITHUB_REPOSITORY}" + CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}" + echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT" + + - name: Extract Docker metadata (tags, labels) with arch suffix + id: meta + uses: docker/metadata-action@v5.8.0 + with: + images: ${{ steps.vars.outputs.canonical }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,format=short,prefix=sha- + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + flavor: | + latest=false + suffix=-${{ matrix.arch }} + labels: | + org.opencontainers.image.source=${{ env.SOURCE_URL }} + + - name: Build and push (${{ matrix.platform }}) + id: build + uses: docker/build-push-action@v6.18.0 + with: + context: . + file: Dockerfile + build-args: | + OCI_SOURCE=${{ env.SOURCE_URL }} + OCI_DESCRIPTION=${{ env.IMAGE_DESCRIPTION }} + platforms: ${{ matrix.platform }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,scope=${{ matrix.platform }},mode=max + sbom: false + provenance: false + + - name: Attest image provenance (per-arch) + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@v3.0.0 + with: + subject-name: ${{ steps.vars.outputs.canonical }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: false + + manifest: + name: Create multi-arch manifest and sign + needs: build + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + steps: + - name: Set up Docker Buildx (for imagetools) + uses: docker/setup-buildx-action@v3.11.1 + + - name: Log in to GHCR + uses: docker/login-action@v3.5.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute canonical image name (lowercase) + id: vars + shell: bash + run: | + set -eu + IMAGE_REF="${IMAGE_NAME:-$GITHUB_REPOSITORY}" + CANONICAL_IMAGE="${REGISTRY}/${IMAGE_REF,,}" + echo "canonical=${CANONICAL_IMAGE}" >> "$GITHUB_OUTPUT" + + - name: Compute base tags (no suffix) + id: meta + uses: docker/metadata-action@v5.8.0 + with: + images: ${{ steps.vars.outputs.canonical }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,format=short,prefix=sha- + type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + flavor: | + latest=false + labels: | + org.opencontainers.image.source=${{ env.SOURCE_URL }} + + - name: Create multi-arch manifests + shell: bash + run: | + set -eu + tags="${{ steps.meta.outputs.tags }}" + echo "Creating manifests for tags:" + printf '%s\n' "$tags" + while IFS= read -r tag; do + [ -z "$tag" ] && continue + echo "Creating manifest for $tag" + src_tag="$tag" + if [[ "$tag" == *:latest && "${GITHUB_REF}" == refs/tags/* ]]; then + ref="${GITHUB_REF#refs/tags/}" + src_tag="${tag%:latest}:$ref" + fi + if [ -n "${IMAGE_DESCRIPTION:-}" ]; then + docker buildx imagetools create \ + --tag "$tag" \ + --annotation "index:org.opencontainers.image.description=${IMAGE_DESCRIPTION}" \ + "${src_tag}-amd64" \ + "${src_tag}-arm64" + else + docker buildx imagetools create \ + --tag "$tag" \ + "${src_tag}-amd64" \ + "${src_tag}-arm64" + fi + done <<< "$tags" + + - name: Install Cosign + uses: sigstore/cosign-installer@v3.9.2 + + - name: Cosign sign all tags (keyless OIDC) + shell: bash + run: | + set -eu + tags="${{ steps.meta.outputs.tags }}" + printf '%s\n' "$tags" + while IFS= read -r tag; do + [ -z "$tag" ] && continue + echo "Signing $tag" + cosign sign --yes "$tag" + done <<< "$tags" + + diff --git a/Dockerfile b/Dockerfile index 3cd25c7f1..6033d297e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,6 +59,13 @@ RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile # Final stage for app image FROM base +# Image metadata +ARG OCI_DESCRIPTION +LABEL org.opencontainers.image.description="${OCI_DESCRIPTION}" +ARG OCI_SOURCE +LABEL org.opencontainers.image.source="${OCI_SOURCE}" +LABEL org.opencontainers.image.licenses="O'Saasy" + # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash From 07ee2908a017bd36d9dc28a184c498de86ae5caf Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 2 Dec 2025 18:50:54 +0100 Subject: [PATCH 4/9] Add libssl-dev --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6033d297e..59cb8cfef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ WORKDIR /rails # Install base packages RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libssl-dev && \ ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives From 94d1a1e10a90ecd35986cb6f8b10085fdc87d1b8 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 2 Dec 2025 18:51:07 +0100 Subject: [PATCH 5/9] Disable image builds for PRs --- .github/workflows/publish-image.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 0c02245c2..8605e21ae 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -6,7 +6,6 @@ on: - main tags: - 'v*' - pull_request: workflow_dispatch: concurrency: From fc8ca04c05b7eeed89c39626cd393983d55317ae Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 2 Dec 2025 18:59:40 +0100 Subject: [PATCH 6/9] Disable ARM image builds --- .github/workflows/publish-image.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 8605e21ae..4970ef182 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -34,9 +34,6 @@ jobs: - runner: ubuntu-latest platform: linux/amd64 arch: amd64 - - runner: ubuntu-24.04-arm - platform: linux/arm64 - arch: arm64 env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} @@ -173,13 +170,11 @@ jobs: docker buildx imagetools create \ --tag "$tag" \ --annotation "index:org.opencontainers.image.description=${IMAGE_DESCRIPTION}" \ - "${src_tag}-amd64" \ - "${src_tag}-arm64" + "${src_tag}-amd64" else docker buildx imagetools create \ --tag "$tag" \ - "${src_tag}-amd64" \ - "${src_tag}-arm64" + "${src_tag}-amd64" fi done <<< "$tags" From a2e024c1b5dde4e477142416bd42ea822ea05bf0 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 2 Dec 2025 14:22:50 -0500 Subject: [PATCH 7/9] Disconnect action cable when user is deactivated ref: https://app.fizzy.do/5986089/cards/2785 --- app/models/user.rb | 6 ++++++ test/models/user_test.rb | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 68df97a65..d70a260ea 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,10 +24,16 @@ class User < ApplicationRecord transaction do accesses.destroy_all update! active: false, identity: nil + close_remote_connections end end def setup? name != identity.email_address end + + private + def close_remote_connections + ActionCable.server.remote_connections.where(current_user: self).disconnect(reconnect: false) + end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 5ffc5db56..36b0e7986 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -25,7 +25,10 @@ class UserTest < ActiveSupport::TestCase test "deactivate" do assert_changes -> { users(:jz).active? }, from: true, to: false do assert_changes -> { users(:jz).accesses.count }, from: 1, to: 0 do - users(:jz).deactivate + users(:jz).tap do |user| + user.stubs(:close_remote_connections).once + user.deactivate + end end end end From 8bc2b7c8c8487bdc7e4ca5b10e0bf97057f3183c Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 2 Dec 2025 20:37:58 +0100 Subject: [PATCH 8/9] Add contributing guide --- .github/ISSUE_TEMPLATE/config.yml | 5 +++ .github/ISSUE_TEMPLATE/preapproved.md | 8 +++++ CONTRIBUTING.md | 49 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/preapproved.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..f4f8f439c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Features, Bug Reports, Questions + url: https://github.com/basecamp/fizzy/discussions/new/choose + about: Please use the discussions area to report issues or ask quest diff --git a/.github/ISSUE_TEMPLATE/preapproved.md b/.github/ISSUE_TEMPLATE/preapproved.md new file mode 100644 index 000000000..153e13a3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/preapproved.md @@ -0,0 +1,8 @@ +--- +name: Pre-Discussed and Approved Topics +about: |- + For topics already discussed and approved in the GitHub Discussions section. +--- + +** PLEASE START A DISCUSSION INSTEAD OF OPENING AN ISSUE ** +** For more details see CONTRIBUTING.md ** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..f981eb646 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# How to contribute to Fizzy + +Fizzy uses GitHub +[discussions](https://github.com/basecamp/fizzy/discussions) to track +feature requests and questions, rather than [the issue +tracker](https://github.com/basecamp/fizzy/issues). If you're considering +opening an issue or pull request, please open a discussion instead. + +Whenever a discussion leads to an actionable and well-understood task, we'll +move it to the issue tracker where it can be worked on. + +This is a little different than how some other projects work, but it makes it +easier for us to triage and prioritise the work. It also means that the open +issues all represent agreed-upon tasks that are either being worked on, or are +ready to be worked on. + +This should also make it easier to see what's in progress, and to find +something to work on if you'd like to do so. + +## What this means in practice + +### If you'd like to contribute to the code... + +1. If you're interested in working on one of the open issues, please do! We are + grateful for the help! +2. You'll want to make sure someone else isn't already working on the same + issue. If they are, it will be tagged "in progress" and/or it should be clear + from the comments. When in doubt, you can always comment on the issue to ask. +3. Similarly, if you need any help or guidance on the issue, please comment on + the issue as you go, and we'll do our best to help. +4. When you have something ready for review or collaboration, open a PR. + +### If you've found a bug... + +1. If you don't have steps to reproduce the problem, or you're not certain it's a + bug, open a discussion. +2. If you have steps to reproduce, open an issue. + +### If you have an idea for a feature... + +1. Open a discussion. + +### If you have a question, or are having trouble with configuration... + +1. Open a discussion. + +Hopefully this process makes it easier for everyone to be involved. Thanks for +helping! ❤️ + From cdc1f9b14347d777cd7956fb274ae2218c3ccbe9 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 2 Dec 2025 19:54:20 +0000 Subject: [PATCH 9/9] Allow typing magic links on mobile When typing a magic link, rather than pasting it, iOS seems to send `keydown.enter`, and we weren't submitting the form in that case. Changed to submit the form in either case, and use the state of the input target to help guard against double submissions. --- .../controllers/magic_link_controller.js | 14 +++++--------- app/views/sessions/magic_links/show.html.erb | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/javascript/controllers/magic_link_controller.js b/app/javascript/controllers/magic_link_controller.js index b2e487489..4d6c45ab5 100644 --- a/app/javascript/controllers/magic_link_controller.js +++ b/app/javascript/controllers/magic_link_controller.js @@ -4,16 +4,12 @@ import { onNextEventLoopTick } from "helpers/timing_helpers" export default class extends Controller { static targets = [ "input" ] - submit(event) { + submit() { onNextEventLoopTick(() => { - this.inputTarget.disabled = true - }) - } - - paste(event) { - onNextEventLoopTick(() => { - this.element.submit() - this.inputTarget.disabled = true + if (!this.inputTarget.disabled) { + this.element.submit() + this.inputTarget.disabled = true + } }) } } diff --git a/app/views/sessions/magic_links/show.html.erb b/app/views/sessions/magic_links/show.html.erb index affa97b8a..f8be106a0 100644 --- a/app/views/sessions/magic_links/show.html.erb +++ b/app/views/sessions/magic_links/show.html.erb @@ -10,7 +10,7 @@ <%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large", autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true, autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code], - data: { magic_link_target: "input", action: "keydown.enter->magic-link#submit paste->magic-link#paste" } %> + data: { magic_link_target: "input", action: "keydown.enter->magic-link#submit paste->magic-link#submit" } %> <% end %>

The code you receive will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.