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/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 000000000..4970ef182 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,196 @@ +name: Build and publish container image to GHCR + +on: + push: + branches: + - main + tags: + - 'v*' + 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 + 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" + else + docker buildx imagetools create \ + --tag "$tag" \ + "${src_tag}-amd64" + 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/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! ❤️ + diff --git a/Dockerfile b/Dockerfile index 3cd25c7f1..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 @@ -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 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; 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/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/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/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 %> 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) %>.
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