Add the coffeylabs theme and the image build #1

Merged
jcoffey-dev merged 1 commits from coffeylabs-theme into main 2026-10-06 05:29:27 +00:00
35 changed files with 593 additions and 1650 deletions

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
# CI for the Coffey Labs build of Mastodon: upstream Mastodon plus the
# coffeylabs theme (COFFEYLABS.md). Upstream's own .github/workflows were
# removed in this fork; this is the only workflow, and it runs on Gitea only.
#
# Every change builds the image without pushing it, to prove the theme still
# compiles against the upstream it sits on. Releases are cut by pushing a tag
# named v<upstream>-coffeylabs.<n>, e.g. v4.7.3-coffeylabs.1, where <upstream>
# is the version in lib/mastodon/version.rb. The tag must name the commit's
# own upstream version and be on main, or nothing is published.
#
# Every job runs in an image pinned by digest (tag in the trailing comment),
# and the only action used is coffey-labs/actions/checkout pinned by SHA.
# Read the comment for the version; the digest is what runs.
name: ci
on:
push:
branches: [main]
tags: ['v*-coffeylabs.*']
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ------------------------------------------------------------- build ------
# The full production image, as the Dockerfile builds it: Ruby gems, the
# Vite asset build with both themes, libvips and ffmpeg. Nothing is pushed.
docker-build:
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
runs-on: docker
timeout-minutes: 90
container:
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
- run: |
tag="mastodon:ci-$(echo "$GITHUB_SHA" | cut -c1-8)"
docker build --build-arg MASTODON_VERSION_METADATA=coffeylabs.ci -t "$tag" .
docker run --rm --entrypoint sh "$tag" -c \
'ls public/packs/assets/themes/ | grep -q "^coffeylabs-.*\.css$"' \
|| { echo "!! the image has no compiled coffeylabs theme" >&2; exit 1; }
docker image rm "$tag"
# ----------------------------------------------------------- release ------
# linux/amd64 only: the one server that runs it is amd64. The streaming
# server is unchanged by the theme, so it keeps using upstream's image at
# the same version. PACKAGE_TOKEN (jcoffey-dev, write:package) logs in: the
# job's own token is refused by the container registry. The release is
# created last, so a release on the page always has its image behind it.
publish:
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
runs-on: docker
timeout-minutes: 90
container:
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env:
REGISTRY: ${{ vars.REGISTRY }}
IMAGE: ${{ vars.REGISTRY }}/${{ github.repository }}
TAG: ${{ github.ref_name }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
with:
fetch-depth: 0
- id: v
run: |
set -eu
upstream="$(awk '/def (major|minor|patch)$/ { getline; gsub(/ /, ""); printf "%s%s", sep, $0; sep="." }' lib/mastodon/version.rb)"
case "$TAG" in
"v$upstream-coffeylabs."[0-9]*) ;;
*) echo "!! $TAG does not name this commit's upstream version; expected v$upstream-coffeylabs.<n>" >&2; exit 1 ;;
esac
git merge-base --is-ancestor "$(git rev-parse "${TAG}^{commit}")" origin/main \
|| { echo "!! $TAG is not on main" >&2; exit 1; }
echo "METADATA=${TAG#v"$upstream"-}" >> "$GITHUB_OUTPUT"
- run: |
test -n "$REGISTRY"
test -n "$PACKAGE_TOKEN" || { echo "PACKAGE_TOKEN secret is not set on this repository" >&2; exit 1; }
echo "$PACKAGE_TOKEN" | docker login -u jcoffey-dev --password-stdin "$REGISTRY"
- env:
METADATA: ${{ steps.v.outputs.METADATA }}
run: |
docker build \
--build-arg MASTODON_VERSION_METADATA="$METADATA" \
--build-arg SOURCE_COMMIT="$GITHUB_SHA" \
--build-arg SOURCE_BASE_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
--build-arg SOURCE_TAG="$TAG" \
--tag "$IMAGE:$TAG" .
docker push "$IMAGE:$TAG"
docker image rm "$IMAGE:$TAG"
# Show the package on the repository's Packages tab. Idempotent.
- run: |
apk add --no-cache -q curl
curl -fsS -o /dev/null -X POST -H "Authorization: token $PACKAGE_TOKEN" \
"$CI_SERVER_INTERNAL/api/v1/packages/${GITHUB_REPOSITORY%%/*}/container/${GITHUB_REPOSITORY#*/}/-/link/${GITHUB_REPOSITORY#*/}" \
|| echo "package already linked (or link refused); not fatal"
- env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
body="Mastodon ${TAG%-coffeylabs.*} with the coffeylabs theme.\n\nImage: \`$IMAGE:$TAG\` (linux/amd64). Use upstream's mastodon-streaming image at ${TAG%-coffeylabs.*}."
curl -fsS -o /dev/null -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
--data "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"$body\"}" \
"$CI_SERVER_INTERNAL/api/v1/repos/$GITHUB_REPOSITORY/releases"
echo "release $TAG created"
- if: always()
run: docker logout "$REGISTRY" || true
-171
View File
@@ -1,171 +0,0 @@
on:
workflow_call:
inputs:
cache:
type: boolean
default: true
push_to_images:
type: string
version_prerelease:
type: string
version_metadata:
type: string
flavor:
type: string
tags:
type: string
labels:
type: string
file_to_build:
type: string
# This builds multiple images with one runner each, allowing us to build for multiple architectures
# using Github's runners.
# The two-step process is adapted form:
# https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
jobs:
# Build each (amd64 and arm64) image separately
build-image:
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Prepare
env:
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
# Transform multi-line variable into comma-separated variable
image_names=${PUSH_TO_IMAGES//$'\n'/,}
image_names_split=${image_names%,}
echo "IMAGE_NAMES=${image_names_split,,}" >> $GITHUB_ENV
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
id: buildx
- name: Log in to Docker Hub
if: contains(inputs.push_to_images, 'tootsuite')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the GitHub Container registry
if: contains(inputs.push_to_images, 'ghcr.io')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
if: ${{ inputs.push_to_images != '' }}
with:
images: ${{ inputs.push_to_images }}
flavor: ${{ inputs.flavor }}
labels: ${{ inputs.labels }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ${{ inputs.file_to_build }}
build-args: |
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
SOURCE_COMMIT=${{ github.sha }}
platforms: ${{ matrix.platform }}
provenance: false
push: ${{ inputs.push_to_images != '' }}
cache-from: ${{ inputs.cache && format('type=gha,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
cache-to: ${{ inputs.cache && format('type=gha,mode=max,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
outputs: type=image,"name=${{ env.IMAGE_NAMES }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push_to_images != '' }}
- name: Export digest
if: ${{ inputs.push_to_images != '' }}
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
if: ${{ inputs.push_to_images != '' }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
# `hashFiles` is used to disambiguate between streaming and non-streaming images
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
# Then merge the docker images into a single one
merge-images:
if: ${{ inputs.push_to_images != '' }}
runs-on: ubuntu-24.04
needs:
- build-image
env:
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: ${{ runner.temp }}/digests
# `hashFiles` is used to disambiguate between streaming and non-streaming images
pattern: digests-${{ hashFiles(inputs.file_to_build) }}-*
merge-multiple: true
- name: Log in to Docker Hub
if: contains(inputs.push_to_images, 'tootsuite')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the GitHub Container registry
if: contains(inputs.push_to_images, 'ghcr.io')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Docker meta
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
if: ${{ inputs.push_to_images != '' }}
with:
images: ${{ inputs.push_to_images }}
flavor: ${{ inputs.flavor }}
tags: ${{ inputs.tags }}
labels: ${{ inputs.labels }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
echo "${PUSH_TO_IMAGES,,}" | xargs -I{} \
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '{}@sha256:%s ' *)
- name: Inspect image
run: |
echo "${PUSH_TO_IMAGES,,}" | xargs -i{} \
docker buildx imagetools inspect {}:${{ steps.meta.outputs.version }}
-62
View File
@@ -1,62 +0,0 @@
name: Build nightly container image
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
permissions:
contents: read
packages: write
jobs:
compute-suffix:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- id: version_vars
env:
TZ: Etc/UTC
run: |
echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
outputs:
prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
build-image:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: Dockerfile
cache: false
push_to_images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
latest=auto
tags: |
type=raw,value=edge
type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit
build-image-streaming:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
cache: false
push_to_images: |
tootsuite/mastodon-streaming
ghcr.io/mastodon/mastodon-streaming
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
latest=auto
tags: |
type=raw,value=edge
type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit
-58
View File
@@ -1,58 +0,0 @@
name: Build container image for PR
on:
pull_request:
types: [labeled, synchronize, reopened, ready_for_review, opened]
permissions:
contents: read
packages: write
jobs:
compute-suffix:
runs-on: ubuntu-latest
# This is only allowed to run if:
# - the PR branch is in the `mastodon/mastodon` repository
# - the PR is not a draft
# - the PR has the "build-image" label
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'build-image') }}
steps:
# Repository needs to be cloned so `git rev-parse` below works
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- id: version_vars
run: |
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
echo mastodon_short_sha=$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
outputs:
metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
short_sha: ${{ steps.version_vars.outputs.mastodon_short_sha }}
build-image:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: Dockerfile
push_to_images: |
ghcr.io/mastodon/mastodon
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
flavor: |
latest=auto
tags: |
type=ref,event=pr
type=ref,event=pr,suffix=-${{ needs.compute-suffix.outputs.short_sha }}
secrets: inherit
build-image-streaming:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
push_to_images: |
ghcr.io/mastodon/mastodon-streaming
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
flavor: |
latest=auto
tags: |
type=ref,event=pr
type=ref,event=pr,suffix=-${{ needs.compute-suffix.outputs.short_sha }}
secrets: inherit
-84
View File
@@ -1,84 +0,0 @@
name: Build container release images
on:
push:
tags:
- '*'
permissions:
contents: read
packages: write
jobs:
check-latest-stable:
runs-on: ubuntu-latest
outputs:
latest: ${{ steps.check.outputs.is_latest_stable }}
steps:
# Repository needs to be cloned to list branches
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Check latest stable
shell: bash
id: check
run: |
ref="${GITHUB_REF#refs/tags/}"
if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then
current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
else
echo "tag $ref is not semver"
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
exit 0
fi
latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \
| sed -E 's#^origin/stable-##' \
| sort -Vr \
| head -n1)
if [[ "$current" == "$latest" ]]; then
echo "is_latest_stable=true" >> "$GITHUB_OUTPUT"
else
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
fi
build-image:
needs: check-latest-stable
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: Dockerfile
push_to_images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
cache: false
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ needs.check-latest-stable.outputs.latest }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit
build-image-streaming:
needs: check-latest-stable
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
push_to_images: |
tootsuite/mastodon-streaming
ghcr.io/mastodon/mastodon-streaming
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
cache: false
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ needs.check-latest-stable.outputs.latest }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
secrets: inherit
-59
View File
@@ -1,59 +0,0 @@
name: Build security nightly container image
on:
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
compute-suffix:
runs-on: ubuntu-latest
steps:
- id: version_vars
env:
TZ: Etc/UTC
run: |
echo mastodon_version_prerelease=nightly.$(date --date='next day' +'%Y-%m-%d')-security>> $GITHUB_OUTPUT
outputs:
prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
build-image:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: Dockerfile
cache: false
push_to_images: |
tootsuite/mastodon
ghcr.io/mastodon/mastodon
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
latest=auto
tags: |
type=raw,value=edge
type=raw,value=nightly
type=raw,value=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit
build-image-streaming:
needs: compute-suffix
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
cache: false
push_to_images: |
tootsuite/mastodon-streaming
ghcr.io/mastodon/mastodon-streaming
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |
latest=auto
tags: |
type=raw,value=edge
type=raw,value=nightly
type=raw,value=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit
-40
View File
@@ -1,40 +0,0 @@
name: Bundler Audit
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'Gemfile*'
- '.ruby-version'
- '.github/workflows/bundler-audit.yml'
pull_request:
paths:
- 'Gemfile*'
- '.ruby-version'
- '.github/workflows/bundler-audit.yml'
schedule:
- cron: '0 5 * * 1'
jobs:
security:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BUNDLE_ONLY: development
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Ruby
uses: ruby/setup-ruby@6e5d382445ae5590b7449d8b3bc8cb1c2c27f617 # v1
with:
bundler-cache: true
- name: Run bundler-audit
run: bin/bundler-audit check --update
-52
View File
@@ -1,52 +0,0 @@
name: Check i18n
on:
push:
branches:
- 'main'
- 'stable-*'
pull_request:
branches:
- 'main'
- 'stable-*'
env:
RAILS_ENV: test
permissions:
contents: read
jobs:
check-i18n:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Check for missing strings in English JSON
run: |
yarn i18n:extract --throws
git diff --exit-code
- name: Check locale file normalization
run: bin/i18n-tasks check-normalized
- name: Check for unused strings
run: bin/i18n-tasks unused -l en
- name: Check for missing strings in English YML
run: |
bin/i18n-tasks missing -t used -l en
- name: Check for wrong string interpolations
run: bin/i18n-tasks check-consistent-interpolations
- name: Check that all required locale files exist
run: bin/rake repo:check_locales_files
-64
View File
@@ -1,64 +0,0 @@
name: 'Chromatic'
permissions:
contents: read
on:
push:
branches-ignore:
- renovate/*
- stable-*
jobs:
pathcheck:
name: Check for relevant changes
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
changed: ${{ steps.filter.outputs.src }}
steps:
- name: Checkout code
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- uses: dorny/paths-filter@0e4a8c6effa4802afeda77dc8d303f8176d7dfad # v3
id: filter
with:
filters: |
src:
- 'package.json'
- 'yarn.lock'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '.github/workflows/chromatic.yml'
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
timeout-minutes: 15
needs: pathcheck
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
steps:
- name: Checkout code
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Build Storybook
run: yarn build-storybook
- name: Run Chromatic
uses: chromaui/action@98132da0fca2211fb41b3af7b49ce85176007851 # v17
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
zip: true
storybookBuildDir: 'storybook-static'
exitOnceUploaded: true # Exit immediately after upload
autoAcceptChanges: 'main' # Auto-accept changes on main branch only
-67
View File
@@ -1,67 +0,0 @@
name: 'CodeQL'
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
pull_request:
branches:
- 'main'
- 'stable-*'
schedule:
- cron: '22 6 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['actions', 'javascript', 'ruby']
# CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
with:
category: '/language:${{matrix.language}}'
@@ -1,69 +0,0 @@
name: Crowdin / Download translations (stable branches)
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
download-translations-stable:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?
# See https://github.com/orgs/community/discussions/55820
run: |
git config --global http.version HTTP/1.1
git config --global http.postBuffer 157286400
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2
with:
upload_sources: false
upload_translations: false
download_translations: true
crowdin_branch_name: ${{ github.base_ref || github.ref_name }}
push_translations: false
create_pull_request: false
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root
# We need to fix this before the next steps
- name: Fix file permissions
run: sudo chown -R runner:docker .
# This is needed to run the normalize step
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Run i18n normalize task
run: bin/i18n-tasks normalize
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
author: 'GitHub Actions <[email protected]>'
body: |
New Crowdin translations, automated with GitHub Actions
See `.github/workflows/crowdin-download.yml`
This PR will be updated every day with new translations.
Due to a limitation in GitHub Actions, checks are not running on this PR without manual action.
If you want to run the checks, then please press “Approve workflows to run”.
branch: i18n/crowdin/translations-${{ github.base_ref || github.ref_name }}
base: ${{ github.base_ref || github.ref_name }}
labels: i18n
-71
View File
@@ -1,71 +0,0 @@
name: Crowdin / Download translations
on:
schedule:
- cron: '17 4 * * *' # Every day
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
download-translations:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Increase Git http.postBuffer
# This is needed due to a bug in Ubuntu's cURL version?
# See https://github.com/orgs/community/discussions/55820
run: |
git config --global http.version HTTP/1.1
git config --global http.postBuffer 157286400
# Download the translation files from Crowdin
- name: crowdin action
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2
with:
upload_sources: false
upload_translations: false
download_translations: true
crowdin_branch_name: main
push_translations: false
create_pull_request: false
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root
# We need to fix this before the next steps
- name: Fix file permissions
run: sudo chown -R runner:docker .
# This is needed to run the normalize step
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Run i18n normalize task
run: bin/i18n-tasks normalize
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'
author: 'GitHub Actions <[email protected]>'
body: |
New Crowdin translations, automated with GitHub Actions
See `.github/workflows/crowdin-download.yml`
This PR will be updated every day with new translations.
Due to a limitation in GitHub Actions, checks are not running on this PR without manual action.
If you want to run the checks, then please press “Approve workflows to run”.
branch: i18n/crowdin/translations
base: main
labels: i18n
-38
View File
@@ -1,38 +0,0 @@
name: Crowdin / Upload translations
on:
push:
branches:
- 'main'
- 'stable-*'
paths:
- crowdin.yml
- app/javascript/mastodon/locales/en.json
- config/locales/en.yml
- config/locales/simple_form.en.yml
- config/locales/activerecord.en.yml
- config/locales/devise.en.yml
- config/locales/doorkeeper.en.yml
- .github/workflows/crowdin-upload.yml
workflow_dispatch:
jobs:
upload-translations:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: crowdin action
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2
with:
upload_sources: true
upload_translations: false
download_translations: false
crowdin_branch_name: ${{ github.base_ref || github.ref_name }}
env:
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
-23
View File
@@ -1,23 +0,0 @@
name: Check formatting
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Check formatting
run: yarn format:check
-40
View File
@@ -1,40 +0,0 @@
name: CSS Linting
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- 'stylelint.config.js'
- '**/*.css'
- '**/*.scss'
- '.github/workflows/lint-css.yml'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- 'stylelint.config.js'
- '**/*.css'
- '**/*.scss'
- '.github/workflows/lint-css.yml'
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Stylelint
run: yarn lint:css --custom-formatter @csstools/stylelint-formatter-github
-44
View File
@@ -1,44 +0,0 @@
name: Haml Linting
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- '.github/workflows/lint-haml.yml'
- '.haml-lint*.yml'
- '.rubocop*.yml'
- '.ruby-version'
- '**/*.haml'
- 'Gemfile*'
pull_request:
paths:
- '.github/workflows/lint-haml.yml'
- '.haml-lint*.yml'
- '.rubocop*.yml'
- '.ruby-version'
- '**/*.haml'
- 'Gemfile*'
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BUNDLE_ONLY: development
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Ruby
uses: ruby/setup-ruby@6e5d382445ae5590b7449d8b3bc8cb1c2c27f617 # v1
with:
bundler-cache: true
- name: Run haml-lint
run: |
bin/haml-lint --reporter github
-49
View File
@@ -1,49 +0,0 @@
name: JavaScript Linting
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- '.nvmrc'
- 'eslint.config.mjs'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '.github/workflows/lint-js.yml'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- '.nvmrc'
- 'eslint.config.mjs'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '.github/workflows/lint-js.yml'
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: ESLint
run: yarn workspaces foreach --all --parallel run lint:js --max-warnings 0
- name: Typecheck
run: yarn typecheck
-49
View File
@@ -1,49 +0,0 @@
name: Ruby Linting
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- 'bin/rubocop'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
pull_request:
paths:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- 'bin/rubocop'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BUNDLE_ONLY: development
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Ruby
uses: ruby/setup-ruby@6e5d382445ae5590b7449d8b3bc8cb1c2c27f617 # v1
with:
bundler-cache: true
- name: Run rubocop
run: bin/rubocop --format github
- name: Run brakeman
if: always() # Run both checks, even if the first failed
run: bin/brakeman
-28
View File
@@ -1,28 +0,0 @@
name: PR Needs Rebase
on:
schedule:
- cron: '0 * * * *'
permissions:
pull-requests: write
jobs:
label-rebase-needed:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Check for merge conflicts
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3
with:
dirtyLabel: 'rebase needed :construction:'
repoToken: '${{ secrets.GITHUB_TOKEN }}'
commentOnClean: This pull request has resolved merge conflicts and is ready for review.
commentOnDirty: This pull request has merge conflicts that must be resolved before it can be merged.
retryMax: 30
continueOnMissingPermissions: false
-34
View File
@@ -1,34 +0,0 @@
name: Test container image build
on:
pull_request:
paths:
- .github/workflows/build-nightly.yml
- .github/workflows/build-push-pr.yml
- .github/workflows/build-releases.yml
- .github/workflows/test-image-build.yml
- Dockerfile
- streaming/Dockerfile
- .dockerignore
permissions:
contents: read
jobs:
build-image:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: Dockerfile
cache: true
build-image-streaming:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-streaming
cancel-in-progress: true
uses: ./.github/workflows/build-container-image.yml
with:
file_to_build: streaming/Dockerfile
cache: true
-44
View File
@@ -1,44 +0,0 @@
name: JavaScript Testing
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.snap'
- '.github/workflows/test-js.yml'
pull_request:
paths:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.snap'
- '.github/workflows/test-js.yml'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Clone repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: JavaScript testing
run: yarn test:js
-113
View File
@@ -1,113 +0,0 @@
name: Historical data migration test
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'Gemfile*'
- '.ruby-version'
- '**/*.rb'
- '.github/workflows/test-migrations.yml'
- 'lib/tasks/tests.rake'
- 'lib/tasks/db.rake'
pull_request:
paths:
- 'Gemfile*'
- '.ruby-version'
- '**/*.rb'
- '.github/workflows/test-migrations.yml'
- 'lib/tasks/tests.rake'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
postgres:
- 14-alpine
- 15-alpine
- 16-alpine
- 17-alpine
services:
postgres:
image: postgres:${{ matrix.postgres}}
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 6379:6379
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
RAILS_ENV: test
BUNDLE_CLEAN: true
BUNDLE_FROZEN: true
BUNDLE_WITHOUT: 'development:production'
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Ensure no errors with `db:prepare`
run: |
bin/rails db:drop
bin/rails db:prepare
bin/rails db:migrate
- name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS
run: |
bin/rails db:drop
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare
bin/rails db:migrate
- name: Test "one step migration" flow
run: |
bin/rails db:drop
bin/rails db:create
bin/rails tests:migrations:prepare_database
bin/rails db:migrate
bin/rails tests:migrations:check_database
- name: Test "two step migration" flow
run: |
bin/rails db:drop
bin/rails db:create
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database
# Migrate up to v4.3.0 breakpoint
bin/rails db:migrate VERSION=20241007071624
# Migrate the rest
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate
bin/rails db:migrate
bin/rails tests:migrations:check_database
-390
View File
@@ -1,390 +0,0 @@
name: Ruby Testing
on:
merge_group:
push:
branches:
- 'main'
- 'stable-*'
pull_request:
env:
BUNDLE_CLEAN: true
BUNDLE_FROZEN: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: build (production)
runs-on: ubuntu-latest
timeout-minutes: 15
env:
RAILS_ENV: production
BUNDLE_WITH: production
SECRET_KEY_BASE_DUMMY: 1
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
with:
onlyProduction: 'true'
- name: Cache assets from compilation
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
public/assets
public/packs
public/packs-test
tmp/cache/vite
key: assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: |
assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
assets-${{ github.head_ref || github.ref_name }}
assets-main
assets
- name: Precompile assets
run: |-
bin/rails assets:precompile
- name: Archive asset artifacts
run: |
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs*
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
path: |-
./artifacts.tar.gz
name: ${{ github.sha }}
retention-days: 0
test:
runs-on: ubuntu-latest
timeout-minutes: 60
needs:
- build
services:
postgres:
image: postgres:14-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 6379:6379
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }}
RAILS_ENV: test
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
OIDC_ENABLED: true
OIDC_SCOPE: read
SAML_ENABLED: true
CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test'
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
strategy:
fail-fast: false
matrix:
ruby-version:
- '3.3'
- '3.4'
- '.ruby-version'
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: './'
name: ${{ github.sha }}
- name: Expand archived asset artifacts
run: |
tar xvzf artifacts.tar.gz
- name: Set up Ruby environment
timeout-minutes: 15
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev
- name: Load database schema
run: |
bin/rails db:setup
bin/flatware fan bin/rails db:test:prepare
- name: Cache RSpec persistence file
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
tmp/rspec/examples.txt
key: rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: |
rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}-${{ matrix.ruby-version }}
rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
rspec-persistence-${{ github.head_ref || github.ref_name }}
rspec-persistence-main
rspec-persistence
- run: bin/flatware rspec
- name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
with:
files: coverage/lcov/*.lcov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-e2e:
name: End to End testing
runs-on: ubuntu-latest
timeout-minutes: 30
needs:
- build
services:
postgres:
image: postgres:14-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 6379:6379
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
RAILS_ENV: test
BUNDLE_WITH: test
LOCAL_DOMAIN: localhost:3000
LOCAL_HTTPS: false
strategy:
fail-fast: false
matrix:
ruby-version:
- '3.3'
- '3.4'
- '.ruby-version'
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: './'
name: ${{ github.sha }}
- name: Expand archived asset artifacts
run: |
tar xvzf artifacts.tar.gz
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
timeout-minutes: 15
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- name: Cache Playwright Chromium browser
id: playwright-cache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install Playwright Chromium browser (with deps)
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn run playwright install --with-deps chromium
- name: Install Playwright Chromium browser deps
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: yarn run playwright install-deps chromium
- run: bin/rspec spec/system --tag streaming --tag js
- name: Archive logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: failure()
with:
name: e2e-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: failure()
with:
name: e2e-screenshots-${{ matrix.ruby-version }}
path: tmp/capybara/
test-search:
name: Elastic Search integration testing
runs-on: ubuntu-latest
timeout-minutes: 30
needs:
- build
services:
postgres:
image: postgres:14-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 6379:6379
elasticsearch:
image: ${{ contains(matrix.search-image, 'elasticsearch') && matrix.search-image || '' }}
env:
discovery.type: single-node
xpack.security.enabled: false
options: >-
--health-cmd "curl http://localhost:9200/_cluster/health"
--health-interval 2s
--health-timeout 3s
--health-retries 50
ports:
- 9200:9200
opensearch:
image: ${{ contains(matrix.search-image, 'opensearch') && matrix.search-image || '' }}
env:
discovery.type: single-node
DISABLE_INSTALL_DEMO_CONFIG: true
DISABLE_SECURITY_PLUGIN: true
options: >-
--health-cmd "curl http://localhost:9200/_cluster/health"
--health-interval 2s
--health-timeout 3s
--health-retries 50
ports:
- 9200:9200
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
RAILS_ENV: test
BUNDLE_WITH: test
ES_ENABLED: true
ES_HOST: localhost
ES_PORT: 9200
strategy:
fail-fast: false
matrix:
ruby-version:
- '3.3'
- '3.4'
- '.ruby-version'
search-image:
- docker.elastic.co/elasticsearch/elasticsearch:7.17.29
include:
- ruby-version: '.ruby-version'
search-image: docker.elastic.co/elasticsearch/elasticsearch:8.19.2
- ruby-version: '.ruby-version'
search-image: opensearchproject/opensearch:2
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: './'
name: ${{ github.sha }}
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
timeout-minutes: 15
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg
- name: Set up Javascript environment
uses: ./.github/actions/setup-javascript
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- run: bin/rspec --tag search
- name: Archive logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: failure()
with:
name: test-search-logs-${{ matrix.ruby-version }}
path: log/
+43
View File
@@ -0,0 +1,43 @@
# Coffey Labs build of Mastodon
This repository is [Mastodon](https://github.com/mastodon/mastodon) with one
addition: the **coffeylabs** theme, the palette, type and blueprint grid of
coffeylabs.org. It is what runs on linuxexpert.org. Everything else is
upstream Mastodon, unchanged, under the same license (AGPL-3.0, see
[LICENSE](LICENSE)).
## How it tracks upstream
Upstream releases come in as snapshots, not as a merged history: the
`upstream` branch holds one unmodified "Import Mastodon vX.Y.Z" commit per
release, and each import is merged into `main` through a pull request.
## What is changed
Added (new files, Coffey Labs, 2026-10-06):
- `app/javascript/styles/coffeylabs.scss` and `app/javascript/styles/coffeylabs/`
-- the theme. It loads the stock stylesheet, then overrides Mastodon's color
tokens and adds the fonts and surfaces. Users can still pick Mastodon's
default theme in their appearance preferences.
- `app/javascript/fonts/coffeylabs/` -- Bricolage Grotesque and JetBrains Mono,
both under the SIL Open Font License (license files alongside).
- `.gitea/workflows/ci.yml` -- builds the image on every change and publishes
it from release tags.
- This file.
Modified (each carries a notice at the top):
- `config/themes.yml` -- registers the coffeylabs theme.
- `app/lib/themes.rb` -- the dark browser-bar color matches the theme.
- `Dockerfile` -- `SOURCE_BASE_URL` and `SOURCE_TAG` build arguments, so the
"View source code" link points at this repository.
Removed: upstream's `.github/workflows`, which run upstream's own CI.
## Releases
A tag named `v<upstream>-coffeylabs.<n>` (e.g. `v4.7.3-coffeylabs.1`)
publishes the `mastodon` image for linux/amd64 to this repository's
packages. The streaming server is unchanged, so use upstream's
`mastodon-streaming` image at the same upstream version.
+9
View File
@@ -1,5 +1,9 @@
# syntax=docker/dockerfile:1.18
# Modified by Coffey Labs, 2026-10-06: SOURCE_BASE_URL and SOURCE_TAG build
# arguments, so the "View source code" link points at this fork (see
# COFFEYLABS.md).
# This file is designed for production server deployment, not local development work
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker
@@ -32,6 +36,9 @@ ARG MASTODON_VERSION_PRERELEASE=""
ARG MASTODON_VERSION_METADATA=""
# Will be available as Mastodon::Version.source_commit
ARG SOURCE_COMMIT=""
# Where "View source code" points: the fork's repository and its tag
ARG SOURCE_BASE_URL=""
ARG SOURCE_TAG=""
# Allow Ruby on Rails to serve static files
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
@@ -51,6 +58,8 @@ ENV \
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
SOURCE_COMMIT="${SOURCE_COMMIT}" \
SOURCE_BASE_URL="${SOURCE_BASE_URL}" \
SOURCE_TAG="${SOURCE_TAG}" \
RAILS_SERVE_STATIC_FILES="${RAILS_SERVE_STATIC_FILES}" \
RUBY_YJIT_ENABLE="${RUBY_YJIT_ENABLE}" \
TZ="${TZ}"
@@ -0,0 +1,93 @@
Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) JetBrainsMono-Italic[wght].ttf: Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
// Coffey Labs theme: the stock stylesheet, then the coffeylabs.org palette,
// type and surfaces on top. Everything below overrides tokens or adds rules
// after the stock ones, so a Mastodon upgrade only needs a rebuild.
@use 'application';
@use 'coffeylabs/fonts';
@use 'coffeylabs/palette';
@use 'coffeylabs/surfaces';
@@ -0,0 +1,32 @@
// Bricolage Grotesque for display, JetBrains Mono for the wordmark, labels
// and numbers, the system sans for reading -- as on coffeylabs.org. Both
// faces are variable and OFL; licenses sit next to the font files.
@font-face {
font-family: 'Bricolage Grotesque';
font-style: normal;
font-weight: 200 800;
font-stretch: 75% 100%;
font-display: swap;
src: url('@/fonts/coffeylabs/bricolage-grotesque-latin-wght-normal.woff2')
format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100 800;
font-display: swap;
src: url('@/fonts/coffeylabs/jetbrains-mono-latin-wght-normal.woff2')
format('woff2');
}
html,
html[data-redesign='true'] {
--font-body:
ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Inter, Roboto,
'Helvetica Neue', Arial, sans-serif;
--font-heading: 'Bricolage Grotesque', ui-sans-serif, system-ui, sans-serif;
--font-monospace:
'JetBrains Mono', ui-monospace, 'Cascadia Code', 'SFMono-Regular', Consolas,
'Liberation Mono', monospace;
}
@@ -0,0 +1,89 @@
// coffeylabs.org palette. Mastodon builds its scheme tokens from the grey
// and indigo scales, so those become Coffey Labs' blue-slate and blue; the
// semantic tokens that need an exact coffeylabs.org value are set after.
html {
// Blue-slate, from the ground (#0C1017) up to the light ground (#EEF2F7)
--color-grey-50: #eef2f7;
--color-grey-100: #dce6f2;
--color-grey-200: #c3d0df;
--color-grey-300: #a7b8cc;
--color-grey-400: #8fa3bc;
--color-grey-500: #6b7c92;
--color-grey-600: #46586e;
--color-grey-700: #2f3d52;
--color-grey-800: #222d3c;
--color-grey-900: #131923;
--color-grey-950: #0c1017;
// Blue accent, #4EA8FF on dark and #1668C7 on light
--color-indigo-50: #eef6ff;
--color-indigo-100: #dceeff;
--color-indigo-200: #b9dcff;
--color-indigo-300: #7dc0ff;
--color-indigo-400: #4ea8ff;
--color-indigo-500: #2f8fef;
--color-indigo-600: #1668c7;
--color-indigo-700: #0f54a5;
--color-indigo-800: #0e4380;
--color-indigo-900: #0f3360;
--color-indigo-950: #0b1f3a;
// Status colors from coffeylabs.org: live green, warm orange
--color-green-400: #3ddc97;
--color-green-600: #0e8a5a;
--color-yellow-400: #ff8a00;
--color-yellow-700: #c75a00;
--cl-grid-line: rgb(78 168 255 / 6%);
--cl-glow: rgb(78 168 255 / 20%);
}
[data-color-scheme='dark'],
html:not([data-color-scheme]) {
--color-text-primary: #dce6f2;
--color-text-secondary: #8fa3bc;
--color-text-tertiary: #5f7288;
--color-text-brand: #4ea8ff;
--color-bg-primary: #0c1017;
--color-bg-secondary: #131923;
--color-bg-tertiary: #18202c;
// Accent buttons are bright blue with dark ink, as on coffeylabs.org
--color-bg-brand-base: #4ea8ff;
--color-bg-brand-base-hover: #7dc0ff;
--color-text-on-brand-base: #06101c;
--color-bg-brand-soft: #12304f;
--color-bg-brand-softest: #0f1d2e;
--color-border-brand-soft: #1f4b78;
--color-border-primary: #222d3c;
--dropdown-shadow:
0 1px 2px rgb(0 0 0 / 30%), 0 10px 30px -12px rgb(0 0 0 / 60%);
}
[data-color-scheme='light'] {
--color-text-primary: #0f1822;
--color-text-secondary: #46586e;
--color-text-tertiary: #6b7c92;
--color-text-brand: #1668c7;
--color-bg-primary: #fff;
--color-bg-secondary: #eef2f7;
--color-bg-tertiary: #e9eff6;
--color-bg-brand-base: #1668c7;
--color-bg-brand-base-hover: #0f54a5;
--color-text-on-brand-base: #fff;
--color-bg-brand-soft: #dceeff;
--color-bg-brand-softest: #eef6ff;
--color-border-primary: #d6dfea;
--dropdown-shadow:
0 1px 2px rgb(15 24 34 / 5%), 0 8px 24px -12px rgb(15 24 34 / 14%);
--cl-grid-line: rgb(22 104 199 / 7%);
--cl-glow: rgb(22 104 199 / 14%);
}
@@ -0,0 +1,108 @@
// Coffey Labs surfaces: the blueprint grid behind the page, the logo and
// wordmark in the navigation, mono for labels and numbers.
// The faint 32px grid with a glow where the eye starts, behind the columns
body {
background-color: var(--color-bg-primary);
background-image:
radial-gradient(
ellipse 60rem 40rem at 20% -10%,
var(--cl-glow),
transparent 60%
),
linear-gradient(var(--cl-grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--cl-grid-line) 1px, transparent 1px);
background-size:
100% 100%,
32px 32px,
32px 32px;
background-attachment: fixed;
}
.ui,
.columns-area__panels {
background: transparent;
}
::selection {
background: var(--color-bg-brand-base);
color: var(--color-text-on-brand-base);
}
// LINUXexpert mark and mono wordmark in place of the Mastodon wordmark
.column-link--logo {
display: flex;
align-items: center;
gap: 10px;
.logo--wordmark {
display: none;
}
&::before {
content: '';
flex: none;
width: 32px;
height: 32px;
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%20512%20512%27%3E%3Crect%20width%3D%27512%27%20height%3D%27512%27%20rx%3D%27104%27%20fill%3D%27%2314171D%27%2F%3E%3Crect%20x%3D%2710%27%20y%3D%2710%27%20width%3D%27492%27%20height%3D%27492%27%20rx%3D%2796%27%20fill%3D%27none%27%20stroke%3D%27%234FA7FF%27%20stroke-opacity%3D%270.18%27%20stroke-width%3D%274%27%2F%3E%3Cpolyline%20points%3D%27172%2C150%20284%2C256%20172%2C362%27%20fill%3D%27none%27%20stroke%3D%27%234FA7FF%27%20stroke-width%3D%2742%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%2F%3E%3Crect%20x%3D%27322%27%20y%3D%27308%27%20width%3D%2792%27%20height%3D%2748%27%20rx%3D%278%27%20fill%3D%27%234FA7FF%27%2F%3E%3C%2Fsvg%3E")
center / contain no-repeat;
}
&::after {
content: 'LINUXexpert';
font-family: var(--font-monospace);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text-primary);
}
}
// Display face for headings and names
.column-header__title,
.column-title h1,
.column-title h2,
.account__header__tabs__name h1,
.status__display-name strong,
.display-name__html {
font-family: var(--font-heading);
letter-spacing: -0.01em;
}
.column-header__title {
font-weight: 700;
}
// Mono for timestamps, counts and small labels
.status__relative-time,
.icon-button__counter,
.account__header__extra__links a,
.trends__item__current,
.notification-group__main__header__label,
.dismissable-banner__message,
.column-subheading {
font-family: var(--font-monospace);
font-size: 0.86em;
}
.column-subheading {
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: 600;
}
// Hashtags and mentions read as code, like links on coffeylabs.org
.hashtag-bar a,
.status__content a.hashtag {
font-family: var(--font-monospace);
font-size: 0.92em;
}
// Cards get the coffeylabs.org border and soft shadow
.status-card,
.dropdown-menu,
.search__popout,
.modal-root__modal {
border-color: var(--color-border-primary);
box-shadow: var(--dropdown-shadow);
}
+4 -1
View File
@@ -1,5 +1,8 @@
# frozen_string_literal: true
# Modified by Coffey Labs, 2026-10-06: the dark browser-bar color is the
# coffeylabs theme's ground (see COFFEYLABS.md).
require 'singleton'
require 'yaml'
@@ -7,7 +10,7 @@ class Themes
include Singleton
THEME_COLORS = {
dark: '#181820',
dark: '#0c1017',
light: '#ffffff',
}.freeze
+2
View File
@@ -1 +1,3 @@
# Modified by Coffey Labs, 2026-10-06: adds the coffeylabs theme (see COFFEYLABS.md).
default: styles/application.scss
coffeylabs: styles/coffeylabs.scss