Compare commits
29 Commits
develop
...
stable-3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8777ff5c85 | ||
|
|
bf1e58804f | ||
|
|
4e75f8ff35 | ||
|
|
dad0af4b86 | ||
|
|
692addcf2a | ||
|
|
2bfa144ce0 | ||
|
|
67889e5970 | ||
|
|
215bbe7884 | ||
|
|
bcdf306a75 | ||
|
|
f82ce99ca6 | ||
|
|
a5f314949e | ||
|
|
7ce3166cef | ||
|
|
a4ded74601 | ||
|
|
8212295b27 | ||
|
|
fd84f3a102 | ||
|
|
63c23f22de | ||
|
|
992e734c9e | ||
|
|
375c492931 | ||
|
|
19bb0240db | ||
|
|
528479cee6 | ||
|
|
40972b3fde | ||
|
|
d131f6c5d3 | ||
|
|
d79c85cfb6 | ||
|
|
02066c1a24 | ||
|
|
19b597132b | ||
|
|
566eab44f7 | ||
|
|
de2e8f4b8e | ||
|
|
16fc27bdd6 | ||
|
|
e0ff35cb14 |
@@ -1,2 +0,0 @@
|
||||
https://github.com/heroku/heroku-buildpack-nodejs.git#v371
|
||||
https://github.com/pkgr/heroku-buildpack-ruby.git#v361-1
|
||||
67
.circleci/config.yml
Normal file
67
.circleci/config.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
version: 2.1
|
||||
jobs:
|
||||
lint-scripts:
|
||||
docker:
|
||||
- image: koalaman/shellcheck-alpine
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: lint-scripts
|
||||
command: .circleci/lint-scripts.sh
|
||||
|
||||
install-mysql:
|
||||
docker:
|
||||
- image: circleci/ruby:2.5.5
|
||||
- image: circleci/mysql:5.7-ram
|
||||
command: --max_allowed_packet=64MB
|
||||
environment:
|
||||
MYSQL_DATABASE: zammad_test
|
||||
MYSQL_USER: zammad_test
|
||||
MYSQL_PASSWORD: zammad_test
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: install-mysql
|
||||
command: .circleci/install.sh
|
||||
|
||||
install-postgresql:
|
||||
docker:
|
||||
- image: circleci/ruby:2.5.5
|
||||
- image: circleci/postgres:11-ram
|
||||
environment:
|
||||
POSTGRES_DB: zammad_test
|
||||
POSTGRES_USER: zammad_test
|
||||
POSTGRES_PASSWORD: zammad_test
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: install-postgresql
|
||||
command: .circleci/install.sh
|
||||
|
||||
docker-image-build:
|
||||
machine: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: docker-image-build
|
||||
command: .circleci/docker-image-build.sh
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
install_unittest_dockerbuild:
|
||||
jobs:
|
||||
- lint-scripts
|
||||
- install-mysql:
|
||||
requires:
|
||||
- lint-scripts
|
||||
- install-postgresql:
|
||||
requires:
|
||||
- lint-scripts
|
||||
- docker-image-build:
|
||||
requires:
|
||||
- install-mysql
|
||||
- install-postgresql
|
||||
39
.circleci/docker-image-build.sh
Executable file
39
.circleci/docker-image-build.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# build zammads docker & docker-compose images
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
GITHUB_REPO_USER="zammad"
|
||||
ZAMMAD_VERSION="$(git describe --tags | sed -e 's/-[a-z0-9]\{8,\}.*//g')"
|
||||
export ZAMMAD_VERSION
|
||||
|
||||
if [ "${CIRCLE_BRANCH}" == 'develop' ]; then
|
||||
BUILD_SCRIPT="scripts/build_image.sh"
|
||||
DOCKER_REPOSITORY="zammad"
|
||||
export DOCKER_REPOSITORY
|
||||
GITHUB_REPOSITORY="zammad-docker"
|
||||
elif [ "${CIRCLE_BRANCH}" == 'stable' ]; then
|
||||
BUILD_SCRIPT="hooks/build"
|
||||
DOCKER_REPOSITORY="zammad-docker-compose"
|
||||
export DOCKER_REPOSITORY
|
||||
GITHUB_REPOSITORY="zammad-docker-compose"
|
||||
else
|
||||
echo "branch is ${CIRCLE_BRANCH}... no docker image build needed..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# dockerhub auth
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username="${DOCKER_USERNAME}" --password-stdin
|
||||
|
||||
# clone docker repo
|
||||
git clone https://github.com/"${GITHUB_REPO_USER}"/"${GITHUB_REPOSITORY}"
|
||||
|
||||
# enter dockerfile dir
|
||||
cd "${REPO_ROOT}/${GITHUB_REPOSITORY}"
|
||||
|
||||
# build & push docker image
|
||||
# shellcheck disable=SC1090
|
||||
source "${REPO_ROOT}/${GITHUB_REPOSITORY}/${BUILD_SCRIPT}"
|
||||
41
.circleci/install.sh
Executable file
41
.circleci/install.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# install & unit test zammad
|
||||
#
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
DB_CONFIG="test:\n adapter: postgresql\n database: zammad_test\n host: 127.0.0.1\n pool: 50\n timeout: 5000\n encoding: utf8\n username: zammad_test\n password: zammad_test"
|
||||
|
||||
# install build dependencies
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends autoconf automake autotools-dev bison build-essential curl git-core libffi-dev libgdbm-dev libgmp-dev libmariadbclient-dev-compat libncurses5-dev libreadline-dev libsqlite3-dev libssl-dev libtool libxml2-dev libxslt1-dev libyaml-0-2 libyaml-dev patch pkg-config postfix sqlite3 zlib1g-dev libimlib2 libimlib2-dev
|
||||
|
||||
if [ "${CIRCLE_JOB}" == "install-mysql" ]; then
|
||||
DB_ADAPTER="mysql2"
|
||||
INSTALL_OPTION="postgres"
|
||||
elif [ "${CIRCLE_JOB}" == "install-postgresql" ]; then
|
||||
DB_ADAPTER="postgresql"
|
||||
INSTALL_OPTION="mysql"
|
||||
else
|
||||
echo "nothing to do for circle ci job ${CIRCLE_JOB}..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# create db config
|
||||
echo -e "${DB_CONFIG}" | sed "s/adapter: postgresql/adapter: ${DB_ADAPTER}/g" > config/database.yml
|
||||
|
||||
# install zammad
|
||||
bundle install --without "${INSTALL_OPTION}"
|
||||
|
||||
# unit tests
|
||||
bundle exec rubocop
|
||||
rake db:migrate
|
||||
rake db:seed
|
||||
bundle exec rspec -t ~type:system -t ~searchindex
|
||||
bundle exec rake db:environment:set RAILS_ENV=test
|
||||
rake db:reset
|
||||
rake test:units
|
||||
ruby -I test/ test/integration/object_manager_test.rb
|
||||
ruby -I test/ test/integration/package_test.rb
|
||||
18
.circleci/lint-scripts.sh
Executable file
18
.circleci/lint-scripts.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# lint bash scripts
|
||||
#
|
||||
|
||||
set -o errexit
|
||||
|
||||
CONFIG_DIR="./.circleci"
|
||||
TMP_FILE="$(mktemp)"
|
||||
|
||||
find "${CONFIG_DIR}" -type f -name "*.sh" > "${TMP_FILE}"
|
||||
|
||||
while read -r FILE; do
|
||||
echo lint "${FILE}"
|
||||
shellcheck -x "${FILE}"
|
||||
done < "${TMP_FILE}"
|
||||
|
||||
rm "${TMP_FILE}"
|
||||
@@ -1 +0,0 @@
|
||||
../.dev/ai-agent-instructions.md
|
||||
@@ -1 +0,0 @@
|
||||
../../.dev/agents/code-reviewer.md
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
description: Start the full development workflow for a GitHub issue
|
||||
---
|
||||
|
||||
# Work on issue
|
||||
|
||||
Work on the GitHub issue: $ARGUMENTS
|
||||
|
||||
The arguments may include an optional `--parent <github_issue_url>` flag,
|
||||
indicating that this task is part of a larger story whose branch and MR
|
||||
already exist. Parse this flag from `$ARGUMENTS` if present; everything
|
||||
else is the task issue URL.
|
||||
|
||||
Follow the development lifecycle defined in
|
||||
`.dev/agent_docs/development_workflow.md`. Do not skip phases:
|
||||
|
||||
1. **Understand** — Read the issue thoroughly. Summarize the scope,
|
||||
acceptance criteria, and edge cases. Ask the user for clarification
|
||||
if anything is ambiguous. If `--parent` was supplied, also read the
|
||||
parent issue (it may be in a different repo) and list its sub-issues
|
||||
for context on sibling tasks.
|
||||
2. **Research** — Identify the affected areas of the codebase. Read
|
||||
the agent reference docs from `.dev/agent_docs/` that apply to
|
||||
those areas.
|
||||
3. **Plan** — Present a plan to the user: what files will change, the
|
||||
approach, potential side effects. Wait for approval before proceeding.
|
||||
|
||||
Stop after the Plan phase and wait for user approval before moving to
|
||||
Branch / Implement / Test / Commit / Review / MR.
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# Hook: lint all changed/new files with the appropriate tools.
|
||||
# Collects modified and untracked files from git, then runs the matching linters.
|
||||
|
||||
# Claude Code invokes this script outside of an interactive shell, so rv's
|
||||
# PROMPT_COMMAND-based version switching never runs; re-resolve it here.
|
||||
if command -v rv >/dev/null 2>&1; then
|
||||
RV_ENV=$(rv shell env bash) || { echo "rv shell env bash failed to resolve the Ruby environment" >&2; exit 2; }
|
||||
eval "$RV_ENV"
|
||||
fi
|
||||
|
||||
RUBY_FILES=()
|
||||
FRONTEND_TS_FILES=()
|
||||
FRONTEND_JS_FILES=()
|
||||
COFFEESCRIPT_FILES=()
|
||||
STYLE_FILES=()
|
||||
MARKDOWN_FILES=()
|
||||
EXIT_CODE=0
|
||||
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
case "$file" in
|
||||
*.rb) RUBY_FILES+=("$file") ;;
|
||||
*.ts|*.vue) FRONTEND_TS_FILES+=("$file") ;;
|
||||
# Legacy/vendored assets are third-party code and outside the scope of
|
||||
# `pnpm lint:js`, which only covers app/frontend/ and .eslint-plugin-zammad/.
|
||||
*.js) [[ "$file" == public/* || "$file" == app/assets/javascripts/* ]] || FRONTEND_JS_FILES+=("$file") ;;
|
||||
*.coffee) COFFEESCRIPT_FILES+=("$file") ;;
|
||||
*.scss|*.css) STYLE_FILES+=("$file") ;;
|
||||
*.md) MARKDOWN_FILES+=("$file") ;;
|
||||
esac
|
||||
done < <(git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
|
||||
|
||||
FRONTEND_ALL_FILES=("${FRONTEND_TS_FILES[@]}" "${FRONTEND_JS_FILES[@]}")
|
||||
|
||||
if [[ ${#RUBY_FILES[@]} -gt 0 ]]; then
|
||||
bundle exec rubocop --autocorrect "${RUBY_FILES[@]}" >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
if [[ ${#FRONTEND_ALL_FILES[@]} -gt 0 ]]; then
|
||||
# Generated files (e.g. the GraphQL types) are excluded by the lint configs, so
|
||||
# a changeset consisting only of those must not be treated as an error.
|
||||
{ pnpm lint:js:oxlint:cmd --no-error-on-unmatched-pattern --fix "${FRONTEND_ALL_FILES[@]}" && \
|
||||
pnpm lint:js:eslint:cmd --fix "${FRONTEND_ALL_FILES[@]}" && \
|
||||
{ pnpm format:cmd "${FRONTEND_ALL_FILES[@]}"; _fmt_rc=$?; (( _fmt_rc == 0 || _fmt_rc == 2 )); }; } >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
if [[ ${#FRONTEND_TS_FILES[@]} -gt 0 ]]; then
|
||||
pnpm lint:ts >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
if [[ ${#COFFEESCRIPT_FILES[@]} -gt 0 ]]; then
|
||||
coffeelint --reporter=csv --rules ./.dev/coffeelint/rules/detect_translatable_string.coffee "${COFFEESCRIPT_FILES[@]}" >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
if [[ ${#STYLE_FILES[@]} -gt 0 ]]; then
|
||||
pnpm lint:css:cmd --fix "${STYLE_FILES[@]}" >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
if [[ ${#MARKDOWN_FILES[@]} -gt 0 ]]; then
|
||||
pnpm lint:md:cmd --fix "${MARKDOWN_FILES[@]}" >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
# Hook: regenerate generated files when their sources change.
|
||||
|
||||
# Claude Code invokes this script outside of an interactive shell, so rv's
|
||||
# PROMPT_COMMAND-based version switching never runs; re-resolve it here.
|
||||
# Needed before the pnpm generate-* calls below, which shell out to
|
||||
# `bundle exec rails generate ...` and inherit this process's env.
|
||||
if command -v rv >/dev/null 2>&1; then
|
||||
RV_ENV=$(rv shell env bash) || { echo "rv shell env bash failed to resolve the Ruby environment" >&2; exit 2; }
|
||||
eval "$RV_ENV"
|
||||
fi
|
||||
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
|
||||
|
||||
NEEDS_GRAPHQL=false
|
||||
NEEDS_SETTINGS=false
|
||||
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
case "$file" in
|
||||
app/graphql/*) NEEDS_GRAPHQL=true ;;
|
||||
app/frontend/*.graphql) NEEDS_GRAPHQL=true ;;
|
||||
app/models/setting.rb) NEEDS_SETTINGS=true ;;
|
||||
db/seeds/settings.rb) NEEDS_SETTINGS=true ;;
|
||||
esac
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
if $NEEDS_GRAPHQL; then
|
||||
echo "GraphQL schema changed — regenerating types..." >&2
|
||||
pnpm generate-graphql-api >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
if $NEEDS_SETTINGS; then
|
||||
echo "Settings changed — regenerating types..." >&2
|
||||
pnpm generate-setting-types >&2 || EXIT_CODE=2
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git log *)",
|
||||
"Bash(git show *)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git blame *)",
|
||||
"Bash(git branch)",
|
||||
"Bash(git branch --show-current)",
|
||||
"Bash(git status *)",
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git cherry-pick *)",
|
||||
"Bash(glab *)",
|
||||
"Bash(find *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(bundle exec rubocop *)",
|
||||
"Bash(RAILS_ENV=test VITE_TEST_MODE=1 bundle exec rspec *)",
|
||||
"Bash(pnpm test *)",
|
||||
"Bash(pnpm lint *)",
|
||||
"Bash(pnpm generate-graphql-api)",
|
||||
"Bash(pnpm generate-setting-types)",
|
||||
"mcp__github__issue_read",
|
||||
"mcp__github__list_issue_comments",
|
||||
"mcp__codegraph__*"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/lint-after-edit.sh",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/regenerate-after-edit.sh",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "pnpm exec codegraph prompt-hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
44
.codeclimate.yml
Normal file
44
.codeclimate.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
engines:
|
||||
brakeman:
|
||||
enabled: true
|
||||
bundler-audit:
|
||||
enabled: true
|
||||
csslint:
|
||||
enabled: true
|
||||
coffeelint:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- ruby
|
||||
- javascript
|
||||
eslint:
|
||||
enabled: true
|
||||
fixme:
|
||||
enabled: true
|
||||
phpmd:
|
||||
enabled: false
|
||||
rubocop:
|
||||
enabled: true
|
||||
ratings:
|
||||
paths:
|
||||
- Gemfile.lock
|
||||
- "**.erb"
|
||||
- "**.haml"
|
||||
- "**.rb"
|
||||
- "**.rhtml"
|
||||
- "**.slim"
|
||||
- "**.css"
|
||||
- "**.coffee"
|
||||
- "**.inc"
|
||||
- "**.js"
|
||||
- "**.jsx"
|
||||
- "**.module"
|
||||
exclude_paths:
|
||||
- config/
|
||||
- db/
|
||||
- script/
|
||||
- test/
|
||||
- vendor/
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
inheritance: true
|
||||
reviews:
|
||||
review_status: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
base_branches:
|
||||
- develop
|
||||
ignore_usernames:
|
||||
- renovatebot
|
||||
- zammad-weblate
|
||||
drafts: false
|
||||
2
.csslintrc
Normal file
2
.csslintrc
Normal file
@@ -0,0 +1,2 @@
|
||||
--exclude-exts=.min.css
|
||||
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
|
||||
@@ -1,353 +0,0 @@
|
||||
# Zammad Review Rules
|
||||
|
||||
Single source of truth for how an AI review decides what to flag, and how it says it.
|
||||
|
||||
This file is **tool-neutral on purpose**. It says nothing about `glab`, worktrees, GitLab APIs,
|
||||
approval gates or output transport — those belong to whichever caller is using these rules.
|
||||
|
||||
It is also **not a duplicate of the coding conventions**. Its sibling files in this directory, and
|
||||
`.dev/ai-agent-instructions.md`, are the conventions; this file tells a reviewer which of them to
|
||||
read and how to turn a violation into a useful comment. When a convention changes, it changes there.
|
||||
|
||||
## Consumers
|
||||
|
||||
| Consumer | Lives in | Uses these rules for |
|
||||
| ------------------------------- | ----------- | ------------------------------------------- |
|
||||
| `.dev/agents/code-reviewer.md` | core repo | Reviewing changes in a fresh context |
|
||||
| `code-review` skill | skills repo | Local review, delegates to the agent above |
|
||||
| `review-mr` skill | skills repo | Reviewing a GitLab MR interactively |
|
||||
|
||||
When a rule changes, it changes **here** and every consumer picks it up. Do not copy rule text into
|
||||
a consumer.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Read the conventions for what the change touches
|
||||
|
||||
Before reviewing, identify which areas the diff touches and read **only** the applicable reference
|
||||
docs. Do not read them all upfront, and do not review conventions from memory — these files are the
|
||||
authority, and they change.
|
||||
|
||||
| The diff touches… | Read |
|
||||
| ----------------------------------------------------- | --------------------------------- |
|
||||
| GraphQL types, mutations, queries, subscriptions | `graphql_patterns.md` |
|
||||
| Knowledge base code in the new stack | `knowledge_base_patterns.md` |
|
||||
| Vue components, composables, routing, the form system | `frontend_patterns.md` |
|
||||
| Addon extensions to the Vue frontend | `frontend_addon_layer.md` |
|
||||
| New models used by the legacy CoffeeScript stack | `legacy_model_patterns.md` |
|
||||
| Specs, test setup, test helpers | `testing.md` |
|
||||
| Specs for a service or its GraphQL caller | `testing_services_and_graphql.md` |
|
||||
| Service objects (`app/services/service/`) | `service_patterns.md` |
|
||||
| Migrations or seeds | `database_migrations.md` |
|
||||
|
||||
A convention violation is a finding. "The surrounding code doesn't follow it either" is not a
|
||||
defence — if the doc says it, the change should do it.
|
||||
|
||||
`.dev/ai-agent-instructions.md` carries the repo-wide rules that apply regardless of area:
|
||||
|
||||
- Every new file except Markdown files (`*.md`) needs the Zammad copyright header.
|
||||
- `i18n/*.po` is never edited directly — translations come from translations.zammad.org.
|
||||
- New features target Vue 3 + GraphQL. A new feature built on REST + CoffeeScript is worth
|
||||
questioning unless it is extending existing legacy behaviour.
|
||||
|
||||
## Step 2 — Decide what is worth flagging
|
||||
|
||||
Precision matters more than recall. A review that reports five real problems is more useful than one
|
||||
that reports five real problems buried in fifteen speculative ones, because the second teaches
|
||||
people to skim.
|
||||
|
||||
Flag something only when **all** of these hold:
|
||||
|
||||
1. It meaningfully impacts correctness, performance, security or maintainability.
|
||||
2. It is discrete and actionable — one specific problem, not "this area is messy" or a bundle of
|
||||
related complaints.
|
||||
3. It matches the rigor Zammad actually holds, which is high. A missing spec on new public
|
||||
behaviour, an unchecked authorization path, or a bug fix without a regression test **is** a
|
||||
finding — do not excuse it because neighbouring code is no better.
|
||||
4. It was introduced by this change. Pre-existing problems are out of scope.
|
||||
5. The author would plausibly fix it if they knew. If they would reasonably shrug, it is not a
|
||||
finding.
|
||||
6. It does not rest on unstated assumptions about the codebase or the author's intent.
|
||||
7. It is not speculation. "This might break something elsewhere" is not a finding unless you can
|
||||
name the affected code and show that it is affected.
|
||||
8. It is clearly not a deliberate choice by the author.
|
||||
|
||||
**On volume:** report every finding that meets the bar — do not stop at the first one. If nothing
|
||||
meets the bar, report nothing. "No findings" is a legitimate and useful outcome; inventing issues to
|
||||
make a review look thorough is not.
|
||||
|
||||
Also out of scope in every review:
|
||||
|
||||
- Style and formatting a linter or formatter owns (RuboCop, ESLint, oxlint, Oxfmt, Stylelint),
|
||||
unless it actively obscures meaning or breaks a documented standard.
|
||||
- Naming preferences not covered by a convention doc.
|
||||
- Speculative future requirements that are not in the issue.
|
||||
- Pre-existing code visible in diff context but untouched by the change.
|
||||
|
||||
Verify before reporting: read the actual code, not just the diff hunk. Prefer evidence over
|
||||
inference — if a working checkout is available, grep for related call sites and tests.
|
||||
|
||||
An honest "I cannot tell from the code" is a useful answer. Never manufacture confidence.
|
||||
|
||||
## Step 3 — Classify
|
||||
|
||||
Every finding gets **a category and a severity**. They are independent: the category says what kind
|
||||
of problem it is, the severity says how much it matters.
|
||||
|
||||
**Category** (labels the type of finding):
|
||||
|
||||
| Prefix | Meaning |
|
||||
| ------------ | ------------------------------------------------------------------ |
|
||||
| `[security]` | Security issue introduced by the change |
|
||||
| `[issue]` | Divergence from the linked issue's use cases or acceptance criteria |
|
||||
| `[ui]` | Frontend convention regression |
|
||||
| `[backend]` | Backend convention or correctness concern |
|
||||
| `[test]` | Missing or inadequate test coverage for new behaviour |
|
||||
| `[cleanup]` | Leftover artifacts, dead code, unnecessary complexity |
|
||||
|
||||
**Severity** (drives grouping and ordering):
|
||||
|
||||
| Level | Meaning |
|
||||
| ----- | ------------------------------------------------------------------------------------ |
|
||||
| P0 | Blocks. Security issue, broken functionality, missing authorization, data loss risk. |
|
||||
| P1 | Should fix. Bugs, missing error handling, missing tests, convention violations. |
|
||||
| P2 | Consider. Minor improvements, alternative approaches. |
|
||||
|
||||
Write them together: `[security] P0`, `[ui] P2`. Present findings grouped by severity in the
|
||||
order P0, P1, then P2.
|
||||
|
||||
## Step 4 — Write the comment
|
||||
|
||||
1. Be clear about **why** it is a problem, not just that it is one.
|
||||
2. **State the preconditions up front** — the inputs, environment or scenario needed for the problem
|
||||
to occur, so the reader can judge severity immediately rather than inferring it.
|
||||
3. Communicate severity accurately. Never inflate it.
|
||||
4. One paragraph. The author should grasp it without close reading.
|
||||
5. Be specific and actionable. Never vague feedback like "this could be better" — say what to do.
|
||||
6. No code chunks longer than ~3 lines. Wrap code in inline code tags or a fenced block.
|
||||
7. Matter-of-fact tone. Not accusatory, not congratulatory. No "Great job…", no "Thanks for…".
|
||||
8. Anchor to the tightest line range that pinpoints the problem — 5–10 lines at most. Do not restate
|
||||
the location in the body; the anchor already carries it.
|
||||
9. One comment per distinct problem. Do not merge two issues into one, and do not post the same
|
||||
issue at two locations.
|
||||
|
||||
### Suggestion blocks
|
||||
|
||||
Where the fix is mechanical, offer it as a suggestion the author can apply in one click:
|
||||
|
||||
````markdown
|
||||
```suggestion:-0+0
|
||||
the replacement line(s)
|
||||
```
|
||||
````
|
||||
|
||||
- Preserve the **exact** leading whitespace of the replaced lines — spaces vs tabs, and the count.
|
||||
- Do not add or remove an indentation level unless that is the actual fix.
|
||||
- Nothing but replacement code inside the block. Explanation goes in the paragraph above it.
|
||||
- `-N+M` extends the range N lines above and M lines below the anchored line; `-0+0` replaces just
|
||||
the anchored line. This differs from GitHub's suggestion syntax.
|
||||
- Only when the fix is genuinely mechanical. If it needs a judgement call, describe it instead — a
|
||||
wrong one-click fix is worse than prose.
|
||||
|
||||
## Step 5 — Report what you could not judge
|
||||
|
||||
Step 2 is precision-first on purpose: no speculation, nothing resting on unstated assumptions. That
|
||||
makes "I cannot tell from the code" an honest and expected outcome — and since it produces no
|
||||
finding, it is lost unless it has somewhere to go. It is often the most useful thing a reviewer
|
||||
knows.
|
||||
|
||||
Report it separately from the findings: **at most three items**, each naming a place and the
|
||||
question a human should answer there. Draw only on what the review actually produced:
|
||||
|
||||
- An `Unverifiable` acceptance criterion.
|
||||
- A specific uncertainty with a named file — not "this might break something elsewhere".
|
||||
- Something the change cannot show on its own: migration timing on a large installation, a rendered
|
||||
result, `i18n/*.po` content, binary assets.
|
||||
|
||||
Two rules keep it honest:
|
||||
|
||||
- **Never park a finding here.** If it meets the bar in Step 2 it is a finding, with an anchor and a
|
||||
severity. Moving a real problem into this list hides it from whoever has to fix it.
|
||||
- **No items is the normal case.** Then write nothing — not a reassurance that there is nothing.
|
||||
An open-ended list of things to double-check is a "check everything" disclaimer, and it trains
|
||||
people to skim, for exactly the reason Step 2 exists.
|
||||
|
||||
Where this surfaces is the caller's business: a reviewer working in a terminal can print it for the
|
||||
person running the review, while one posting to a merge request may deliberately keep it out of the
|
||||
thread. It is guidance for the human reviewing, not a comment for the author.
|
||||
|
||||
---
|
||||
|
||||
## What to check
|
||||
|
||||
### Correctness
|
||||
|
||||
Logic errors, edge cases, off-by-ones, incorrect API usage, broken assumptions — anything that would
|
||||
cause wrong behaviour or a crash. Also: **unintended side effects** — does the change affect code
|
||||
paths that were not part of the plan?
|
||||
|
||||
### Security — `[security]`
|
||||
|
||||
OWASP-style issues **introduced by the change**:
|
||||
|
||||
- SQL / command / template injection
|
||||
- XSS, unsafe raw HTML rendering
|
||||
- Missing authorization or authentication checks (Pundit policies in `app/policies/`)
|
||||
- Insecure deserialization
|
||||
- Path traversal, SSRF
|
||||
- Hardcoded secrets or credentials
|
||||
- Weak or missing input validation at trust boundaries
|
||||
- Unsafe use of `eval`, `system`, raw SQL, `send`/`public_send`, `constantize`/`safe_constantize`
|
||||
on user-influenced input
|
||||
|
||||
### Issue alignment — `[issue]`
|
||||
|
||||
Only when a linked issue was found. Does the change cover the use cases the issue describes, and the
|
||||
edge cases implied by them?
|
||||
|
||||
Acceptance criteria get their own pass below — do not fold them in here.
|
||||
|
||||
#### Acceptance criteria pass
|
||||
|
||||
When the linked issue lists acceptance criteria, this is a **closed checklist, not an open search**.
|
||||
Work through it item by item rather than forming a general impression of whether the change "does
|
||||
the right thing".
|
||||
|
||||
This pass is **exempt from the flagging threshold**: every criterion gets a verdict whether or not it
|
||||
produces a finding.
|
||||
|
||||
- Enumerate **every** criterion verbatim, in the issue's own order. Do not merge, reword, skip or
|
||||
summarise any of them — not even ones that look trivially satisfied.
|
||||
- Give each criterion exactly one verdict:
|
||||
- **Met** — demonstrably implemented. Cite the `file:line` that satisfies it.
|
||||
- **Not met** — not implemented, or only partially. Each becomes an `[issue]` finding.
|
||||
- **Unverifiable** — cannot be judged from code alone (needs manual testing, a screenshot, a
|
||||
design review, or depends on code outside the change). State _why_. Never invent a Met/Not met
|
||||
verdict just to avoid this outcome.
|
||||
- Report as a table **every time**, including when everything is met — "all criteria verified" is
|
||||
itself the answer the reviewer needs, not merely an absence of findings.
|
||||
|
||||
| # | Acceptance criterion | Verdict | Evidence |
|
||||
| --- | -------------------- | ------------ | ----------------- |
|
||||
| 1 | <verbatim text> | Met | `app/foo.rb:42` |
|
||||
| 2 | <verbatim text> | Not met | not addressed |
|
||||
| 3 | <verbatim text> | Unverifiable | needs manual test |
|
||||
|
||||
- If the linked issue has **no** acceptance criteria, say so in one line and move on. Do not invent
|
||||
criteria from the issue's prose.
|
||||
- A change with no findings but an unaddressed or unverifiable criterion is **not** clean.
|
||||
|
||||
#### Use-case cross-check
|
||||
|
||||
Criteria and use cases are written at different moments and drift apart. A criteria list can pass
|
||||
completely while the story it was meant to describe is only half covered — so after the criteria
|
||||
pass, map the issue's use cases / user stories onto its criteria.
|
||||
|
||||
Like the criteria pass, this is a **closed mapping between two lists the issue already contains**,
|
||||
not a fresh search for behaviour the issue never asked for. Never invent a use case from prose.
|
||||
|
||||
Give each use case exactly one verdict:
|
||||
|
||||
| Verdict | Meaning |
|
||||
| ----------------- | ---------------------------------------------------------------------- |
|
||||
| Covered | a criterion asserts its observable outcome — cite the criterion numbers |
|
||||
| Partially covered | an edge case, error path or role variant it describes has no criterion |
|
||||
| Not covered | no criterion touches it |
|
||||
| Conflicts | a criterion asserts something the use case contradicts |
|
||||
|
||||
What a gap means depends on the change, and the distinction decides who has work to do:
|
||||
|
||||
- **Not covered, but the change implements it** — not a finding. The change is fine; the issue's
|
||||
criteria list is incomplete. Report it as that, and never attach it to the author's lines.
|
||||
- **Not covered, and the change does not implement it** — an `[issue]` finding. This is the case
|
||||
the cross-check exists for: it is invisible to the criteria pass, because there is no criterion
|
||||
to fail.
|
||||
- **Partially covered** — apply the same two cases to the uncovered aspect alone: an `[issue]`
|
||||
finding when the change does not implement it, a gap in the criteria list when it does. Do not
|
||||
re-litigate the part the criteria already cover.
|
||||
- **Conflicts** — stop and ask. A self-contradictory issue is not resolved by quietly picking
|
||||
whichever reading matches the change.
|
||||
|
||||
If the issue lists use cases but **no** acceptance criteria, there is no second list to map onto, so
|
||||
the verdicts above do not apply — every one of them is defined against a criterion. Run the
|
||||
acceptance-criteria pass over the use cases instead, exactly as written: each use case verbatim, one
|
||||
verdict each (Met / Not met / Unverifiable), evidence as `file:line`, table every time. Label the
|
||||
table as use cases, not criteria, so nobody reads it as something the issue asserted. Use cases are
|
||||
descriptive rather than assertive, so a verdict against one is weaker evidence than a verdict
|
||||
against a criterion — but dropping them entirely is worse. A contradiction _between_ two use cases
|
||||
is still a stop-and-ask.
|
||||
|
||||
### UI principles — `[ui]`
|
||||
|
||||
Only when the change touches Vue components/templates, stylesheets, or translatable strings. These
|
||||
expand the condensed list in `.dev/ai-agent-instructions.md`; read `frontend_patterns.md` for
|
||||
everything else about Vue conventions.
|
||||
|
||||
They regress silently because the component still "works" — it just degrades accessibility, layout,
|
||||
or translation quality for some users.
|
||||
|
||||
- **Icon-only action buttons** — a button/link whose only content is an icon (no visible text label)
|
||||
must carry a `v-tooltip` directive that also supplies the accessible name (e.g. an `aria-label`
|
||||
binding, or tooltip text that doubles as the label). Without it, screen reader users get an
|
||||
unlabeled control.
|
||||
- **Hover/focus-only actions on touch devices** — elements revealed only via CSS `:hover` or
|
||||
`:focus-visible` (e.g. row actions appearing on table row hover) must still be reachable on touch
|
||||
devices, which have no hover state. Flag unless there is a fallback (a `(hover: hover)` /
|
||||
`(pointer: coarse)` media query, or the action is always shown at narrow viewports).
|
||||
- **Container-based responsive grids** — a grid should adapt to the space actually available to it
|
||||
(its container), not just the viewport: CSS Grid with `repeat(auto-fit, minmax(...))` or container
|
||||
queries (`@container`), rather than a fixed column count that only changes at global viewport
|
||||
breakpoints. Flag grids that would break or waste space inside sidebars, modals or split panes.
|
||||
- **Translatable strings** — new or changed user-facing strings (`$t('...')` / `i18n.t('...')`,
|
||||
locale keys) should:
|
||||
- be complete and meaningful, avoiding single standalone words (e.g. `t('Name')`), since
|
||||
translators lack the surrounding context to disambiguate them;
|
||||
- use sentence case for English source strings (only the first word and proper nouns
|
||||
capitalised), not Title Case;
|
||||
- not duplicate an existing string. Before accepting a _new_ string, grep for the literal text in
|
||||
other `$t(...)` / `i18n.t(...)` calls or the base locale file. If a near-identical string
|
||||
exists, flag the new one as a duplicate and suggest reusing it — every distinct string adds
|
||||
translator workload across all supported languages.
|
||||
- **Danger styling and confirmations** — `danger` and `remove` (which counts as danger) mark the
|
||||
click that cannot be undone, so they stay only on controls that act immediately. Flag a `danger`
|
||||
or `remove` trigger whose handler ends in `waitForConfirmation` / `waitForVariantConfirmation`,
|
||||
or opens a flyout that asks first. What replaces it depends on the kind of trigger: a `danger`
|
||||
button becomes `tertiary` with an icon naming the action (`trash3` for delete and discard,
|
||||
`arrow-counterclockwise` for reset); a menu item carries no variant at all, icon included; a
|
||||
`remove` icon button loses the variant, keeps its icon, and takes the variant UX decides per
|
||||
place (`tertiary` so far), so do not assume one. Flag a confirmation for irrecoverable loss
|
||||
(deleting records, resetting user settings, discarding unsaved changes, clearing filters or
|
||||
recent lists) whose confirm button is not `danger`: the `delete` and `unsaved` variants of
|
||||
`CommonConfirmationDialog` already render one, so only the default `confirm` variant needs
|
||||
`buttonVariant: 'danger'`. A control that asks only in some states (a dirty taskbar tab) follows
|
||||
the state.
|
||||
- Never flag a change to `i18n/*.po` as the fix. Those files are managed externally.
|
||||
|
||||
### Backend — `[backend]`
|
||||
|
||||
The conventions live in the sibling files routed in Step 1 — read those rather than reviewing Rails
|
||||
practice from memory. Beyond them, the recurring concerns worth checking:
|
||||
|
||||
- **Query shape** — new associations rendered in a loop without `includes`/`preload` (N+1), a column
|
||||
introduced for lookup or filtering without an index, queries inside view or serializer code.
|
||||
- **Authorization** — new controller actions, GraphQL fields or mutations that reach data without
|
||||
going through a Pundit policy. This is `[security] P0`, not `[backend]`, when data is exposed.
|
||||
- **Background jobs** — jobs that are not idempotent, that carry state across a retry, or that
|
||||
assume the record still exists when they run.
|
||||
|
||||
### Tests — `[test]`
|
||||
|
||||
New public behaviour should have tests: new public methods, new components, new endpoints, new
|
||||
GraphQL fields. A bug fix should have a regression test that fails without the fix.
|
||||
|
||||
`testing.md` covers how tests are written and run here. Flag the gap; do not write
|
||||
the test as part of a review.
|
||||
|
||||
### Leftover artifacts and cleanup — `[cleanup]`
|
||||
|
||||
- Debug code, `console.log`, `binding.pry`, commented-out blocks, TODOs introduced by this change.
|
||||
- Dead code the change orphaned — a method that is now unused, an import that is now unreferenced.
|
||||
- Obvious duplication introduced by the change, and unnecessary complexity.
|
||||
|
||||
This matters most when reviewing an agent's own output, where scaffolding tends to survive into the
|
||||
diff.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Database Migrations & Seeds
|
||||
|
||||
## Creating a Migration
|
||||
|
||||
```bash
|
||||
rails generate migration IssueNumberShortDescription
|
||||
```
|
||||
|
||||
The generator template automatically includes the `system_init_done` guard
|
||||
(`return if !Setting.exists?(name: 'system_init_done')`), which skips
|
||||
data-migration logic on fresh installations where seeds handle setup.
|
||||
Zammad migrations can include data migrations, not just schema changes.
|
||||
|
||||
- Call `Model.reset_column_information` after adding/removing columns
|
||||
in the same migration
|
||||
|
||||
## Schema Changes
|
||||
|
||||
When modifying table structure, also update the base migrations
|
||||
so fresh installations get the same schema (migrations are cumulative,
|
||||
but base files are the canonical schema source for new installs):
|
||||
|
||||
- `db/migrate/20120101000001_create_base.rb`
|
||||
- `db/migrate/20120101000010_create_ticket.rb`
|
||||
|
||||
Conventions:
|
||||
|
||||
- Use `limit: 3` for timestamp precision
|
||||
- Specify `type: :integer` for polymorphic references
|
||||
- Use `id: :integer` in `create_table` calls (Zammad uses integer primary keys, not bigint)
|
||||
- Add indexes for frequently queried columns
|
||||
|
||||
## Settings
|
||||
|
||||
New or updated settings must be added in both the migration and the seeds.
|
||||
See existing examples in `db/seeds/settings.rb` for the correct structure.
|
||||
|
||||
## Seeds
|
||||
|
||||
Seeds live in `db/seeds/` and use idempotent methods like
|
||||
`create_if_not_exists` to avoid duplicates. When adding user-facing fields,
|
||||
also update `db/seeds/object_manager_attributes.rb`.
|
||||
|
||||
## Helpers
|
||||
|
||||
`MigrationHelper` (`lib/migration_helper.rb`) provides utilities for
|
||||
renaming custom object attributes and handling reserved SQL words.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
rake db:migrate # Test the migration
|
||||
rake db:rollback STEP=1 # Undo last migration (schema only — data migrations are not reversed)
|
||||
```
|
||||
@@ -1,191 +0,0 @@
|
||||
# Development Workflow
|
||||
|
||||
## Issue Tracking & Code Hosting
|
||||
|
||||
- Issues (bugs, stories, epics) are tracked on **GitHub** — in `zammad/zammad`
|
||||
(public) and private repositories (e.g. `zammad/coordination-desktop-view`).
|
||||
- Code is developed on a **self-hosted GitLab** instance.
|
||||
- GitHub hosts a **read-only mirror** — synced automatically when public branches
|
||||
like `develop` or `stable` are updated.
|
||||
- Branches prefixed with `private/` are **never synced** to GitHub.
|
||||
|
||||
All branches must start with `private/` to prevent unintended syncing.
|
||||
|
||||
## Tools
|
||||
|
||||
- **GitHub MCP server** — for reading issues, labels, and comments.
|
||||
- **`glab` CLI** — for pushing branches, creating merge requests, and other
|
||||
GitLab operations. Authenticated against the self-hosted GitLab instance.
|
||||
|
||||
## Before the lifecycle: the story
|
||||
|
||||
The lifecycle below starts at "read the GitHub issue" and assumes a task
|
||||
issue already exists that says something useful. Getting there is its own
|
||||
stage, and it acts on a **story** rather than on a task:
|
||||
|
||||
1. **Refine** — `/refine-story`. Checks whether the story can be planned at
|
||||
all, whether it is really several stories, and whether its acceptance
|
||||
criteria, its use cases and its linked design agree. Proposes the missing
|
||||
criteria and sections, and writes them to the story on confirmation.
|
||||
2. **Break down** — `/plan-story`. Cuts a ready story into `Task` and `Spike`
|
||||
sub-issues: a task carries the plan of how to solve it, a spike the question
|
||||
it has to answer. Refuses to plan a story that is not ready.
|
||||
3. **Present** — `/present-story`. Explains the prepared work in a sprint
|
||||
planning. Reads only.
|
||||
|
||||
The rules all three follow — the definition of ready, how story size is
|
||||
judged, the closed list of layers a plan must give a verdict on, and how deep
|
||||
a plan may go — are in `.dev/agent_docs/story_planning_rules.md`, the
|
||||
counterpart of `code_review_rules.md` at the other end of the lifecycle.
|
||||
|
||||
**A bug does not go through this stage.** It enters the lifecycle directly.
|
||||
A bug report has no user story and no acceptance criteria by design — the
|
||||
template asks for actual and expected behaviour and reproduction steps
|
||||
instead — so the definition of ready does not apply to it, and the story
|
||||
skills report that rather than demanding something a bug can never have. The
|
||||
exception is a bug so large it needs splitting, and that is worth a
|
||||
conversation about whether it should be a story.
|
||||
|
||||
Note that **"Plan" means two different things**: step 3 of the lifecycle below
|
||||
is the technical plan for one task, written by whoever implements it.
|
||||
`/plan-story` is the story-level breakdown that produces those tasks.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### 1. Understand
|
||||
|
||||
Read the GitHub issue thoroughly before writing any code. Clarify scope,
|
||||
acceptance criteria, and edge cases. If the issue is ambiguous, ask the user
|
||||
for clarification rather than guessing.
|
||||
|
||||
**Determine the mode.** If the user supplied `--parent <github_issue_url>`,
|
||||
this is **attach mode**: the task is part of a larger story whose branch and
|
||||
MR already exist. Otherwise, this is **standalone mode**: the task gets its
|
||||
own branch and MR. The parent may live in a different repository — parse
|
||||
owner + repo + number from the URL, do not assume same-repo.
|
||||
|
||||
In attach mode, also:
|
||||
|
||||
- Read the parent issue via the GitHub MCP server (it provides context the
|
||||
task issue alone doesn't have).
|
||||
- List the parent's sub-issues. Note which are already closed, which are
|
||||
in progress, and who is assigned — this prevents stepping on a colleague's
|
||||
work.
|
||||
- Confirm the existing branch is already checked out in the current workspace
|
||||
(Conductor creates the workspace from a branch; the branch is on disk
|
||||
before the workflow starts). If it is not — abort and ask the user.
|
||||
|
||||
Then determine **which parts of the codebase the issue affects**:
|
||||
|
||||
- Check labels and comments for scope hints (when available).
|
||||
- Otherwise, search the codebase for keywords from the issue title and body
|
||||
to locate the relevant component(s).
|
||||
- Identify which stack is affected: CoffeeScript frontend, Vue 3 frontend
|
||||
(desktop or mobile), REST backend, GraphQL layer, services, etc.
|
||||
- State your conclusion explicitly before moving on, e.g.
|
||||
"This issue affects the CoffeeScript frontend (KB answers component) and
|
||||
the REST controller behind it. I will NOT investigate Vue 3, GraphQL, or
|
||||
unrelated services unless something during research requires it."
|
||||
|
||||
This explicit scoping prevents broad codebase exploration in areas the
|
||||
issue does not touch.
|
||||
|
||||
### 2. Research
|
||||
|
||||
Investigate only the parts of the codebase identified in the Understand
|
||||
phase. Delegate broad research to separate agents to keep your main context
|
||||
clean. Understand the existing patterns before introducing new ones.
|
||||
|
||||
If during research you discover the scope was wrong (e.g. the data turns
|
||||
out to come from a service you hadn't considered), update the scope
|
||||
explicitly and tell the user before continuing.
|
||||
|
||||
### 3. Plan
|
||||
|
||||
Identify affected files and outline the approach. For non-trivial changes,
|
||||
present the plan to the user before implementing. Consider side effects on
|
||||
other parts of the system.
|
||||
|
||||
### 4. Branch
|
||||
|
||||
**Standalone mode:** Use the `/prepare-issue-branch` skill to create the
|
||||
branch and prepare the commit message. It handles naming conventions, public
|
||||
vs. private issue detection, and commit message formatting automatically.
|
||||
|
||||
**Attach mode:** Do _not_ invoke `/prepare-issue-branch`. The story branch
|
||||
is already checked out (confirmed in step 1). Instead:
|
||||
|
||||
- Run `git fetch origin && git pull --rebase` to align with what colleagues
|
||||
have pushed since you started.
|
||||
- If `git pull --rebase` produces conflicts, stop and ask the user to
|
||||
resolve them before continuing.
|
||||
- Do not prepare a commit message yet — it will be written in step 8
|
||||
based on what was actually changed.
|
||||
|
||||
### 5. Implement
|
||||
|
||||
Write the code following the patterns described in the relevant agent reference
|
||||
docs (graphql_patterns, frontend_patterns, service_patterns, etc.). Read the
|
||||
relevant doc before starting implementation — do not read them all upfront.
|
||||
|
||||
### 6. Test
|
||||
|
||||
Run the specific tests affected by your changes. Write new tests for new
|
||||
behavior. Never run the full test suite — CI handles comprehensive testing.
|
||||
|
||||
### 7. Review
|
||||
|
||||
Before committing, delegate the review to the `code-reviewer` sub-agent.
|
||||
The sub-agent has its own context window and reviews the uncommitted diff
|
||||
without bias from the implementation history.
|
||||
|
||||
The sub-agent will report findings grouped by severity (P0/P1/P2). If
|
||||
findings are reported, return to step 5 (Implement) to address them, then
|
||||
test again before re-running the review.
|
||||
|
||||
If the sub-agent reports no findings, proceed to commit.
|
||||
|
||||
### 8. Commit
|
||||
|
||||
Pre-commit hooks run linting and validation automatically. If you changed
|
||||
the GraphQL schema or settings, the regenerate hook runs the appropriate
|
||||
generators automatically.
|
||||
|
||||
**Standalone mode:** Use the prepared commit message from step 4
|
||||
(`/prepare-issue-branch` wrote it to `.git/COMMIT_EDITMSG`).
|
||||
|
||||
**Attach mode:** Write a commit message that describes what this commit
|
||||
changed — no issue reference, no `Closes`/`Fixes` keywords. The parent
|
||||
story's MR carries the issue context; individual commits within it only
|
||||
need to describe their own change. Keep it short and conventional, e.g.
|
||||
`Add validation for empty subject on ticket form.`
|
||||
|
||||
### 9. Merge Request
|
||||
|
||||
**Standalone mode:** Use the `/create-mr` skill to push the branch and
|
||||
create a Merge Request on GitLab. It selects the correct MR template, fills
|
||||
in the sections, and targets the appropriate branch automatically.
|
||||
|
||||
**Attach mode:** Do _not_ invoke `/create-mr` — the MR for the parent story
|
||||
already exists.
|
||||
|
||||
**Don't push automatically.** After committing, stop and wait. Tell the
|
||||
user what you committed and ask before pushing — every time. Pushing
|
||||
sends your commit to the team's MR and runs CI, so the user decides
|
||||
when that happens.
|
||||
|
||||
Push only after the user confirms.
|
||||
|
||||
CI runs automatically on MR creation (standalone) or push (attach). A human
|
||||
review is required before merging.
|
||||
|
||||
### 10. Cherry-pick
|
||||
|
||||
If the fix also applies to `stable`, use the `/cherry-pick-to-stable` skill
|
||||
after the MR is merged to `develop`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Never push directly to `develop` or `stable`.
|
||||
- Never reference private issue numbers in public commits.
|
||||
- Never run the full test suite — always target specific files.
|
||||
@@ -1,107 +0,0 @@
|
||||
<!-- Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/ -->
|
||||
|
||||
# Frontend addon layer
|
||||
|
||||
How an external addon (szpm package) extends the new Vue frontend — without
|
||||
overwriting core files, without runtime monkey-patching, and in a way that
|
||||
survives the planned Vapor migration.
|
||||
|
||||
## Mechanism: build-time weave
|
||||
|
||||
An addon's UI changes are applied by **rewriting the targeted core SFC's source
|
||||
text at build time, before the Vue compiler runs** — a Vite `enforce: 'pre'`
|
||||
plugin in `app/frontend/build/addonWeave` (see its `README.md` for internals).
|
||||
|
||||
It is Vapor-safe by construction: it edits the compiler's _input_ (template
|
||||
source), not the vnode tree at runtime, so whatever compiler runs next compiles
|
||||
the merged source normally — the vdom compiler today, the Vapor compiler later.
|
||||
(Runtime monkey-patching — wrapping `render`, walking/cloning vnodes — is the
|
||||
opposite: automatic but vdom-only, and cannot work under Vapor.) Core SFCs are
|
||||
never modified on disk; only the in-memory module is rewritten during the build.
|
||||
|
||||
## Multi-addon: per-addon manifests, discovered
|
||||
|
||||
There is **no central rule list**. Each addon ships its own `*.weave.mjs`
|
||||
manifest next to its code; `discoverRules.mjs` globs every manifest under
|
||||
`app/frontend` and concatenates them. Core names no addon — adding one is
|
||||
dropping a manifest + its code, never editing a shared core file. Any number of
|
||||
addons compose.
|
||||
|
||||
## Manifest format
|
||||
|
||||
`…/<addon>.weave.mjs` (plain JS — it is imported at config time):
|
||||
|
||||
```js
|
||||
export const addonWeaveRules = [
|
||||
{
|
||||
target: 'shared/components/CommonOrganizationAvatar/CommonOrganizationAvatar.vue',
|
||||
scriptSetup: [
|
||||
"import { useThing } from '#shared/addons/<addon>/useThing.ts'",
|
||||
'const __addonThing = useThing(() => props.entity)',
|
||||
].join('\n'),
|
||||
template: [
|
||||
// decorate the element (class / style / attribute)
|
||||
{ match: { component: 'CommonAvatar' }, addAttribute: ':class="__addonThing"' },
|
||||
// inject a sibling before / after a matched element
|
||||
{ match: { hasClass: 'addon-anchor-ticket-reply' }, insertBefore: '<MyBanner />' },
|
||||
],
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
- `target` — core SFC path (suffix match); stays byte-for-byte untouched.
|
||||
- `scriptSetup` — code appended inside the SFC's `<script setup>` (imports +
|
||||
setup), referencing the addon's own files via `#shared` / `#desktop` aliases.
|
||||
- `template` — semantic edits on the parsed template AST (not raw text). Each op
|
||||
is a `match` plus an action: **additive** (composable) `insertBefore` /
|
||||
`insertAfter` (sibling), `prepend` / `append` (first/last child), `addAttribute`,
|
||||
`setAttribute` / `removeAttribute` (override/remove `name` or `:name`),
|
||||
`wrap: { open, close }`; or **destructive** (stands alone) `replace` / `remove`.
|
||||
`match` accepts `component` / `element` / `tag` / `id` / `testId` / `hasClass` /
|
||||
`attr: { name, value? }` / `root` (AND-combined, component≠native) and must hit
|
||||
≥1 element; overlapping edits — even across addons — fail the build too.
|
||||
|
||||
The addon's logic (composables/components) lives in its own files. Data usually
|
||||
comes from the Apollo cache (`objectAttributeValues` etc. — a stable public
|
||||
GraphQL contract), so an addon rarely needs anything new from core.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Vitest** — render the **pristine** core component; the weave runs during the
|
||||
test build (the plugin is in `vite.config.mjs`), so the woven behavior is what
|
||||
you assert. Seed the Apollo cache via the mock client for cache-backed
|
||||
composables.
|
||||
- **For addons, prefer Capybara/system tests** — they exercise the package in the
|
||||
real, built app (the weave actually applied on install), which is the addon's
|
||||
main risk. Vitest is the faster secondary check.
|
||||
- A test runs in CI **only if registered** in the addon's `.gitlab-ci.yml`
|
||||
`script` (and listed in the `.szpm` `files`); CI does not auto-discover specs.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Discovery roots on `process.cwd()`, not `import.meta.url`** — Vite bundles
|
||||
`vite.config.mjs` (and its imports) to a temp file before running, so an
|
||||
`import.meta.url`-relative path resolves wrong and the glob finds nothing.
|
||||
- A newly linked addon's woven SFC can be stale in a **warm Vite cache** (the
|
||||
pristine SFC source never changed, so only a config-dependency change busts
|
||||
it). A fresh build / CI run is unaffected.
|
||||
- Avatar/icon colors: use the desktop **design tokens** (`var(--color-…)`), not
|
||||
hard-coded hsl/rgb.
|
||||
- A **linked** package needs `PRESERVE_SYMLINKS=1` _and_ pnpm
|
||||
`nodeLinker: hoisted`: without the first, `#shared/*` (a subpath import of
|
||||
`app/frontend/package.json`) does not resolve from the symlinked addon file;
|
||||
without the second, pnpm's symlinked `node_modules` breaks transitive
|
||||
resolution. `zammad:package:pnpm_hoisted_mode` covers `assets:precompile`,
|
||||
not `pnpm dev`.
|
||||
|
||||
## Honest scope
|
||||
|
||||
- **Vapor itself is not yet exercised** (this Vue has no Vapor compiler); the
|
||||
Vapor-safety is by construction and must be confirmed once the repo builds
|
||||
Vapor.
|
||||
- Matching is AST-based (component/element identity, `id`, `data-test-id`, a
|
||||
static class, or the root) and edits splice at exact source offsets; a missed
|
||||
anchor **fails the build loudly**. A failing weave fails the _whole_ build —
|
||||
there is no per-addon isolation (intended: a failed install over a broken UI).
|
||||
- Manifest discovery globs `app/frontend`; the real source of truth should be the
|
||||
installed-package (szpm) list.
|
||||
@@ -1,32 +0,0 @@
|
||||
# Frontend Patterns
|
||||
|
||||
## Code Placement Rules
|
||||
|
||||
Code shared across both apps (desktop/mobile) lives in `app/frontend/shared/`.
|
||||
Code used across multiple pages within one app goes at the app level.
|
||||
Page-specific code stays in the page folder.
|
||||
|
||||
**Pages cannot import from other pages** (enforced by eslint).
|
||||
Move shared logic up to the app or shared level.
|
||||
|
||||
## shared/entities/
|
||||
|
||||
Business logic per domain entity (e.g. ticket, user) lives in
|
||||
`shared/entities/<entity>/`. This includes entity-specific composables,
|
||||
GraphQL operations, object attribute stores, and types.
|
||||
Do not place components inside entity folders.
|
||||
|
||||
## shared/components/ — CommonXYZ Convention
|
||||
|
||||
Shared components follow the `CommonXYZ` naming pattern.
|
||||
The folder name, Vue file name, and component name must match
|
||||
(e.g. `CommonNotifications/CommonNotifications.vue`).
|
||||
Related composables, types (`types.ts`), and sub-components live
|
||||
inside the same folder.
|
||||
|
||||
## Auto-Generated Files — Never Edit
|
||||
|
||||
- `shared/graphql/types.ts`
|
||||
- `shared/types/config.ts`
|
||||
- `.ts` files next to `.graphql` operation files
|
||||
(e.g. `graphql/queries/`, `graphql/mutations/`, `graphql/subscriptions/`)
|
||||
@@ -1,72 +0,0 @@
|
||||
# GraphQL Patterns
|
||||
|
||||
Knowledge base queries, mutations and subscriptions follow extra rules on top of
|
||||
these, see `.dev/agent_docs/knowledge_base_patterns.md`.
|
||||
|
||||
## Resolver Structure
|
||||
|
||||
Resolvers are thin wrappers. Business logic lives in `app/services/service/`:
|
||||
|
||||
```ruby
|
||||
def resolve(...)
|
||||
Service::SomeAction.execute(...)
|
||||
end
|
||||
```
|
||||
|
||||
Their specs follow the same split: the behavior is covered by the service
|
||||
spec, the GraphQL spec only covers the GraphQL surface — see
|
||||
`.dev/agent_docs/testing_services_and_graphql.md`.
|
||||
|
||||
## Authorization
|
||||
|
||||
Multiple layers, applied declaratively:
|
||||
|
||||
- `requires_permission('ticket.agent')` — static permission check at resolver level
|
||||
- `allow_public_access!` — opt-out of authentication
|
||||
- `loads_pundit_method: :update?` — authorize loaded objects via Pundit
|
||||
at argument level
|
||||
- `requires_enabled_setting('some_feature')` /
|
||||
`requires_disabled_setting(...)` — feature flag guards
|
||||
|
||||
Object-level authorization uses Pundit via `HasPunditAuthorization`
|
||||
concern on types.
|
||||
|
||||
## Auto-Registration
|
||||
|
||||
Queries, mutations, and subscriptions are auto-registered — no manual
|
||||
field registration needed. Just create the class inheriting from the
|
||||
appropriate base and it will be discovered.
|
||||
|
||||
## Custom Relation DSL
|
||||
|
||||
Types use custom macros (not standard graphql-ruby):
|
||||
|
||||
```ruby
|
||||
belongs_to :group, Gql::Types::GroupType
|
||||
has_one :organization, Gql::Types::OrganizationType
|
||||
```
|
||||
|
||||
These automatically set up batch-loaded resolvers and mark fields as
|
||||
dependent for permission checks.
|
||||
|
||||
## Scoped Fields
|
||||
|
||||
```ruby
|
||||
scoped_fields do
|
||||
field :some_field, String, null: true
|
||||
end
|
||||
```
|
||||
|
||||
Fields inside `scoped_fields` are restricted by Pundit FieldScope —
|
||||
they return `nil` instead of errors when unauthorized. Must be nullable.
|
||||
|
||||
## Internal Fields
|
||||
|
||||
```ruby
|
||||
internal_fields do
|
||||
field :some_field, String
|
||||
end
|
||||
```
|
||||
|
||||
Fields inside `internal_fields` are restricted by checking for admin/agent permission.
|
||||
They return `nil` instead of errors when unauthorized.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Knowledge Base Patterns
|
||||
|
||||
**Scope: the new stack only** — the Vue desktop app
|
||||
(`app/frontend/apps/desktop/pages/knowledge-base/`), the GraphQL layer
|
||||
(`app/graphql/gql/**/knowledge_base*`) and the services behind it
|
||||
(`app/services/service/knowledge_base/`). Nothing here describes the other two
|
||||
consumers: the legacy CoffeeScript admin frontend, and the server-rendered
|
||||
public help site (`app/controllers/knowledge_base/public/`) — which is not
|
||||
legacy, just a different stack. Both work on the knowledge base directly and by
|
||||
their own rules, so do not apply these rules to them, and do not "align" them
|
||||
with these.
|
||||
|
||||
For the layers themselves see `.dev/agent_docs/graphql_patterns.md` and
|
||||
`.dev/agent_docs/service_patterns.md`.
|
||||
|
||||
## A single, active knowledge base
|
||||
|
||||
The system supports exactly one knowledge base, and the new stack works on it
|
||||
only while it is **active** — browse queries, feeds, subscriptions and write
|
||||
mutations alike. An inactive one is neither browsable nor editable there;
|
||||
activating it and changing its settings (not its texts) stays with the legacy
|
||||
admin dialog, which edits the record directly.
|
||||
|
||||
Rules for new code:
|
||||
|
||||
- Never accept a knowledge base id from the client, and never scope anything by
|
||||
one. Resolve it: `::KnowledgeBase.active.first!`.
|
||||
- Raise `ActiveRecord::RecordNotFound` when none is active, rather than
|
||||
answering with null or an empty list. Whoever resolves the record asserts it —
|
||||
a write service does it for itself, a resolver with no service behind it does
|
||||
it inline.
|
||||
- Do not pass a `knowledge_base:` into a write service. Pass it to a read
|
||||
service only if the resolver had to resolve the record anyway; nothing
|
||||
resolves it twice.
|
||||
- Pass the client's `locale` code straight to the service, which resolves it
|
||||
strictly against that knowledge base (a `KnowledgeBase::Locale` record is
|
||||
accepted too), then call `store_knowledge_base_locale` so the payload renders
|
||||
in the locale that was written.
|
||||
|
||||
The single-knowledge-base assumption rests on there being no create route
|
||||
(`resources :knowledge_bases, only: %i[show update]`) and the legacy admin
|
||||
dialog offering its creation form only while none exists. It is not enforced by
|
||||
a validation or a database constraint, so `first!` picks arbitrarily if seeds or
|
||||
a console ever create a second one.
|
||||
|
||||
Where the rules live today — current code, not itself a rule: the write services
|
||||
resolve the record inside `Service::KnowledgeBase::Base#active_knowledge_base!`.
|
||||
Everywhere else the resolver resolves it, then either hands it to a read service
|
||||
(`CategoryContent`, `FeedPaths`) or — where no service sits behind the
|
||||
operation — asserts it inline and works on the record itself. Grep
|
||||
`KnowledgeBase.active.first` to see which operations do which; it is a moving
|
||||
set, so do not rely on a list here.
|
||||
|
||||
## Deliberate exceptions
|
||||
|
||||
Do not "fix" these without a decision:
|
||||
|
||||
- `Gql::Subscriptions::KnowledgeBase::ContentUpdates` pings with a **null**
|
||||
knowledge base instead of raising — a subscription cannot raise usefully, and
|
||||
this ping is the only signal browse views get after a deactivation. They
|
||||
refetch, then hit the not-found from the queries.
|
||||
- `KnowledgeBasePolicy#show_any?` and `KnowledgeBasePolicy::Scope` let granted
|
||||
access outrank the active check, because the public help site previews an
|
||||
inactive knowledge base that way — the policy for anyone with effective access
|
||||
to the record (a reader included), the scope for the `knowledge_base.editor`
|
||||
permission.
|
||||
- `Gql::Concerns::SearchesKnowledgeBaseAnswers` answers with an **empty result**
|
||||
instead of raising when no knowledge base is active. It is reached from a
|
||||
document that asks for more than the knowledge base — the desktop quicksearch
|
||||
requests every searchable entity in one query — and the search field is
|
||||
`null: false`, so a raise would null the root and take the ticket, user and
|
||||
organization groups down with it on every instance that has no knowledge base.
|
||||
The knowledge base's own queries, `Gql::Queries::KnowledgeBase::Search`
|
||||
included, do raise: they are asked for on their own.
|
||||
|
||||
Each of these carries the reasoning in a code comment; keep the two in sync.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Model Patterns for the Legacy CoffeeScript Stack
|
||||
|
||||
When introducing a new model that is used by the legacy stack (e.g. a generic
|
||||
admin interface), two concerns are needed so the frontend can render and stay
|
||||
in sync: REST assets and client notifications.
|
||||
|
||||
## REST Assets
|
||||
|
||||
### What assets are
|
||||
|
||||
REST responses for the legacy stack ship related records ("assets") alongside the
|
||||
requested objects, so the frontend Spine store can render relations without extra
|
||||
requests. `ApplicationModel::CanAssets` provides the default `assets(data)`
|
||||
implementation; models only need their own `Model::Assets` concern when the default
|
||||
is not enough.
|
||||
|
||||
### When to add a custom assets concern
|
||||
|
||||
- The model has `belongs_to` relations whose records the UI needs to render
|
||||
(e.g. a user reference or a polymorphic relation).
|
||||
- The model needs to filter attributes or restrict visibility per user
|
||||
(see `app/models/group/assets.rb` — `filter_unauthorized_attributes` /
|
||||
`authorized_asset?`).
|
||||
|
||||
### Rules
|
||||
|
||||
- Only follow one-to-one (`belongs_to`) relations. NEVER include one-to-many
|
||||
relations — e.g. organization members can be a million records; shipping them
|
||||
as assets must be prevented (`app/models/organization/assets.rb` limits this).
|
||||
- Guard against `nil` relations: optional `belongs_to` and polymorphic targets
|
||||
whose record has been deleted return `nil`.
|
||||
- Keep the dedup guard (`return data if data[app_model][id]`) — it prevents
|
||||
infinite loops between mutually referencing assets.
|
||||
- Include the concern in the model: `include Model::Assets` (after the generic
|
||||
concerns block).
|
||||
|
||||
### Skeleton
|
||||
|
||||
Minimal concern following `app/models/mention/assets.rb` (belongs_to user +
|
||||
polymorphic relation):
|
||||
|
||||
```ruby
|
||||
class MyModel
|
||||
module Assets
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def assets(data)
|
||||
app_model = self.class.to_app_model
|
||||
|
||||
data[ app_model ] ||= {}
|
||||
return data if data[ app_model ][ id ]
|
||||
|
||||
data[ app_model ][ id ] = attributes_with_association_ids
|
||||
|
||||
if my_polymorphic_relation.present?
|
||||
data = my_polymorphic_relation.assets(data)
|
||||
end
|
||||
|
||||
return data if user.blank?
|
||||
|
||||
user.assets(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Reference examples
|
||||
|
||||
- `app/models/mention/assets.rb` — user + polymorphic `mentionable`
|
||||
- `app/models/online_notification/assets.rb` — lookup tables + created/updated by
|
||||
- `app/models/group/assets.rb` — attribute filtering and per-user authorization
|
||||
|
||||
## Client Notifications
|
||||
|
||||
Models rendered by the legacy stack (admin interfaces, collections) must
|
||||
broadcast changes to connected clients, otherwise open sessions show stale
|
||||
lists until a manual reload:
|
||||
|
||||
```ruby
|
||||
class MyModel < ApplicationModel
|
||||
include ChecksClientNotification
|
||||
end
|
||||
```
|
||||
|
||||
- `app/models/concerns/checks_client_notification.rb` pushes
|
||||
`MyModel:create/update/touch/destroy` events (id + updated_at) via
|
||||
`PushMessages`; the frontend Spine collections then refetch.
|
||||
- Default recipient type is `authenticated` (all logged-in clients); actual
|
||||
record access is still enforced on fetch by the controller policy.
|
||||
- Restrict recipients with `client_notification_send_to :user_id` (only the
|
||||
referenced user) or skip events with
|
||||
`client_notification_events_ignored :touch, ...`.
|
||||
@@ -1,188 +0,0 @@
|
||||
# Zammad Product Principles
|
||||
|
||||
What the product holds to be true, in a form a story can be checked against.
|
||||
|
||||
Every principle is an **assertion plus what it rules out**. The second half is what does the work: it
|
||||
says what a violation looks like. A statement nothing could violate — "we value a consistent user
|
||||
experience" — is decoration, and it makes this file worse, because it trains readers to skim and it
|
||||
lets a tool generate findings nobody can act on.
|
||||
|
||||
This file is **not** the coding conventions. Its sibling files in this directory are the conventions;
|
||||
this file is about the product, and most of its content was extracted from decisions the team had
|
||||
already taken and documented once, in `BREAKING_CHANGES.md` or in a single story, where the next
|
||||
story could not find it.
|
||||
|
||||
## Consumers
|
||||
|
||||
| Consumer | Lives in | Uses these principles for |
|
||||
| -------------------- | ----------- | ------------------------------------------------------------ |
|
||||
| `refine-story` skill | skills repo | Checking a story and its design against the product's stance |
|
||||
|
||||
## How to cite and change
|
||||
|
||||
- IDs are **section-prefixed and append-only**: `CHANGE-1`, `UI-5`. Cite as
|
||||
`.dev/agent_docs/product_principles.md#CHANGE-1` — the repository-relative path, because most
|
||||
citations are written from outside this directory.
|
||||
- The `#ID` suffix is a **stable citation token, not a Markdown anchor**. The IDs are bold list
|
||||
items, and an explicit anchor would need inline HTML, which this repository's `markdownlint`
|
||||
configuration rejects.
|
||||
- A citation therefore resolves by **searching for the list item that defines the ID** — a line
|
||||
starting `- **` followed by the ID and then either `**` or a space, as in `- **CHANGE-1**` or
|
||||
`- **UI-6 (proposed)**`. That line is unique for every ID in this file. Searching for the bare ID
|
||||
is not deterministic: it also matches this section, and it prefix-matches a future `CHANGE-10`.
|
||||
- Do not "fix" the missing anchor by promoting the IDs to headings. A heading anchor is derived from
|
||||
the heading text, so it would break on every wording change — the opposite of what the next rule
|
||||
guarantees.
|
||||
- **Never renumber.** The change and dependency principles get cited outside this file — in a
|
||||
`BREAKING_CHANGES.md` entry, a review comment, a refinement note — so an old citation has to keep
|
||||
resolving.
|
||||
- A principle that no longer holds keeps its ID and is marked `(retired)` with the date. It is not
|
||||
deleted.
|
||||
- Anything genuinely undecided goes in **Unsettled** at the bottom rather than being guessed at. A
|
||||
consumer reading this file must be able to tell "we decided against it" from "we never decided".
|
||||
|
||||
---
|
||||
|
||||
## How we change things
|
||||
|
||||
Owner: development · last reviewed 2026-08
|
||||
|
||||
- **CHANGE-1** — Nothing is removed, renamed or restricted without a deprecation announced at least
|
||||
one release earlier. _Violation: a removal whose first appearance in `BREAKING_CHANGES.md` is the
|
||||
release that removes it._
|
||||
- **CHANGE-2** — Every breaking change states who is affected, what they have to do, and links a
|
||||
public issue. _Violation: an entry that describes the change but not the action, or that has no
|
||||
public issue to point at._
|
||||
- **CHANGE-3** — Only versions the vendor still maintains are supported, for every dependency —
|
||||
database, search index, Redis, Node, browsers, reverse proxy. The supported matrix stays small and
|
||||
predictable for administrators. _Violation: keeping compatibility with an end-of-life version in
|
||||
order to avoid a breaking change._
|
||||
- **CHANGE-4** — Prefer one general mechanism over several bespoke ones. Where a general mechanism
|
||||
covers what a special-cased feature did, the special case is a candidate for removal. This is a
|
||||
preference and a judgement call, not an automatic verdict: _a new special case for something an
|
||||
existing general mechanism already does is worth questioning, not automatically wrong._
|
||||
- **CHANGE-5** — Every removal names its replacement, or states explicitly that there is none.
|
||||
_Violation: "X was removed" with no forward path and no admission that there isn't one._
|
||||
|
||||
## Access and permissions
|
||||
|
||||
Owner: development · last reviewed 2026-08
|
||||
|
||||
- **ACCESS-1** — Access is least-privilege and explicit, and tightening it is worth a breaking
|
||||
change. _Violation: a new endpoint, field or action that is reachable more widely than the data it
|
||||
exposes justifies, or a permission left broad because narrowing it would break someone._
|
||||
|
||||
## Defaults and configuration
|
||||
|
||||
Owner: product · last reviewed 2026-08
|
||||
|
||||
- **DEFAULT-1** — Defaults move toward the safe value, even when that breaks someone. _Violation: an
|
||||
unsafe default kept for compatibility._
|
||||
- **DEFAULT-2** — Prefer a single behaviour that fits most users of the product over a new setting. A
|
||||
setting is the right answer when installations genuinely differ, not when a decision is hard to
|
||||
make. Configuration growing over a product's lifecycle is normal, so this is a preference and not a
|
||||
prohibition: _a new setting whose justification is "people will disagree" is worth questioning; one
|
||||
whose justification is a real difference between installations is not._
|
||||
|
||||
## Interface
|
||||
|
||||
Owner: product & UX · last reviewed 2026-08
|
||||
|
||||
- **UI-1** — Icon-only controls carry their accessible name. _Violation: a button whose only content
|
||||
is an icon and that has no tooltip or label supplying the name._
|
||||
- **UI-2** — Actions revealed only on hover or focus are reachable on touch devices. _Violation: a
|
||||
row action that exists only behind `:hover`._
|
||||
- **UI-3** — Responsive layouts adapt to the space their container actually has, not to the viewport.
|
||||
_Violation: a fixed column count that only changes at global breakpoints, inside something that can
|
||||
sit in a sidebar, a modal or a split pane._
|
||||
- **UI-4** — In the contexts that use the taskbar — tickets, and the knowledge base in the new
|
||||
desktop view — work that auto-saves and has to survive the user switching to another context for a
|
||||
moment lives in a taskbar tab. The taskbar is not the container for everything; it is the container
|
||||
for resumable work in those contexts. _Violation: putting auto-saving, resumable work in a modal or
|
||||
a flyout that cannot be left open._
|
||||
- **UI-5** — An action that deletes, removes or discards something the user cannot get back is
|
||||
confirmed before it happens, through the shared confirmation dialog. _Violation: a delete that goes
|
||||
straight through, or a bespoke confirm dialog next to the shared one._
|
||||
|
||||
## Deployment
|
||||
|
||||
Owner: development · last reviewed 2026-08
|
||||
|
||||
- **DEPLOY-1** — What the application is made of is decided at **build time**. A running instance
|
||||
never mutates its own application files, and a container least of all. _Violation: any feature that
|
||||
writes into the application directory at runtime and expects it to survive — installing a package,
|
||||
dropping in a custom file override, generating assets in place._
|
||||
|
||||
Worth stating rather than assuming, because the codebase already contradicts it:
|
||||
`app/models/package.rb` writes to `Rails.root` on install and uninstall, and reads
|
||||
`auto_install/`. That mechanism predates containers being a supported deployment.
|
||||
|
||||
This principle rules the runtime write **out**. It says nothing about what replaces it — which of
|
||||
an image layer, a mount or a documented build step is the supported answer is **unsettled**, so
|
||||
treat a story that proposes one of them as touching an open question rather than as aligned.
|
||||
|
||||
## Language and localisation
|
||||
|
||||
Owner: product · last reviewed 2026-08
|
||||
|
||||
- **LANG-1** — Source strings are complete, meaningful sentences in sentence case. _Violation: a
|
||||
single standalone word as a translatable string — translators have no context to disambiguate it._
|
||||
- **LANG-2** — An existing string is reused rather than a near-duplicate introduced. _Violation: a
|
||||
new string that differs from an existing one only in punctuation or word order; every distinct
|
||||
string is work in every supported language._
|
||||
- **LANG-3** — Translations come from translations.zammad.org. _Violation: editing `i18n/*.po`
|
||||
directly, or proposing that as the fix for a wording problem._
|
||||
- **LANG-4** — Locale codes follow the standard, even when correcting one is a breaking change.
|
||||
_Violation: keeping a wrong locale code because changing it would affect existing installations._
|
||||
|
||||
## Honesty about what the system does
|
||||
|
||||
Owner: product · last reviewed 2026-08
|
||||
|
||||
- **HONEST-1** — Labels describe what the system actually did, not the most impressive reading of it.
|
||||
In particular, AI is not claimed for mechanical behaviour: a similarity search is presented as
|
||||
knowledge, not as AI understanding. _Violation: wording that implies reasoning where there is
|
||||
retrieval, or that credits AI for a step it only fed into._
|
||||
|
||||
This is the one principle in the file that cost a whole story to learn — `zammad/zammad#6323`
|
||||
renamed "Suggested by AI" to "Suggested knowledge" because the label overstated a similarity
|
||||
search. It is also the product-level form of the company's stated commitment to AI that is "safe,
|
||||
transparent, and aligned with our values".
|
||||
|
||||
## Stack direction
|
||||
|
||||
Owner: development · last reviewed 2026-08
|
||||
|
||||
- **STACK-1** — New features target Vue 3 and GraphQL. _Violation: new behaviour built on REST and
|
||||
the legacy CoffeeScript interface without a stated reason, such as extending existing legacy
|
||||
behaviour._
|
||||
|
||||
---
|
||||
|
||||
## Unsettled
|
||||
|
||||
Questions this file cannot answer yet. A consumer that hits one of these **raises it as a question
|
||||
for a human** — it must not produce a verdict, and it must not infer the answer from the shape of the
|
||||
existing code.
|
||||
|
||||
- **ACCESS-2 (proposed)** — Should every new surface have to state which roles can see and use it,
|
||||
including the granular-permission case? "Agents" would not be an acceptable answer.
|
||||
- **UI-6 (proposed)** — Should new UI be required to compose existing design-system components and
|
||||
tokens, with a new primitive treated as a decision rather than a detail?
|
||||
- **UI-7 (proposed)** — Should a resumable view offer its own taskbar-behaviour-on-update setting,
|
||||
stored independently per view? Seen once, in the story that introduced editing knowledge base
|
||||
answers; unclear whether it generalises.
|
||||
- **When is a new setting justified?** `DEFAULT-2` states the preference but not the test. There are
|
||||
~300 settings today, so a usable criterion would have real leverage.
|
||||
- **Are bulk or batch variants expected** for single-record actions, or added only on demand?
|
||||
- **Where does deferred or scheduled state belong** in the interface? The same story grew a whole
|
||||
scheduled visibility capability, which suggests there is no settled pattern for it yet.
|
||||
- **How much of the legacy interface does a new feature have to reach**, and when is parity
|
||||
explicitly not required?
|
||||
- **How custom file overrides and packages are meant to work in a container**, given `DEPLOY-1`.
|
||||
The principle says a runtime write is out; it does not say which of an image layer, a mount or a
|
||||
build step is the answer. `zammad/zammad#6229` is blocked on exactly that choice.
|
||||
- **Product direction beyond the above.** The company vision — democratising customer service,
|
||||
transparency, open source, data protection — is real but not story-level: nothing in a story can
|
||||
violate it. Whatever exists as actual direction is not written down anywhere in this repository
|
||||
yet, and until it is, alignment with it is a question for product, never a finding.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Service Patterns
|
||||
|
||||
## Structure
|
||||
|
||||
Services live in `app/services/service/` and encapsulate business logic.
|
||||
They are used e.g. by GraphQL resolvers and REST controllers.
|
||||
|
||||
- Pass all arguments via the execute class method, e.g. Service.execute(param: :value). It is the single entry point.
|
||||
- Some services have with_current_user chainable method, e.g.g Service.with_current_user(user).execute(param: :value)
|
||||
- Inherit from `Service::Base`
|
||||
- Current user is supplied automatically and available as current_user instance method
|
||||
- Services expecting current user should have requires_current_user! in their class body
|
||||
- In specs, Service.execute class method should be stubbed when needed
|
||||
- The service spec owns the behavior of everything the service decides — the
|
||||
GraphQL spec around it only tests the GraphQL layer, see
|
||||
`.dev/agent_docs/testing_services_and_graphql.md`
|
||||
- Knowledge base services follow extra rules, see
|
||||
`.dev/agent_docs/knowledge_base_patterns.md`
|
||||
- Add attr_reader for all arguments passed to the initializer, so they are available in instance methods
|
||||
- Service initializer should not have super() call, as Service::Base does not have an initializer
|
||||
@@ -1,462 +0,0 @@
|
||||
# Zammad Story Rules
|
||||
|
||||
Single source of truth for when a story is ready to be planned, how it is sized, and what its plan
|
||||
has to cover.
|
||||
|
||||
This file is **tool-neutral on purpose**. It says nothing about `gh`, GitHub issue types, board
|
||||
columns, sub-issue mechanics or output format — those belong to whichever caller is using these
|
||||
rules.
|
||||
|
||||
It is also **not a duplicate of the coding conventions**. Its sibling files in this directory are
|
||||
the conventions; this file says which of them a plan has to account for, and it is the counterpart
|
||||
of `code_review_rules.md` at the other end of the lifecycle: the same acceptance criteria that are
|
||||
verified there are shaped and mapped here.
|
||||
|
||||
## Consumers
|
||||
|
||||
| Consumer | Lives in | Uses these rules for |
|
||||
| -------------------- | ----------- | ------------------------------------------------------- |
|
||||
| `refine-story` skill | skills repo | Checking and improving a story before it can be planned |
|
||||
| `plan-story` skill | skills repo | Cutting a ready story into sub-issues and its plan |
|
||||
|
||||
When a rule changes, it changes **here** and every consumer picks it up. Do not copy rule text into
|
||||
a consumer.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Definition of ready
|
||||
|
||||
A story is plannable when someone can cut it into work without inventing the story first. Two
|
||||
groups of checks; the mechanical ones are cheap and unambiguous, the judgement ones are not.
|
||||
|
||||
These checks are defined for **stories**. A bug has no user story and no acceptance criteria by
|
||||
design, so M2 and M3 can never pass for one and gating on them would be meaningless. For anything
|
||||
that is not a story, report the definition of ready as not applicable rather than as failed.
|
||||
|
||||
**Mechanical** — a caller may enforce these before anything else runs:
|
||||
|
||||
- **M1** The body is not a placeholder: it is non-empty, it is not `TBD`, `TDB` or `?` as its
|
||||
entire content, and it carries **200 characters of prose or more** (fewer than 200 fails; exactly
|
||||
200 passes). Prose is what remains after removing headings, code blocks, HTML comments, list
|
||||
markers, emphasis and heading markup, and link and image syntax — link text counts, the URL does
|
||||
not — and after collapsing every run of whitespace to a single space.
|
||||
- **M2** A user story is present — a role, an action and a benefit, in whatever wording.
|
||||
- **M3** An acceptance-criteria section is present. Heading wording varies
|
||||
(`## Acceptance criteria`, `### Acceptance Criteria`); match tolerantly.
|
||||
- **M4** At least two criteria, each an assertion rather than a question.
|
||||
- **M5** No unresolved blocking item under a `Clarification needed` heading. Anything in that section
|
||||
that is not a ticked task item (`- [x]` or `1. [x]`) counts as open. An open item is blocking
|
||||
unless its text contains `non-blocking` or `blocks planning: no` — those two markers exactly, so
|
||||
that two callers reading the same item reach the same verdict. Prose in the section with no list
|
||||
items at all counts as one open blocking item, because a section that had nothing left to settle
|
||||
would have been removed.
|
||||
|
||||
**Judgement** — decided by reading, never by pattern matching:
|
||||
|
||||
- **J1** Every criterion is verifiable: it states an observable outcome, not an intention.
|
||||
- **J2** Scope is bounded. A criterion that delegates its own scope — "any other relevant surface",
|
||||
"and wherever else this appears" — is not ready, because whoever plans it inherits an open search.
|
||||
- **J3** Criteria do not contradict each other or the user story.
|
||||
- **J4** Where the story is about UI, a design is linked and it is the current one (see Step 3).
|
||||
- **J5** Open questions are stated as questions rather than implied by vague wording.
|
||||
- **J6** No question anywhere on the story is left unanswered — and open questions belong under
|
||||
`Clarification needed` in the body, not in a comment, so that answering one removes it.
|
||||
|
||||
### Verdicts
|
||||
|
||||
| Verdict | Meaning |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| **Ready** | plan it |
|
||||
| **Ready with gaps** | plannable, but named gaps go to their owner first — ask before cutting |
|
||||
| **Not ready** | do not plan. The list of failures _is_ the refinement to-do list |
|
||||
|
||||
A **Not ready** story is not a failure of the caller. Producing a confident plan from an unshaped
|
||||
story is worse than producing nothing, because the plan then looks like a decision that was taken.
|
||||
|
||||
### Decisions taken outside the issue
|
||||
|
||||
A decision made in a meeting, a chat or a call is **not a decision** until it is written in the
|
||||
story or in a comment on it. Everything downstream of this file reads the issue and its comments as
|
||||
the record. If a criterion is known to be settled but nothing on the issue says so, that is an
|
||||
unanswered question, not a settled one.
|
||||
|
||||
Where the body and a comment disagree, the **comment wins** — a body describing the
|
||||
pre-discussion state is normal.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Story size
|
||||
|
||||
> **A shared prerequisite is a dependency, not a scope boundary.**
|
||||
|
||||
This is the rule that matters most, and the one most often broken. Several capabilities that each
|
||||
need the same base to exist are not one story. They are one base story plus several stories that
|
||||
depend on it.
|
||||
|
||||
### The per-criterion test
|
||||
|
||||
This is a **closed checklist, not an impression**. For every acceptance criterion, write the single
|
||||
sentence that connects it to the story's own outcome, then classify it:
|
||||
|
||||
| The sentence is… | Then |
|
||||
| ------------------------------------------------ | ------------------------------------------ |
|
||||
| "it also changes another view or object" | separate story — its scope left this story |
|
||||
| "it needs the base to exist" | separate story, with a dependency |
|
||||
| "the story's outcome is not achieved without it" | it stays |
|
||||
| neither, or unclear | an open question, not a silent decision |
|
||||
|
||||
The rows are in **precedence order, and the first one that applies decides**. A criterion can
|
||||
truthfully match more than one — something required for the outcome can still reach another view —
|
||||
and without an order two callers classify the same story differently. The two separating tests
|
||||
therefore run before the keeping one: a criterion stays only when neither of them fires.
|
||||
|
||||
Where the first two rows both apply, the result is still one separate story, and **both** facts get
|
||||
recorded: the dependency, because the order of work depends on it, and the scope reach, because that
|
||||
is the part product has to hear.
|
||||
|
||||
Every criterion gets exactly one classification. A criterion that cannot be classified is itself a
|
||||
finding.
|
||||
|
||||
### Signals that a story is too big
|
||||
|
||||
Two or more firing is a conversation, not a footnote:
|
||||
|
||||
- More criteria than roughly five, or criteria carrying sub-bullets of their own.
|
||||
- A criterion naming a capability the title does not imply.
|
||||
- A criterion whose scope reaches outside the story's own object or view.
|
||||
- More than one outstanding UX decision.
|
||||
- More than one surface affected (desktop, mobile, legacy interface, public help site).
|
||||
- A criterion needing its own backend surface — a new model, a new mutation, a new endpoint.
|
||||
- More than one spike needed before the story can be planned at all.
|
||||
- A blocked criterion holding unblocked criteria hostage.
|
||||
|
||||
That last one is the real cost, and it is easy to miss because nothing looks wrong. One unresolved
|
||||
UX decision on one criterion delays every other criterion in the same story.
|
||||
|
||||
### Splitting is cheap
|
||||
|
||||
Issue types are `Epic` → `Story` → `Task`, with `Spike` a sibling of `Task` under the same story
|
||||
(Step 6), and the Epic level is in active use. A story split into three stories under the same Epic
|
||||
changes nothing about the hierarchy, the board or product's view of the work. What changes is that
|
||||
each piece carries its own criteria and can ship on its own.
|
||||
|
||||
### What may not become a story
|
||||
|
||||
A **layer slice** can never be a story. "Add the service and the mutation" has no role, no benefit
|
||||
and nothing observable — forcing it into story form produces a fake user story, which is worse than
|
||||
a large one. Layer slices are tasks. Split stories along capability and surface seams only.
|
||||
|
||||
### Worked example — editing knowledge base answers
|
||||
|
||||
"Edit an answer of the knowledge base", one story, sixteen tasks, one very large merge request.
|
||||
Its criteria classified by the test above:
|
||||
|
||||
| Proposed story | Criteria | Why separate |
|
||||
| --------------------------------------- | ---------------------------------- | ---------------------------------- |
|
||||
| Edit an answer | new editor, auto-save, own taskbar | this _is_ the outcome |
|
||||
| Manage tags and attachments of an answer | tags, attachments | needs the base only |
|
||||
| Link related tickets to an answer | links | needs the base only |
|
||||
| Show live users on the edit tab | live users | needs the base only |
|
||||
| Warn when an answer was edited meanwhile | concurrent-edit warning | needs the base only — blocked on UX |
|
||||
| Configure taskbar behaviour on update | taskbar selector | also changes the _Add answer_ view |
|
||||
| Schedule visibility changes | added a month after planning | own capability, own backend surface |
|
||||
|
||||
Seven stories where there was one. Only the first four words of the original title survive as the
|
||||
base story. The concurrent-edit warning was blocked on a UX decision and, in the single-story
|
||||
shape, blocked everything else with it.
|
||||
|
||||
The story also grew from ten to sixteen sub-issues after it was planned, because a new capability
|
||||
was added as tasks rather than raised as a story. **The size check therefore runs again whenever a
|
||||
breakdown is extended**, not only before the first cut.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Alignment: criteria, use cases, design
|
||||
|
||||
Three lists, written at three different moments by three different people, and they drift. This step
|
||||
is a **closed cross-check between lists that already exist**, in three directions — never a fresh
|
||||
search for behaviour nobody asked for.
|
||||
|
||||
### Which design
|
||||
|
||||
Collect every design link from the body **and every comment**. The most recent link wins; an earlier
|
||||
one is superseded, and a superseded link that is still in the body is itself a finding. If the
|
||||
design cannot be read — no connector, no access — say so explicitly and mark every check in this
|
||||
step as **not run**. Never infer a design's contents from the story's prose.
|
||||
|
||||
### Direction 1 — design → criteria
|
||||
|
||||
Something is drawn that no criterion asserts.
|
||||
|
||||
| Verdict | Meaning |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| Asserted | a criterion covers it — cite the criterion |
|
||||
| Missing a criterion | the design is right and the criteria are incomplete |
|
||||
| Design overreach | the design went beyond what the story asked for |
|
||||
|
||||
The two gaps have different owners, and collapsing them is what makes a refinement unactionable.
|
||||
"Missing a criterion" is work for whoever writes the story; "design overreach" is a scope
|
||||
conversation.
|
||||
|
||||
### Direction 2 — criteria → design
|
||||
|
||||
Something is asserted that is not drawn.
|
||||
|
||||
| Verdict | Meaning |
|
||||
| --------------- | -------------------------------------------------- |
|
||||
| Drawn | the design covers it |
|
||||
| Not drawn | UX work outstanding — name it |
|
||||
| Not applicable | the criterion has no UI (backend, API, deprecation) |
|
||||
|
||||
"Not applicable" must be stated, not assumed. It is the verdict that gets skipped silently.
|
||||
|
||||
### Direction 3 — states the design does not contain
|
||||
|
||||
The most expensive failures are not disagreements between the lists — they are states nobody drew
|
||||
and nobody wrote down, found during implementation or in review. Walk this list as a closed
|
||||
checklist and give each row a verdict, including "not applicable":
|
||||
|
||||
- empty state
|
||||
- loading state
|
||||
- error and validation states
|
||||
- permission-denied — and for which roles, including granular permissions
|
||||
- long content, truncation, overflow
|
||||
- zero vs one vs many
|
||||
- disabled state
|
||||
- unsaved changes when leaving
|
||||
- concurrent edit by a second user
|
||||
- narrow viewport and mobile
|
||||
- dark mode
|
||||
- keyboard operation and focus order
|
||||
- accessible name for icon-only controls
|
||||
- translation length, where a longer language breaks the layout
|
||||
|
||||
The UI rules that apply to each of these are in `code_review_rules.md` under **UI principles**, and
|
||||
the condensed list is in `.dev/ai-agent-instructions.md`. Do not restate them; check against them.
|
||||
|
||||
### Direction 4 — use cases → criteria
|
||||
|
||||
Where the story states use cases separately from its criteria, run the mapping described in
|
||||
`code_review_rules.md` under **Use-case cross-check**. It is defined there for review; the verdicts
|
||||
and their meanings are identical here, one stage earlier, and a gap found here costs nothing to fix.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Affected layers
|
||||
|
||||
Task breakdown is open-ended imagination, and open-ended imagination misses things. The structural
|
||||
fix is a closed list.
|
||||
|
||||
Walk **every** row. Give each one a verdict, and **write the negative verdicts out** — "not
|
||||
affected" with its reason is the whole point, because a row that is silently skipped was never
|
||||
considered. Read the routed document only for rows that come out affected.
|
||||
|
||||
| Layer | Read |
|
||||
| ----------------------------------------------------------- | --------------------------- |
|
||||
| Data model, migration, seeds | `database_migrations.md` |
|
||||
| Model behaviour, validations, legacy REST/notification concerns | `legacy_model_patterns.md` |
|
||||
| Authorization — policies, permissions, granular roles | — |
|
||||
| Service objects | `service_patterns.md` |
|
||||
| GraphQL — types, mutations, queries, subscriptions | `graphql_patterns.md` |
|
||||
| Form updater / Core Workflow | `frontend_patterns.md` |
|
||||
| Vue frontend — name which apps (desktop, mobile, shared) | `frontend_patterns.md` |
|
||||
| Addon layer | `frontend_addon_layer.md` |
|
||||
| Legacy CoffeeScript interface and its REST controllers | — |
|
||||
| Real time — subscriptions, taskbar, live users, concurrent edit | — |
|
||||
| Search index | — |
|
||||
| Background jobs and the scheduler | — |
|
||||
| Settings, defaults, feature flags | — |
|
||||
| Translations — new or changed source strings | `code_review_rules.md` |
|
||||
| Tests — backend, frontend, system | `testing.md` |
|
||||
| Documentation — admin docs, system requirements | — |
|
||||
| Breaking changes | `BREAKING_CHANGES.md` |
|
||||
|
||||
Two rows are missed most often and are worth naming: **authorization**, because a feature works
|
||||
perfectly while being visible to the wrong role, and **breaking changes**, because nothing in the
|
||||
diff looks like a breaking change. A story that removes, renames, restricts or raises anything needs
|
||||
a `BREAKING_CHANGES.md` entry, and that entry is work in its own right.
|
||||
|
||||
Where a row is affected but cannot yet be planned, it becomes a **spike** — a piece of work whose
|
||||
output is an answer, not a change. What a spike issue contains is in Step 6. Never hide an
|
||||
unresolved question inside an implementation task.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Depth of the plan
|
||||
|
||||
The plan exists so that whoever implements — a person or an agent — does not repeat the research,
|
||||
and so that the review afterwards has something concrete to check the result against. It is not a
|
||||
pre-written diff.
|
||||
|
||||
Go to code level **only** when one of these is true:
|
||||
|
||||
1. **The obvious reference diverges** — the thing that looks right to copy is the wrong thing.
|
||||
2. **There is a trap** — something that looks right and is not, or something that must explicitly
|
||||
not be done, and why.
|
||||
3. **An ordering or invariant constraint** that could not be inferred from reading the files.
|
||||
4. **A decision was already taken**, with its reason, so nobody re-litigates it and the reviewer can
|
||||
check the result against it.
|
||||
|
||||
Everything else is an anchor: the reference implementation to follow, the files and areas touched,
|
||||
the specs to write, the open questions. Name the anchor and stop.
|
||||
|
||||
The test on any bullet is _which of the four reasons is this here for?_ If the answer is
|
||||
"completeness", delete it. Argument lists, method signatures and file skeletons are derivable from
|
||||
the reference implementation faster than they can be written down, and they are wrong by the time
|
||||
anyone reads them.
|
||||
|
||||
Depth is **earned by risk**, never applied uniformly. A layer whose verdict in Step 4 came out clean
|
||||
and boring gets an anchor and one line. A layer with a trap gets as much as the trap needs.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — What a task issue contains
|
||||
|
||||
Two audiences: a human who wants to know in thirty seconds what this is and why it exists, and
|
||||
whoever implements it, starting from the body without redoing the research.
|
||||
|
||||
In this order:
|
||||
|
||||
1. **Lead paragraph** — what this piece of work is and where it sits: the reference implementation
|
||||
it follows, the sibling it stays in sync with, the state of the code today. No heading.
|
||||
2. **The plan** — grouped by file or area, at the depth Step 5 allows.
|
||||
3. **Done means** — one sentence naming the observable state that makes this task finished. This is
|
||||
what a reviewer judges against, and what an implementing agent stops at. It is not a copy of the
|
||||
story's acceptance criteria.
|
||||
4. **Open questions** — real ones only, each with a recommendation and, if it blocks, who decides.
|
||||
Never a question without a proposed answer.
|
||||
5. **Tests** — the spec files to add or extend and the cases they must cover, including edge cases
|
||||
found during research. Say when coverage does not exist today.
|
||||
6. **Dependencies** — what exactly is needed from which sibling. Only for siblings that already
|
||||
exist; refer to not-yet-created ones in prose.
|
||||
7. **Parent reference** — always the last line.
|
||||
|
||||
Sections 1, 3, 5 and 7 are mandatory. A small task is a lead paragraph, three bullets, a "Done
|
||||
means" line, a "Tests" line and the parent reference.
|
||||
|
||||
### Rules
|
||||
|
||||
- **Nothing invented.** Every path, class, setting and pattern named in the plan was opened in the
|
||||
checkout. If something could not be verified, say so in that bullet.
|
||||
- **No copy of the story.** The task states what _it_ changes; the story is one click away.
|
||||
- **No estimates, no assignees, no due dates.** That is the board's job.
|
||||
- **No customer or personal data**, and no reference to a private support ticket beyond its number.
|
||||
- One task is what one person can finish without waiting on a sibling, and what a reviewer can judge
|
||||
on its own. It is deliberately **not** defined in terms of merge requests — how the work is
|
||||
branched and merged is a separate decision that does not belong in a breakdown.
|
||||
|
||||
### What a spike issue contains
|
||||
|
||||
A **spike** is an issue type of its own, a sibling of `Task` under the same story — not a task with
|
||||
a different name. Its output is an **answer**, so the task contract above does not fit it: there is
|
||||
no plan of how to solve the thing, because what to solve is the open question. A spike carries:
|
||||
|
||||
1. **The question** — one sentence, answerable, naming the decision it unblocks.
|
||||
2. **Why it cannot be answered now** — what was already read or tried, so the spike does not start
|
||||
from zero.
|
||||
3. **What an answer looks like** — the form the result has to take: a recommendation with its
|
||||
reason, a measurement, a throwaway proof of concept. Name which one.
|
||||
4. **Done means** — the answer is written down _in the spike_, in its body or as a comment. A spike
|
||||
whose result exists only in someone's head is not finished, and a spike does not finish by
|
||||
producing production code.
|
||||
5. **What it blocks** — the sibling tasks or criteria waiting on the answer. A spike that blocks
|
||||
nothing is research, and research is not part of a breakdown.
|
||||
6. **Parent reference** — always the last line.
|
||||
|
||||
All six are mandatory; a spike is short enough that none of them is a burden. Its traceability runs
|
||||
the other way round from a task's: a spike traces to the **question** — the criterion or the Step 4
|
||||
layer verdict that could not be planned — not to a criterion it implements.
|
||||
|
||||
### Coverage before anything is created
|
||||
|
||||
- Every acceptance criterion is covered by at least one task, and every task traces to a criterion.
|
||||
A task tracing to nothing is either invented work or a criterion nobody wrote down — both are
|
||||
findings, not things to resolve silently. A criterion that is blocked by a spike is covered by
|
||||
that spike for now, and the fact that it has no task yet is recorded, not left to be noticed.
|
||||
- Every layer verdict from Step 4 is reflected: affected rows have a task, unaffected rows are
|
||||
recorded as unaffected. An affected row that cannot be planned yet is covered by the **blocking
|
||||
spike** instead — the spike names the row it holds, and the implementing task is created once the
|
||||
spike is answered. A row covered by neither is the gap this check exists to catch.
|
||||
- Decisions taken in the story's comments are reflected, and outrank the body.
|
||||
- What the story leaves out stays out. Where the story is vague, that becomes an open question in
|
||||
the affected task, not a silent decision.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Writing back to the story
|
||||
|
||||
A caller may improve the story it is working on. Two sections behave differently, and the
|
||||
distinction is not cosmetic.
|
||||
|
||||
**Acceptance criteria are a contract.** `code_review_rules.md` enumerates them verbatim and gives
|
||||
each one a verdict, months later. Rewording a criterion silently changes what gets verified.
|
||||
Criteria may only be added, changed or removed after an explicit confirmation from a human, one
|
||||
change at a time, with the reason recorded.
|
||||
|
||||
A story may also carry a **`Solution outline`**: a few lines on which areas and layers are affected,
|
||||
the shape of the approach, and what was ruled out. It is the only place the whole solution is visible
|
||||
without opening every task. It names areas and decisions, never files, classes or methods — those
|
||||
belong in the tasks, where they are checked against the checkout and can be corrected without
|
||||
touching product's artefact.
|
||||
|
||||
**Everything else may be improved freely** — background information, technical notes, links, a scope
|
||||
or out-of-scope section, a clarification section. Prefer adding a section over rewriting a
|
||||
paragraph.
|
||||
|
||||
Mechanics that apply to any write-back:
|
||||
|
||||
- **Edit by section. Never regenerate the body.** Sections with nothing to add stay byte-identical.
|
||||
- **Open questions live in the body, never in a comment.** They go under `Clarification needed`, as
|
||||
task items, because the body is current state: answering a question removes its item, and the
|
||||
answer becomes a criterion or background. A comment cannot do that — and comments are read
|
||||
downstream as settled context, so a question posted there is later taken for a decision.
|
||||
- **A body edit notifies nobody, but a comment is not automatic.** Post one when the change would
|
||||
surprise somebody who read the story before: a criterion changed or removed, scope narrowed or
|
||||
widened, a decision that reverses an earlier one. An addition that fills a gap needs none — the
|
||||
body shows it and the edit history keeps it. Which of the two a given change is, is a judgement for
|
||||
the caller; this file does not try to decide it.
|
||||
- **Never delete product's words** to make room. If the existing wording is wrong, propose the
|
||||
replacement; do not apply it silently.
|
||||
- Splitting a story is a **product act**. Propose the split, never execute it.
|
||||
|
||||
---
|
||||
|
||||
## Alignment with product direction
|
||||
|
||||
Two different things get called this, and only one is checkable.
|
||||
|
||||
**Mechanically checkable, no extra source needed:**
|
||||
|
||||
- Does the design compose existing design-system components and tokens, or invent new ones? Whether
|
||||
composing them is _required_ is unsettled (`.dev/agent_docs/product_principles.md#UI-6`), so a new
|
||||
primitive is raised as a question for UX, never reported as a violation.
|
||||
- Does a new source string near-duplicate an existing one? (`code_review_rules.md`, translatable
|
||||
strings.)
|
||||
- Does the story build new behaviour on REST and the legacy CoffeeScript interface? New features
|
||||
target Vue 3 and GraphQL, but the principle keeps an exception, so this is a finding **only when
|
||||
the story states no reason** — extending existing legacy behaviour is a reason, and a stated one
|
||||
closes the check (`.dev/agent_docs/product_principles.md#STACK-1`).
|
||||
- Does it need a pattern that does not exist in the codebase yet?
|
||||
|
||||
Two of these overlap a principle below. Where they do, the **principle's wording decides**,
|
||||
exceptions included; the bullet is only the prompt to look.
|
||||
|
||||
**Checkable against `product_principles.md`**, in this directory: it states what the product holds to
|
||||
be true, each principle as an assertion plus what it rules out, with stable IDs to cite
|
||||
(`.dev/agent_docs/product_principles.md#UI-5`). Check against it; do not restate it here.
|
||||
|
||||
Its **Unsettled** section is as important as the rest. A story that touches one of those questions
|
||||
gets a **question for a human**, never a verdict — and the distinction between "we decided against
|
||||
it" and "we never decided" only exists because that section is written down. If the file is missing,
|
||||
report product alignment as **not run**. Never reason about product direction from memory or from the
|
||||
shape of the existing code.
|
||||
|
||||
---
|
||||
|
||||
## What none of this decides
|
||||
|
||||
- Which repository sub-issues are filed in, which board they land on, and in which column. That is
|
||||
team configuration and belongs to the caller.
|
||||
- How the work is branched and merged.
|
||||
- Sizing in points or days. Nothing here produces an estimate.
|
||||
- Whether a story should exist at all. That is product's call, upstream of every rule in this file.
|
||||
@@ -1,128 +0,0 @@
|
||||
# Testing
|
||||
|
||||
## Strategy
|
||||
|
||||
- Test at the **lowest level** possible — unit over request over E2E
|
||||
- Focus on the object under test, mock dependencies where it makes sense
|
||||
- Only important user stories get E2E/system tests (Capybara + Selenium)
|
||||
- Service specs cover behavior, GraphQL specs only the GraphQL layer —
|
||||
see `.dev/agent_docs/testing_services_and_graphql.md`
|
||||
|
||||
## Backend (RSpec)
|
||||
|
||||
Tests live in `spec/` mirroring the `app/` structure.
|
||||
Factories are in `spec/factories/`.
|
||||
|
||||
```bash
|
||||
RAILS_ENV=test VITE_TEST_MODE=1 bundle exec rspec spec/path/to/file_spec.rb # Run file
|
||||
RAILS_ENV=test VITE_TEST_MODE=1 bundle exec rspec spec/path/to/file_spec.rb:42 # Run specific example
|
||||
```
|
||||
|
||||
Add `CI_SKIP_DB_RESET=1` to skip database reset between runs.
|
||||
Add `CI_SKIP_ASSETS_PRECOMPILE=1` to skip asset compilation —
|
||||
but omit it when frontend files have changed.
|
||||
|
||||
### RSpec meta attributes
|
||||
|
||||
- `authenticated_as:` — accepts `true` (admin), `false` (skip login),
|
||||
a User, Symbol, or lambda
|
||||
- `performs_jobs: true` — required to run ActiveJob jobs
|
||||
outside of `spec/jobs/`
|
||||
- `db_strategy: :reset` — reset database after examples that modify
|
||||
the schema
|
||||
|
||||
### System tests (Capybara + Selenium)
|
||||
|
||||
Set `SELENIUM_BROWSER_HEADLESS=1` when running system specs — otherwise
|
||||
Selenium opens a real, visible browser window.
|
||||
|
||||
```bash
|
||||
SELENIUM_BROWSER_HEADLESS=1 RAILS_ENV=test VITE_TEST_MODE=1 bundle exec rspec spec/system/path/to/file_spec.rb
|
||||
```
|
||||
|
||||
### Ports of a test run
|
||||
|
||||
The suite serves Capybara on `CAPYBARA_PORT` (default `3001`) and its websocket
|
||||
server on `WS_PORT` (default `6043`) — next to, never on, the ports of the
|
||||
development stack, so a run does not collide with a stack of the same checkout.
|
||||
Set both to run two suites side by side; they still share the `test` database,
|
||||
the Redis keys and the Elasticsearch indices, so those (`DATABASE_URL`,
|
||||
`REDIS_URL`, `ES_INDEX`) need separating as well.
|
||||
|
||||
```bash
|
||||
CAPYBARA_PORT=3101 WS_PORT=6143 bundle exec rspec spec/system/path/to/file_spec.rb
|
||||
```
|
||||
|
||||
### Capybara form helpers (new frontend stack)
|
||||
|
||||
System tests for the Vue frontend use custom helpers to interact
|
||||
with FormKit-based fields:
|
||||
|
||||
```ruby
|
||||
find_input('Title').type('text')
|
||||
find_select('Owner').select_option('Agent Name')
|
||||
find_autocomplete('Customer').search_for_option(
|
||||
customer.email, label: customer.fullname
|
||||
)
|
||||
find_editor('Text').type('content')
|
||||
find_treeselect('Category').select_option('Parent::Child')
|
||||
find_datepicker('Date').select_date(Date.tomorrow)
|
||||
find_toggle('Boolean').toggle_on
|
||||
```
|
||||
|
||||
Use `within_form` for stable multi-field interactions — it automatically
|
||||
waits for form updater (Core Workflow) responses:
|
||||
|
||||
```ruby
|
||||
within_form(form_updater_gql_number: 2) do
|
||||
find_autocomplete('CC').search_for_options([email_1, email_2])
|
||||
find_editor('Text').type(body)
|
||||
end
|
||||
```
|
||||
|
||||
For the new frontend system tests, use `wait_for_gql` to wait for
|
||||
GraphQL responses and `wait_for_test_flag` to wait for frontend
|
||||
state changes.
|
||||
|
||||
Full reference:
|
||||
`doc/developer_manual/cookbook/how-to-test-with-rspec-and-capybara.md`
|
||||
|
||||
## Frontend (Vitest)
|
||||
|
||||
Test files use `.spec.ts` extension and are co-located with source files.
|
||||
|
||||
```bash
|
||||
pnpm test --run app/frontend/path/to/file.spec.ts
|
||||
```
|
||||
|
||||
Always pass `--run` and at least one file path. In an interactive terminal,
|
||||
watch mode is the default and does not exit. Never put `--` between `pnpm test` and the arguments: pnpm
|
||||
forwards it, Vitest then ignores everything after it and runs the complete
|
||||
suite. `VITE_TEST_MODE` is not needed, Vitest enables the test mode itself.
|
||||
|
||||
Tests use **Testing Library** (on top of Vue Test Utils).
|
||||
|
||||
### Two levels of frontend tests
|
||||
|
||||
- **Component/unit tests** — co-located with source files,
|
||||
test individual components or composables in isolation
|
||||
- **Frontend integration tests** — live in `__tests__/` folders
|
||||
inside page directories, render the full view with routing
|
||||
|
||||
GraphQL requests are mocked via the auto mocker. Activate it by
|
||||
importing `#tests/graphql/builders/mocks.ts` or by importing a
|
||||
`.mocks.ts` file next to a GraphQL operation.
|
||||
|
||||
Cypress is only used when pure Node.js testing is not possible
|
||||
(e.g. the editor). See
|
||||
`doc/developer_manual/cookbook/how-to-test-with-vitest-and-cypress.md`.
|
||||
|
||||
### Vitest utils
|
||||
|
||||
To handle some vue specific setup we use:
|
||||
|
||||
For component test -
|
||||
`app/frontend/tests/support/components/renderComponent.ts`
|
||||
|
||||
For integration test -
|
||||
`app/frontend/tests/support/components/visitView.ts`
|
||||
@@ -1,36 +0,0 @@
|
||||
# Service and GraphQL Specs
|
||||
|
||||
How to split spec coverage between a service and the query, mutation or
|
||||
subscription that calls it. For everything else about tests see
|
||||
`.dev/agent_docs/testing.md`.
|
||||
|
||||
When a query, mutation or subscription delegates to a service, test each
|
||||
layer once. Write the service spec first.
|
||||
|
||||
Service spec (`spec/services/service/...`) — everything the service
|
||||
decides:
|
||||
|
||||
- what it writes, and what it leaves alone
|
||||
- failures, as the raised exception, plus the rollback
|
||||
- authorization it performs itself, and `requires_current_user!`
|
||||
- every form its arguments accept (a record as well as an id or code)
|
||||
|
||||
GraphQL spec (`spec/graphql/gql/{queries,mutations,subscriptions}/...`) —
|
||||
only what the GraphQL layer adds:
|
||||
|
||||
- schema-level rejections: missing or blank arguments, custom scalars
|
||||
- declarative authorization: `requires_permission`,
|
||||
`loads_pundit_method:`, and the shared example
|
||||
`graphql responds with error if unauthenticated`
|
||||
- how a service error reaches the client: user error with `message` and
|
||||
`field`, or a top-level error
|
||||
- the payload: which fields come back, in which locale
|
||||
- fields the types resolve themselves (batch loaders, connections,
|
||||
pagination) and their query-count tests
|
||||
|
||||
Do not repeat the service's matrix (roles, locales, permissions, tree
|
||||
shapes) in the GraphQL spec. One loose example that an argument reaches
|
||||
the service is enough. Name the service spec in a comment at the top.
|
||||
|
||||
Example: `spec/services/service/knowledge_base/category/create_spec.rb`
|
||||
and `spec/graphql/gql/mutations/knowledge_base/category/add_spec.rb`.
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description: Reviews uncommitted changes for convention violations, missing tests, unintended side effects, and implementation correctness. Invoke this after the Test phase, before Commit.
|
||||
---
|
||||
|
||||
# Code Reviewer
|
||||
|
||||
You are a code reviewer for the Zammad project.
|
||||
|
||||
Your job is to review the changes the main agent has just made — before they
|
||||
are committed — and report findings.
|
||||
|
||||
## Process
|
||||
|
||||
1. Determine what to review.
|
||||
- **Default** — invoked from the development workflow, after Test and before Commit —
|
||||
run `git diff HEAD` for all uncommitted changes, staged and unstaged.
|
||||
- **If the caller supplied a scope, use that instead.** The `code-review` skill is
|
||||
invoked by a person outside the workflow and may pass a branch-wide scope
|
||||
(`merge-base(develop, HEAD)` → working tree), where `git diff HEAD` would be empty.
|
||||
|
||||
Also check for untracked files, which appear in no diff. With no caller-supplied
|
||||
scope, run `git ls-files --others --exclude-standard` repository-wide. With a
|
||||
supplied scope, constrain that command to the same pathspecs (or the paths covered
|
||||
by the supplied diff) using `git ls-files --others --exclude-standard -- <paths>`.
|
||||
Do not include unrelated untracked files outside that scope.
|
||||
2. Identify which areas of the codebase the changes touch (CoffeeScript
|
||||
frontend, Vue 3 frontend, REST backend, GraphQL, services, migrations, etc.).
|
||||
3. Read `.dev/agent_docs/code_review_rules.md`. It routes you to the applicable
|
||||
`.dev/agent_docs/` reference docs for those areas — read only the ones that apply,
|
||||
not all of them.
|
||||
4. If the work relates to a GitHub issue, read it via `mcp__github__issue_read` to
|
||||
understand the original requirements and acceptance criteria.
|
||||
5. Review the diff and report findings.
|
||||
|
||||
## What to check
|
||||
|
||||
`.dev/agent_docs/code_review_rules.md` is the authority: it defines what qualifies as a
|
||||
finding, what is out of scope, how to classify findings, and how to write them.
|
||||
Apply it rather than your own judgement about what matters, and do not restate it
|
||||
here.
|
||||
|
||||
Three things from it deserve emphasis in this position specifically:
|
||||
|
||||
- **The flagging threshold.** You may be reviewing work in progress, where scaffolding
|
||||
and half-finished edges are expected. Report what meets the bar and nothing else.
|
||||
If there are no findings, say so explicitly. Do not invent issues to fill the review.
|
||||
- **You are not the author.** You run in a fresh context deliberately: you see the diff
|
||||
and the issue, not the reasoning that produced the code. Judge what is there against
|
||||
the conventions and the issue, and do not reconstruct or assume the author's intent —
|
||||
the rules file treats that as disqualifying for a finding either way.
|
||||
- **Leftover artifacts.** Debug code, `console.log`, `binding.pry`, commented-out
|
||||
blocks and stray TODOs survive into agent diffs more often than into human ones.
|
||||
This is the last checkpoint before they get committed.
|
||||
|
||||
## Output
|
||||
|
||||
Report findings as a structured list, grouped by severity in the order **P0**, **P1**,
|
||||
then **P2**. Label each finding with the category from the taxonomy in the rules file.
|
||||
For each finding include:
|
||||
|
||||
- File path and line number (if applicable)
|
||||
- A clear description of the issue
|
||||
- A concrete suggestion how to fix it
|
||||
|
||||
Anything at **P0** blocks the commit.
|
||||
|
||||
## Important
|
||||
|
||||
- You are READ-ONLY. Do not modify any files. Do not commit. Do not push.
|
||||
If a fix is obvious, describe it — do not apply it.
|
||||
- Be specific and actionable. Do not give vague feedback like "this could be better".
|
||||
- Verify your claims by reading the actual code before reporting.
|
||||
@@ -1,181 +0,0 @@
|
||||
# AI Agent Instructions
|
||||
|
||||
This file provides guidance on how to work with code in the Zammad repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Zammad is an open-source helpdesk/support platform. The backend is **Ruby on Rails**.
|
||||
There are two frontend stacks: the **current production frontend** is CoffeeScript
|
||||
(served via the Rails asset pipeline), and the **new frontend** is **Vue 3 + TypeScript**
|
||||
served via **Vite** (managed by pnpm), which is being built alongside it. **PostgreSQL** is
|
||||
used for persistence, **Redis** for ActionCable/GraphQL subscriptions, and **GraphQL** is the
|
||||
API layer between the new Vue apps and the backend.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
CoffeeScript frontend ──→ REST controllers ──→ Rails backend ──→ PostgreSQL
|
||||
↑
|
||||
Vue 3 frontend (desktop/mobile) ──→ GraphQL API ────┘
|
||||
```
|
||||
|
||||
New features target the Vue 3 + GraphQL stack.
|
||||
The CoffeeScript frontend uses REST controllers.
|
||||
|
||||
## Key Directories (non-standard)
|
||||
|
||||
- `app/services/service/` — Service objects encapsulating business logic
|
||||
- `app/graphql/gql/` — GraphQL schema, types, mutations, subscriptions
|
||||
- `app/policies/` — Authorization policies (Pundit)
|
||||
- `app/frontend/apps/desktop/` — Vue 3 desktop app
|
||||
- `app/frontend/apps/mobile/` — Vue 3 mobile app
|
||||
- `app/frontend/shared/` — Shared Vue components, composables, stores, GraphQL types, form system
|
||||
- `app/assets/javascripts/` — CoffeeScript frontend (Spine + jQuery, legacy, still actively maintained)
|
||||
- `lib/` — Library code, prefer minimal Rails coupling
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- All new files except Markdown files (`*.md`) must include the Zammad copyright header.
|
||||
- Never edit translation files (`i18n/*.po`) directly —
|
||||
translations are managed via translations.zammad.org.
|
||||
|
||||
### Code Comments
|
||||
|
||||
Comment only what the code cannot say itself: a non-obvious _why_, a
|
||||
workaround with its reason, a spec/protocol quirk, or a deliberate
|
||||
deviation from a pattern. Otherwise add no comment — clear naming and
|
||||
small methods are preferred over explanation.
|
||||
|
||||
- One or two short lines, in the style of the surrounding file.
|
||||
- Never restate what the next line does, summarise a block, label
|
||||
sections (`# --- setup ---`), or narrate a change (`# new`, `# was …`,
|
||||
`# fixed`).
|
||||
- Keep out reasoning that belongs in the commit message, the issue or
|
||||
the MR description.
|
||||
- Never reference an issue from an internal repository (`coordination-*`) —
|
||||
this repository is publicly mirrored. Reference a public `zammad/zammad`
|
||||
issue, or state the reason without a link.
|
||||
- Never mark a fix for a security vulnerability as such — this repository
|
||||
is publicly mirrored, and the comment points at the vulnerable code in
|
||||
installations that are not updated yet.
|
||||
|
||||
### UI Principles
|
||||
|
||||
- Icon-only buttons need `v-tooltip` supplying the accessible name.
|
||||
- Hover/focus-only visible actions need to display on touch fallback
|
||||
- Responsive grids should adapt to their container
|
||||
- Translatable strings: use complete, meaningful source strings,
|
||||
sentence case for English source strings, and check for existing
|
||||
near-duplicate strings before adding new ones.
|
||||
- Red marks the point of no return: `danger` and `remove` stay only on
|
||||
controls that act immediately. A trigger that only opens a confirmation
|
||||
dialog or a confirming flyout is not red: a button becomes `tertiary`
|
||||
with an icon naming the action (`trash3` for delete and discard,
|
||||
`arrow-counterclockwise` for reset), a menu item carries no variant, a
|
||||
`remove` icon button keeps its icon and takes the variant UX names per
|
||||
place. The decisive button in the dialog or flyout is `danger`.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
RAILS_ENV=test VITE_TEST_MODE=1 bundle exec rspec spec/path/to/file_spec.rb # Run specific RSpec test
|
||||
bundle exec rubocop --autocorrect app/path/to/file.rb # Lint specific Ruby file(s)
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Always use pnpm for frontend and cross-stack commands.
|
||||
|
||||
```bash
|
||||
pnpm test --run app/frontend/path/to/file.spec.ts # Run specific Vitest test (never add `--`)
|
||||
pnpm lint # Run all linters
|
||||
pnpm generate-graphql-api # Regenerate GraphQL types after schema changes
|
||||
pnpm generate-setting-types # Regenerate Config types after setting changes
|
||||
```
|
||||
|
||||
### Development stack
|
||||
|
||||
A browser-clickable instance for this checkout. Start it via `bin/dev` rather
|
||||
than `forego` directly, and wait for it to answer before using it.
|
||||
|
||||
```bash
|
||||
RAILS_ENV=development bundle exec rake db:create zammad:bootstrap:reset # once per checkout
|
||||
bin/dev # does not exit; run it in the background
|
||||
```
|
||||
|
||||
It serves on `localhost:3000`, with the websocket server on `6042` and Vite on
|
||||
`3036`. Export `ZAMMAD_RAILS_PORT`, `ZAMMAD_WEBSOCKET_PORT` and
|
||||
`VITE_RUBY_PORT` before `bin/dev` to move them — the whole stack has to agree
|
||||
on them, so export, do not set them per process.
|
||||
|
||||
`zammad:bootstrap:reset` migrates, seeds and runs the auto wizard, which creates
|
||||
`admin@example.com` / `test`. Plain `zammad:db:init` skips the wizard and leaves
|
||||
the browser on the getting-started screen. Never run `zammad:bootstrap:init` or
|
||||
`zammad:setup:db_config`: they overwrite `config/database.yml` and prompt on
|
||||
stdin, which hangs a non-interactive session.
|
||||
|
||||
The test suite uses its own ports (`3001` and `6043`) and its own database, so
|
||||
it can run while the stack is up. Several checkouts, however, share the
|
||||
development database and Redis — only one stack per machine unless
|
||||
`DATABASE_URL` and `REDIS_URL` say otherwise.
|
||||
|
||||
## Development Lifecycle
|
||||
|
||||
Issues are tracked on **GitHub**, code is developed on a self-hosted **GitLab**.
|
||||
GitHub hosts a read-only mirror. The agent uses the **GitHub MCP server** for
|
||||
issues and **`glab` CLI** for GitLab operations.
|
||||
|
||||
Stories are refined and broken down into task issues before this lifecycle
|
||||
starts (`/refine-story` → `/plan-story`). Bugs skip that and enter it directly.
|
||||
|
||||
Lifecycle: Understand → Research → Plan → Branch (`/prepare-issue-branch`) →
|
||||
Implement → Test → Review → Commit → MR (`/create-mr`) → Cherry-pick
|
||||
(`/cherry-pick-to-stable`)
|
||||
|
||||
See `.dev/agent_docs/development_workflow.md` for the full lifecycle and
|
||||
workflow rules.
|
||||
|
||||
## Agent Reference Docs
|
||||
|
||||
You MUST read the relevant file(s) below before responding when working on that area — do NOT read them all upfront:
|
||||
|
||||
- `.dev/agent_docs/graphql_patterns.md` — How to add/modify GraphQL types,
|
||||
mutations, queries, and subscriptions
|
||||
- `.dev/agent_docs/frontend_patterns.md` — Vue component conventions,
|
||||
composables, routing, and the form system
|
||||
- `.dev/agent_docs/frontend_addon_layer.md` — How an addon extends the Vue
|
||||
frontend via the build-time weave (Vapor-safe, multi-addon)
|
||||
- `.dev/agent_docs/legacy_model_patterns.md` — Concerns needed by new models
|
||||
used in the legacy stack: REST assets (one-to-one relations only) and
|
||||
client notifications
|
||||
- `.dev/agent_docs/testing.md` — How to write and run backend and frontend tests
|
||||
- `.dev/agent_docs/testing_services_and_graphql.md` — How to split spec
|
||||
coverage between a service and the query, mutation or subscription that
|
||||
calls it
|
||||
- `.dev/agent_docs/service_patterns.md` — Service object conventions and structure
|
||||
- `.dev/agent_docs/knowledge_base_patterns.md` — Knowledge base rules for the
|
||||
new stack (Vue/GraphQL/services) only: the single active knowledge base and
|
||||
the deliberate exceptions to it
|
||||
- `.dev/agent_docs/database_migrations.md` — How to write migrations and work
|
||||
with seeds
|
||||
|
||||
## CodeGraph
|
||||
|
||||
In repositories indexed by CodeGraph (a `.codegraph/` directory exists at the repo
|
||||
root), reach for it before grep/find or reading files when you need to understand
|
||||
or locate code:
|
||||
|
||||
- **MCP tool** (when available): `codegraph_explore` answers most code questions
|
||||
in one call — the relevant symbols' verbatim source plus the call paths between
|
||||
them, including dynamic-dispatch hops grep can't follow. Name a file or symbol
|
||||
in the query to read its current line-numbered source. If it's listed but
|
||||
deferred, load it by name via tool search.
|
||||
- **Shell** (always works): `pnpm exec codegraph explore "<symbol names or question>"`
|
||||
prints the same output using the repository-pinned CodeGraph version.
|
||||
|
||||
If there is no `.codegraph/` directory, skip CodeGraph entirely — indexing is the
|
||||
user's decision. If they ask to initialize it, run `pnpm codegraph:init`. Developer
|
||||
setup and removal are documented in
|
||||
`doc/developer_manual/development_environment/codegraph-setup.md`.
|
||||
@@ -1,92 +0,0 @@
|
||||
module.exports = class DetectTranslatableString
|
||||
|
||||
# coffeelint: disable=detect_translatable_string
|
||||
rule:
|
||||
name: 'detect_translatable_string'
|
||||
level: 'ignore'
|
||||
message: 'The following string looks like it should be marked as translatable via __(...)'
|
||||
description: '''
|
||||
'''
|
||||
|
||||
constructor: ->
|
||||
@callTokens = []
|
||||
|
||||
tokens: ['STRING', 'CALL_START', 'CALL_END']
|
||||
|
||||
lintToken: (token, tokenApi) ->
|
||||
[type, tokenValue] = token
|
||||
|
||||
if type in ['CALL_START', 'CALL_END']
|
||||
@trackCall token, tokenApi
|
||||
return
|
||||
|
||||
return false if @isInIgnoredMethod()
|
||||
|
||||
return @lintString(token, tokenApi)
|
||||
|
||||
lintString: (token, tokenApi) ->
|
||||
[type, tokenValue] = token
|
||||
|
||||
# Remove quotes.
|
||||
string = tokenValue[1..-2]
|
||||
|
||||
# Ignore strings with less than two words.
|
||||
return false if string.split(' ').length < 2
|
||||
|
||||
# Ignore strings that are being used as exception; unlike Ruby exceptions, these should not reach the user.
|
||||
return false if tokenApi.peek(-3)[1] == 'throw'
|
||||
return false if tokenApi.peek(-2)[1] == 'throw'
|
||||
return false if tokenApi.peek(-1)[1] == 'throw'
|
||||
|
||||
# Ignore strings that are being used for comparison
|
||||
return false if tokenApi.peek(-1)[1] == '=='
|
||||
|
||||
# String interpolation is handled via concatenation, ignore such strings.
|
||||
return false if tokenApi.peek(1)[1] == '+'
|
||||
return false if tokenApi.peek(2)[1] == '+'
|
||||
|
||||
BLOCKLIST = [
|
||||
# Only look at strings starting with upper case letters
|
||||
/^[^A-Z]/,
|
||||
# # Ignore strings starting with three upper case letters like SELECT, POST etc.
|
||||
# /^[A-Z]{3}/,
|
||||
]
|
||||
|
||||
return false if BLOCKLIST.some (entry) ->
|
||||
#console.log([string, entry, string.match(entry), token, tokenApi.peek(-1), tokenApi.peek(1)])
|
||||
string.match(entry)
|
||||
|
||||
# console.log(tokenApi.peek(-3))
|
||||
# console.log(tokenApi.peek(-2))
|
||||
# console.log(tokenApi.peek(-1))
|
||||
# console.log(token)
|
||||
|
||||
return { context: "Found: #{token[1]}" }
|
||||
|
||||
ignoredMethods: {
|
||||
'__': true,
|
||||
'log': true,
|
||||
'T': true,
|
||||
'controllerBind': true,
|
||||
'debug': true, # App.Log.debug
|
||||
'error': true, # App.Log.error
|
||||
'set': true, # App.Config.set
|
||||
'translateInline': true,
|
||||
'translateContent': true,
|
||||
'translatePlain': true,
|
||||
}
|
||||
|
||||
isInIgnoredMethod: ->
|
||||
#console.log(@callTokens)
|
||||
for t in @callTokens
|
||||
return true if t.isIgnoredMethod
|
||||
return false
|
||||
|
||||
trackCall: (token, tokenApi) ->
|
||||
if token[0] is 'CALL_START'
|
||||
p = tokenApi.peek(-1)
|
||||
token.isIgnoredMethod = p and @ignoredMethods[p[1]]
|
||||
@callTokens.push(token)
|
||||
else
|
||||
@callTokens.pop()
|
||||
return null
|
||||
@@ -1,91 +0,0 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { initPlugin as initVisualRegressionPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins'
|
||||
import pkg from '../../package.json' with { type: 'json' }
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const isCYCI = !process.env.CY_OPEN
|
||||
const root = resolve(dir, '../..')
|
||||
|
||||
// we don't need to optimize graphql and apollo
|
||||
const skipDeps = ['graphql', 'apollo', '@tiptap/pm']
|
||||
|
||||
export default defineConfig({
|
||||
videosFolder: '.dev/cypress/videos',
|
||||
supportFolder: '.dev/cypress/support/index.js',
|
||||
fixturesFolder: '.dev/cypress/fixtures',
|
||||
downloadsFolder: '.dev/cypress/downloads',
|
||||
screenshotsFolder: '.dev/cypress/screenshots',
|
||||
videoCompression: false,
|
||||
// Cypress 16 defaults to `0`, which types faster than the rich text editor can
|
||||
// process input events.
|
||||
keystrokeDelay: 10,
|
||||
// Cypress 16 defaults to `modern`, which relies on `Element.checkVisibility()`
|
||||
// and therefore reports elements moved out of the viewport via `transform` as
|
||||
// visible.
|
||||
visibilityStrategy: 'legacy',
|
||||
expose: {
|
||||
CY_CI: isCYCI,
|
||||
pluginVisualRegressionDiffConfig: {
|
||||
threshold: 0.02,
|
||||
},
|
||||
pluginVisualRegressionMaxDiffThreshold: 0.02,
|
||||
},
|
||||
component: {
|
||||
supportFile: '.dev/cypress/support/index.js',
|
||||
setupNodeEvents(on, config) {
|
||||
on('after:spec', (spec, results) => {
|
||||
if (results && results.stats.failures === 0 && results.video) {
|
||||
return rm(results.video)
|
||||
}
|
||||
})
|
||||
initVisualRegressionPlugin(on, config)
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser?.family === 'chromium' && browser?.name !== 'electron') {
|
||||
launchOptions.args.push('--force-device-scale-factor=2')
|
||||
}
|
||||
return launchOptions
|
||||
})
|
||||
},
|
||||
devServer: {
|
||||
framework: 'vue',
|
||||
bundler: 'vite',
|
||||
viteConfig: {
|
||||
mode: 'cypress',
|
||||
root,
|
||||
configFile: resolve(dir, '../..', 'vite.config.mjs'),
|
||||
cacheDir: resolve(dir, 'node_modules', '.vite'),
|
||||
server: {
|
||||
fs: {
|
||||
strict: false,
|
||||
},
|
||||
hmr: true,
|
||||
...(isCYCI && { watch: { ignored: ['**/*'] } }),
|
||||
},
|
||||
optimizeDeps: {
|
||||
entries: [
|
||||
'**/cypress/**/*.cy.ts',
|
||||
'!**/node_modules/**',
|
||||
'!**/*.d.ts',
|
||||
],
|
||||
include: [
|
||||
// if you see cypress failing on some dependency, add it to skipDeps
|
||||
...Object.keys(pkg.dependencies).filter(
|
||||
(name) => !skipDeps.some((dep) => name.includes(dep)),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// iPhone 12 viewport
|
||||
viewportWidth: 390,
|
||||
viewportHeight: 844,
|
||||
fileServerFolder: '..',
|
||||
indexHtmlFile: '.dev/cypress/support/component-index.html',
|
||||
specPattern: ['app/frontend/cypress/**/*.cy.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
retries: 0,
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@frsource/cypress-plugin-visual-regression-diff": "^4.2.1",
|
||||
"@testing-library/cypress": "^10.1.3",
|
||||
"cypress": "^16.1.1",
|
||||
"cypress-real-events": "^1.15.1"
|
||||
},
|
||||
"imports": {
|
||||
"#tests/*": "../../app/frontend/tests/*",
|
||||
"#shared/*": "../../app/frontend/shared/*",
|
||||
"#mobile/*": "../../app/frontend/apps/mobile/*",
|
||||
"#desktop/*": "../../app/frontend/apps/desktop/*",
|
||||
"#cy/*": "./*"
|
||||
}
|
||||
}
|
||||
1871
.dev/cypress/pnpm-lock.yaml
generated
1871
.dev/cypress/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
---
|
||||
allowBuilds:
|
||||
cypress: true
|
||||
sharp: true
|
||||
|
||||
# Restore old default behavior like in pnpm v10.
|
||||
verifyDepsBeforeRun: false
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import '@testing-library/cypress/add-commands'
|
||||
import 'cypress-real-events/support'
|
||||
import '@frsource/cypress-plugin-visual-regression-diff'
|
||||
|
||||
import { configure } from '@testing-library/cypress'
|
||||
import { mount } from 'cypress/vue'
|
||||
|
||||
configure({ testIdAttribute: 'data-test-id' })
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
/**
|
||||
* Simulates a paste event.
|
||||
* Modified from https://gist.github.com/nickytonline/bcdef8ef00211b0faf7c7c0e7777aaf6
|
||||
*
|
||||
* @param subject A jQuery context representing a DOM element.
|
||||
* @param pasteOptions Set of options for a simulated paste event.
|
||||
* @param pasteOptions.pastePayload Simulated data that is on the clipboard.
|
||||
* @param pasteOptions.pasteFormat The format of the simulated paste payload. Default value is 'text'.
|
||||
* @param pasteOptions.files A list of assisiated file, if any
|
||||
*
|
||||
* @returns The subject parameter.
|
||||
*
|
||||
* @example
|
||||
* cy.get('body').paste({
|
||||
* pasteType: 'application/json',
|
||||
* pastePayload: {hello: 'yolo'},
|
||||
* });
|
||||
*/
|
||||
Cypress.Commands.add('paste', { prevSubject: true }, function onPaste(subject, pasteOptions) {
|
||||
const { pastePayload = '', pasteType = 'text', files = [] } = pasteOptions
|
||||
const data = pasteType === 'application/json' ? JSON.stringify(pastePayload) : pastePayload
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData(pasteType, data)
|
||||
files.forEach((file) => {
|
||||
clipboardData.items.add(file)
|
||||
})
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event
|
||||
const pasteEvent = new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataType: pasteType,
|
||||
data,
|
||||
clipboardData,
|
||||
})
|
||||
subject[0].dispatchEvent(pasteEvent)
|
||||
|
||||
return subject
|
||||
})
|
||||
|
||||
Cypress.Commands.add('selectText', { prevSubject: true }, (subject, direction, size) => {
|
||||
return cy
|
||||
.wrap(subject)
|
||||
.realPress([
|
||||
'Shift',
|
||||
...new Array(size).fill(direction === 'right' ? 'ArrowRight' : 'ArrowLeft'),
|
||||
])
|
||||
})
|
||||
|
||||
// :TODO - remove this helper if we decide in the long run to not reintroduce the uniqueID extension from tiptap
|
||||
const normalizeHTML = (html) =>
|
||||
html
|
||||
// strip node ids from unique ids
|
||||
.replaceAll(/\sdata-id="[^"]*"/g, '')
|
||||
|
||||
Cypress.Commands.add(
|
||||
'shouldContainNormalizedHtml',
|
||||
{ prevSubject: 'element' },
|
||||
(subject, expected) => {
|
||||
const actual = normalizeHTML(subject[0].innerHTML)
|
||||
const wanted = normalizeHTML(expected)
|
||||
expect(actual).to.contain(wanted)
|
||||
},
|
||||
)
|
||||
|
||||
Cypress.Commands.add(
|
||||
'shouldHaveNormalizedHtml',
|
||||
{ prevSubject: 'element' },
|
||||
(subject, expected) => {
|
||||
const actual = normalizeHTML(subject[0].innerHTML)
|
||||
const wanted = normalizeHTML(expected)
|
||||
expect(actual).to.equal(wanted)
|
||||
},
|
||||
)
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en-gb">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Zammad</title>
|
||||
</head>
|
||||
<style>
|
||||
body,
|
||||
html,
|
||||
#loading-app,
|
||||
[data-cy-root] {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<body class="bg-black font-sans text-sm text-white antialiased">
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import './prepare.js'
|
||||
|
||||
import '#mobile/styles/main.css'
|
||||
import '#shared/components/CommonIcon/injectIcons.ts'
|
||||
|
||||
import '../../../public/assets/frontend/fonts.css'
|
||||
|
||||
import './commands.js'
|
||||
|
||||
// @testing-library/cypress uses env to display errors
|
||||
globalThis.process.env = {
|
||||
DEBUG_PRINT_LIMIT: 5000,
|
||||
}
|
||||
|
||||
Cypress.Screenshot.defaults({ capture: 'viewport' })
|
||||
|
||||
if (Cypress.expose('CY_CI')) {
|
||||
Cypress.config('defaultCommandTimeout', 20000)
|
||||
}
|
||||
|
||||
beforeEach(() => document.fonts.ready)
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
window.__ = (str) => str
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
/**
|
||||
* Simulates a paste event.
|
||||
* Modified from https://gist.github.com/nickytonline/bcdef8ef00211b0faf7c7c0e7777aaf6
|
||||
*
|
||||
* @param pasteOptions.pastePayload Simulated data that is on the clipboard.
|
||||
* @param pasteOptions.pasteFormat The format of the simulated paste payload. Default value is 'text'.
|
||||
* @param pasteOptions.files A list of assisted file, if any
|
||||
*
|
||||
* @returns The subject parameter.
|
||||
*
|
||||
* @example
|
||||
* cy.get('body').paste({
|
||||
* pasteType: 'application/json',
|
||||
* pastePayload: {hello: 'yolo'},
|
||||
* });
|
||||
* @param options
|
||||
*/
|
||||
paste(options: {
|
||||
pastePayload?: string
|
||||
pasteFormat?: string
|
||||
files?: File[]
|
||||
}): Chainable<Subject>
|
||||
selectText(direction: 'left' | 'right', size: number): Chainable<Subject>
|
||||
matchImage(
|
||||
options?: Partial<{
|
||||
// screenshot configuration, passed directly to the the Cypress screenshot method: https://docs.cypress.io/api/cypress-api/screenshot-api#Arguments
|
||||
// default: { }
|
||||
screenshotConfig: Partial<Cypress.ScreenshotOptions>
|
||||
// pixelmatch options, see: https://www.npmjs.com/package/pixelmatch#pixelmatchimg1-img2-output-width-height-options
|
||||
// default: { includeAA: true }
|
||||
diffConfig: Partial<{
|
||||
// Matching threshold, ranges from 0 to 1. Smaller values make the comparison more sensitive. 0.1 by default.
|
||||
threshold: number
|
||||
// If true, disables detecting and ignoring anti-aliased pixels.
|
||||
includeAA: boolean
|
||||
// Blending factor of unchanged pixels in the diff output. Ranges from 0 for pure white to 1 for original brightness. 0.1 by default.
|
||||
alpha: number
|
||||
// The color of anti-aliased pixels in the diff output in [R, G, B] format. [255, 255, 0] by default.
|
||||
aaColor: [number, number, number]
|
||||
// The color of differing pixels in the diff output in [R, G, B] format. [255, 0, 0] by default.
|
||||
diffColor: [number, number, number]
|
||||
// An alternative color to use for dark on light differences to differentiate between "added" and "removed" parts. If not provided, all differing pixels use the color specified by diffColor. null by default.
|
||||
diffColorAlt: [number, number, number] | null
|
||||
// Draw the diff over a transparent background (a mask), rather than over the original image. Will not draw anti-aliased pixels (if detected).
|
||||
diffMask: boolean
|
||||
}>
|
||||
// whether to update images automatically, without making a diff - useful for CI
|
||||
// default: false
|
||||
updateImages: boolean
|
||||
// directory path in which screenshot images will be stored
|
||||
// image visualiser will normalise path separators depending on OS it's being run within, so always use / for nested paths
|
||||
// default: '__image_snapshots__'
|
||||
imagesDir: string
|
||||
// maximum threshold above which the test should fail
|
||||
// default: 0.01
|
||||
maxDiffThreshold: number
|
||||
// forces scale factor to be set as value "1"
|
||||
// helps with screenshots being scaled 2x on high-density screens like Mac Retina
|
||||
// default: true
|
||||
forceDeviceScaleFactor: boolean
|
||||
// title used for naming the image file
|
||||
// default: Cypress.currentTest.titlePath (your test title)
|
||||
title: string
|
||||
}>,
|
||||
): Chainable<Subject>
|
||||
mount: (typeof import('cypress/vue'))['mount']
|
||||
}
|
||||
|
||||
interface Chainable<Subject> {
|
||||
shouldContainNormalizedHtml(expected: string): Chainable<Subject>
|
||||
shouldHaveNormalizedHtml(expected: string): Chainable<Subject>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import { merge } from 'lodash-es'
|
||||
|
||||
import { initializeAppName } from '#shared/composables/useAppName.ts'
|
||||
import initializeStore from '#shared/stores/index.ts'
|
||||
import initializeGlobalComponents from '#shared/initializer/globalComponents.ts'
|
||||
import initializeGlobalProperties from '#shared/initializer/globalProperties.ts'
|
||||
import { initializeForm, initializeFormFields } from '#mobile/form/index.ts'
|
||||
import { initializeGlobalComponentStyles } from '#mobile/initializer/initializeGlobalComponentStyles.ts'
|
||||
import { initializeMobileIcons } from '#mobile/initializer/initializeMobileIcons.ts'
|
||||
import { initializeMobileVisuals } from '#mobile/initializer/mobileVisuals.ts'
|
||||
|
||||
// imported only for types
|
||||
// for some reason adding it to tsconfig doesn't work
|
||||
import '@testing-library/cypress'
|
||||
import 'cypress-real-events'
|
||||
import './types/commands.ts'
|
||||
|
||||
import type { FormSchemaField } from '#shared/components/Form/types.ts'
|
||||
import { FormKit } from '@formkit/vue'
|
||||
import type { mount } from 'cypress/vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import type { App } from 'vue'
|
||||
|
||||
import { cacheInitializerModules } from '#mobile/server/apollo/index.ts'
|
||||
import createCache from '#shared/server/apollo/cache.ts'
|
||||
|
||||
import { createMockClient } from 'mock-apollo-client'
|
||||
import { provideApolloClient } from '@vue/apollo-composable'
|
||||
import { useSessionStore } from '#shared/stores/session.ts'
|
||||
import { useApplicationStore } from '#shared/stores/application.ts'
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory('/'),
|
||||
routes: [{ path: '/', component: { template: '<div />' } }],
|
||||
})
|
||||
|
||||
const mockPermissions = (permissions: string[]) => {
|
||||
const session = useSessionStore()
|
||||
if (!session.user) {
|
||||
session.user = {
|
||||
id: '123',
|
||||
internalId: 1,
|
||||
objectAttributeValues: [],
|
||||
}
|
||||
}
|
||||
|
||||
session.user!.permissions = { names: permissions }
|
||||
}
|
||||
|
||||
const mockApplicationConfig = (config: Record<string, unknown>) => {
|
||||
const app = useApplicationStore()
|
||||
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
app.config[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
export const mountComponent: typeof mount = (
|
||||
component: any,
|
||||
options: any,
|
||||
): Cypress.Chainable => {
|
||||
const plugins = []
|
||||
plugins.push(() => {
|
||||
initializeAppName('mobile')
|
||||
})
|
||||
plugins.push(initializeStore)
|
||||
plugins.push(initializeGlobalComponentStyles)
|
||||
plugins.push(initializeGlobalComponents)
|
||||
plugins.push(initializeGlobalProperties)
|
||||
plugins.push(initializeMobileIcons)
|
||||
plugins.push(initializeForm)
|
||||
plugins.push(initializeFormFields)
|
||||
plugins.push(initializeMobileVisuals)
|
||||
plugins.push((app: App) => router.install(app))
|
||||
|
||||
return cy.mount(component, merge({ global: { plugins } }, options)).then(() => {
|
||||
if (options?.config) mockApplicationConfig(options.config)
|
||||
if (options?.permissions) mockPermissions(options.permissions)
|
||||
})
|
||||
}
|
||||
|
||||
export const mockApolloClient = () => {
|
||||
const client = createMockClient({
|
||||
cache: createCache(cacheInitializerModules),
|
||||
queryDeduplication: true,
|
||||
})
|
||||
provideApolloClient(client)
|
||||
return client
|
||||
}
|
||||
|
||||
export const mountFormField = (
|
||||
field: string,
|
||||
props?: Partial<FormSchemaField> & Record<string, unknown>,
|
||||
) => {
|
||||
return mountComponent(FormKit, {
|
||||
props: {
|
||||
name: field,
|
||||
type: field,
|
||||
...props,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface CheckFormMatchOptions {
|
||||
subTitle?: string
|
||||
type?: string
|
||||
wrapperSelector?: string
|
||||
assertion?: (subject: JQuery<HTMLElement>) => void
|
||||
}
|
||||
|
||||
export const checkFormMatchesSnapshot = (
|
||||
options: CheckFormMatchOptions = {},
|
||||
) => {
|
||||
const title = options?.subTitle
|
||||
? `${Cypress.currentTest.title} - ${options.subTitle}`
|
||||
: Cypress.currentTest.title
|
||||
const wrapperSelector = options?.wrapperSelector || '.formkit-outer'
|
||||
|
||||
return cy.wrap(document.fonts.ready).then(() => {
|
||||
cy.wrap(new Promise((resolve) => setTimeout(resolve))).then(() => {
|
||||
cy.get(wrapperSelector)
|
||||
.should(options?.assertion || (() => {}))
|
||||
.matchImage({
|
||||
title,
|
||||
imagesDir: options?.type
|
||||
? `__image_snapshots__/${options.type}`
|
||||
: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Cypress Visual Regression Tests
|
||||
|
||||
## Updating Image Snapshots
|
||||
|
||||
This command only _updates_ snapshots. Cypress will run only tests with `*-visual.cy` in their name, and will not fail,
|
||||
if snapshot is not correct, but will generate a new snapshot instead.
|
||||
|
||||
```sh
|
||||
# from the root directory
|
||||
pnpm cypress:snapshots
|
||||
```
|
||||
|
||||
Please, make sure snapshot is actually correct before pushing your changes.
|
||||
|
||||
## Running Tests
|
||||
|
||||
Although you may run visual tests using `pnpm test:ci:ct` command, this will execute them in your current development
|
||||
environment. However, stored image snapshots were generated in Docker containers in order to achieve consistent
|
||||
environment every time. Due to multiple factors (i.e. screen density, font resolution, browser version, etc), your
|
||||
mileage may vary and you may receive false negatives.
|
||||
|
||||
It is therefore advisable to run the visual regression tests in the same environment where the image snapshots are
|
||||
normally updated. You can do this by setting an environment variable and running the snapshots command:
|
||||
|
||||
```sh
|
||||
# from the root directory
|
||||
CYPRESS_UPDATE_SNAPSHOTS=false pnpm cypress:snapshots
|
||||
```
|
||||
|
||||
## Multiple Architecture (AMD64 vs ARM)
|
||||
|
||||
Cypress doesn't work inside amd64 image, if it's running on ARM processor, even under `--platform=linux/amd64`. So we
|
||||
have two compose files, and based on user processor we run one or the other.
|
||||
|
||||
ARM compose file has a custom cache folder, so it doesn't conflict with Mac's usual `~/Library/Cache` folder. It's
|
||||
meant to store cache inside the project folder, but Linux cache can be theoretically shared.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
volumes:
|
||||
visual-regression-dependencies-amd64:
|
||||
|
||||
services:
|
||||
cypress:
|
||||
environment:
|
||||
CYPRESS_UPDATE_SNAPSHOTS: ${CYPRESS_UPDATE_SNAPSHOTS:-true}
|
||||
platform: linux/amd64
|
||||
image: cypress/base:latest
|
||||
ipc: host
|
||||
volumes:
|
||||
- ../../..:/app
|
||||
- visual-regression-dependencies-amd64:/app/node_modules
|
||||
- ~/.cache/Cypress:/root/.cache/Cypress
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "443:443"
|
||||
command: bash ./.dev/cypress/visual-regression/run.sh
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
volumes:
|
||||
visual-regression-dependencies-arm:
|
||||
|
||||
services:
|
||||
cypress:
|
||||
environment:
|
||||
CYPRESS_CACHE_FOLDER: $PWD/.cache/Cypress
|
||||
CYPRESS_UPDATE_SNAPSHOTS: ${CYPRESS_UPDATE_SNAPSHOTS:-true}
|
||||
platform: linux/arm64
|
||||
image: cypress/base:latest
|
||||
ipc: host
|
||||
volumes:
|
||||
- ../../..:/app
|
||||
- visual-regression-dependencies-arm:/app/node_modules
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "443:443"
|
||||
command: bash ./.dev/cypress/visual-regression/run.sh
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
corepack enable pnpm
|
||||
|
||||
pnpm install --frozen-lockfile --ignore-scripts
|
||||
pnpm cypress:install
|
||||
pnpm test:ci:ct --expose pluginVisualRegressionUpdateImages=$CYPRESS_UPDATE_SNAPSHOTS --spec '../../**/*-visuals.cy.*'
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $(uname -m) == 'arm64' ]]; then
|
||||
# cannot run cypress while on arm, but in amd container
|
||||
docker compose -f .dev/cypress/visual-regression/docker-compose.arm.yml up --exit-code-from cypress
|
||||
else
|
||||
docker compose -f .dev/cypress/visual-regression/docker-compose.amd64.yml up --exit-code-from cypress
|
||||
fi
|
||||
@@ -1,46 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
#
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# Ticket.where(state: open_states).order(...)
|
||||
#
|
||||
# # good
|
||||
# Ticket.where(state: open_states).reorder(...)
|
||||
# default_scope { order(:prio, :id) }
|
||||
# scope :sorted, -> { order(position: :asc) }
|
||||
|
||||
class ActiveRecordReorder < Base
|
||||
extend AutoCorrector
|
||||
|
||||
def_node_matcher :active_record_order_call?, <<-PATTERN
|
||||
$(send _ {:order} _ ...)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :default_scope_call?, <<-PATTERN
|
||||
$(send _ {:default_scope} ...)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :scope_call?, <<-PATTERN
|
||||
$(send _ {:scope} sym ...)
|
||||
PATTERN
|
||||
|
||||
MSG = "Prefer 'reorder' over 'order' to prevent issues with default ordering.".freeze
|
||||
|
||||
def on_send(node)
|
||||
return if active_record_order_call?(node).nil?
|
||||
return if default_scope_call?(node.parent&.children&.first)
|
||||
return if scope_call?(node.parent&.parent)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node.loc.selector, :reorder)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
module CanCheck51PlusMigrations
|
||||
VERSION_THRESHOLD = 5.1
|
||||
|
||||
# Apply to 7.0 migrations and newer
|
||||
MIGRATION_THRESHOLD = 20250519095403 # rubocop:disable Style/NumericLiterals
|
||||
|
||||
def migration_51_or_newer?(node)
|
||||
return false if old_migration?
|
||||
|
||||
migration?(node) && rails_51_or_newer?(node)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def migration?(node)
|
||||
superclass(node)&.children&.at(0)&.const_name == 'ActiveRecord::Migration'
|
||||
end
|
||||
|
||||
def old_migration?
|
||||
file_timestamp = File.basename(processed_source.file_path)[%r{\A\d+}]
|
||||
|
||||
return false if !file_timestamp
|
||||
|
||||
file_timestamp.to_i < MIGRATION_THRESHOLD
|
||||
end
|
||||
|
||||
def rails_51_or_newer?(node)
|
||||
klass = superclass(node)
|
||||
|
||||
return false if !klass.send_type?
|
||||
|
||||
klass.arguments.first.value.to_f >= VERSION_THRESHOLD
|
||||
end
|
||||
|
||||
def superclass(node)
|
||||
node
|
||||
.each_ancestor(:class)
|
||||
.first
|
||||
&.parent_class
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'can_check_namespace'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
module CanCheckGqlOperation
|
||||
include CanCheckNamespace
|
||||
|
||||
DISALLOW_IN = %w[Gql::Mutations Gql::Queries Gql::Subscriptions].freeze
|
||||
|
||||
def inside_gql_operation?(node)
|
||||
inside_namespace?(node, *DISALLOW_IN)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
module CanCheckNamespace
|
||||
def namespace_path(node)
|
||||
node.each_ancestor(:class, :module).filter_map do |ancestor|
|
||||
ancestor.identifier&.const_name
|
||||
end
|
||||
end
|
||||
|
||||
def inside_namespace?(node, *namespaces)
|
||||
path = namespace_path(node).reverse.join('::')
|
||||
|
||||
namespaces.any? { |ns| path.start_with?(ns) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# This cop checks if migration file name begins with a valid timestamp
|
||||
# https://github.com/zammad/zammad/issues/3702
|
||||
class CorrectMigrationTimestamp < Base
|
||||
MSG = 'Migration filename must begin with a valid timestamp'.freeze
|
||||
|
||||
def on_new_investigation
|
||||
file_path = processed_source.file_path
|
||||
|
||||
return if !migration?(file_path)
|
||||
return if config.file_to_exclude?(file_path) || config.allowed_camel_case_file?(file_path)
|
||||
return if filename_good?(file_path)
|
||||
|
||||
add_global_offense(MSG)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def migration?(file_path)
|
||||
match_path? %r{(?<!spec/)db/(migrate|addon/[^/]+)/.+\.rb}, file_path
|
||||
end
|
||||
|
||||
def filename_good?(file_path)
|
||||
filename = File.basename(file_path)
|
||||
|
||||
# the check makes trouble in old addons which do not match the rubocop.
|
||||
# since we can't disable the rubocop for whatever reason is was the only solution.
|
||||
return true if filename[0..3].to_i < 2021
|
||||
|
||||
filename.match? %r{^20\d{12}_}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,91 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class DetectTranslatableString < Base
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'This string looks like it should be marked as translatable via __(...).'.freeze
|
||||
|
||||
def on_str(node)
|
||||
# Constants like __FILE__ are handled as strings, but don't respond to begin.
|
||||
return if !node.loc.respond_to?(:begin) || !node.loc.begin
|
||||
return if part_of_ignored_node?(node)
|
||||
|
||||
return if !offense?(node)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, "__(#{node.source})")
|
||||
end
|
||||
end
|
||||
|
||||
def on_regexp(node)
|
||||
ignore_node(node)
|
||||
end
|
||||
|
||||
METHOD_NAME_BLOCKLIST = %i[
|
||||
__ translate
|
||||
include? eql? parse
|
||||
debug info warn error fatal unknown log log_error
|
||||
field argument description value has_one belongs_to
|
||||
].freeze
|
||||
|
||||
def on_send(node)
|
||||
ignore_node(node) if METHOD_NAME_BLOCKLIST.include? node.method_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
PARENT_SOURCE_BLOCKLIST = [
|
||||
# Ignore logged strings
|
||||
'Rails.logger'
|
||||
].freeze
|
||||
|
||||
NODE_START_BLOCKLIST = [
|
||||
# Only look at strings starting with upper case letters
|
||||
%r{[^A-Z]},
|
||||
# Ignore strings starting with three upper case letters like SELECT, POST etc.
|
||||
%r{[A-Z]{3}},
|
||||
].freeze
|
||||
|
||||
NODE_CONTAIN_BLOCKLIST = [
|
||||
# Ignore strings with interpolation.
|
||||
'#{',
|
||||
# Ignore Email addresses
|
||||
'@'
|
||||
].freeze
|
||||
|
||||
def offense?(node)
|
||||
|
||||
# Ignore Hash Keys
|
||||
return false if node.parent.type.eql?(:pair) && node.parent.children.first.equal?(node)
|
||||
|
||||
# Ignore equality checks like ... == 'My String'
|
||||
return false if node.left_sibling.eql?(:==)
|
||||
|
||||
# Remove quotes
|
||||
node_source = node.source[1..-2]
|
||||
|
||||
# Only match strings with at least two words
|
||||
return false if node_source.split.count < 2
|
||||
|
||||
NODE_START_BLOCKLIST.each do |entry|
|
||||
return false if node_source.start_with? entry
|
||||
end
|
||||
|
||||
NODE_CONTAIN_BLOCKLIST.each do |entry|
|
||||
return false if node_source.include? entry
|
||||
end
|
||||
|
||||
parent_source = node.parent.source
|
||||
PARENT_SOURCE_BLOCKLIST.each do |entry|
|
||||
return false if parent_source.include? entry
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class EnforceInModal < Base
|
||||
|
||||
MSG = "Prefer `in_modal` over `within '.modal'`.".freeze
|
||||
|
||||
def on_send(node)
|
||||
|
||||
return if node.method_name != :within
|
||||
|
||||
first_arg = node.arguments.first
|
||||
return if first_arg.type != :str
|
||||
# Remove quotes
|
||||
return if !first_arg.source[1..-2].start_with? '.modal'
|
||||
|
||||
add_offense(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,63 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# This spec ensures that time zones in system specs are set at the top level only.
|
||||
# The problem is if time zone changes in the middle of the spec, browser does not pick it up.
|
||||
# This leads to confusing issues when whole file vs specific portion are executed.
|
||||
#
|
||||
# Non-system tests are ignored. Timezone in pure RSPec tests can be set at any moment.
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# RSpec.describe 'Test', type: :system do
|
||||
# it 'is fine', time_zone: 'Europe/London' do
|
||||
# example
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # good
|
||||
# RSpec.describe 'Test', type: :system, time_zone: 'Europe/London' do
|
||||
# it 'is fine' do
|
||||
# example
|
||||
# end
|
||||
# end
|
||||
class EnsuresTimeZoneAtSpecTopLevel < Base
|
||||
def_node_matcher :is_rspec_block_with_time_zone?, <<-PATTERN
|
||||
$(send _ {:describe :context :it :shared_examples} (_ ...) (hash <(pair (sym :time_zone) (str...))...>))
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :is_system_type?, <<-PATTERN
|
||||
$(block (send (const _ :RSpec) :describe (_ ...) (hash <(pair (sym :type) (sym :system))...>)...)...)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :is_rspec_top_level_block?, <<-PATTERN
|
||||
$(send (const _ :RSpec) :describe...)
|
||||
PATTERN
|
||||
|
||||
MSG = 'RSpec system tests (aka Capybara) should set custom time zones at top level only'.freeze
|
||||
|
||||
def on_send(node)
|
||||
return if !is_rspec_block_with_time_zone?(node)
|
||||
return if is_rspec_top_level_block?(node)
|
||||
return if !in_system_spec?(node)
|
||||
|
||||
add_offense(node)
|
||||
end
|
||||
|
||||
def in_system_spec?(node)
|
||||
top_parent = node.parent
|
||||
loop do
|
||||
break if !top_parent
|
||||
return true if is_system_type?(top_parent)
|
||||
|
||||
top_parent = top_parent.parent
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# This cop is used to identify usages of `find_by` in conditions and
|
||||
# changes them to use `exists?` instead.
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# if User.find_by(name: 'Rubocop')
|
||||
# if !User.find_by(name: 'Rubocop')
|
||||
# unless User.find_by(name: 'Rubocop')
|
||||
# if find_by(name: 'Rubocop')
|
||||
# if !find_by(name: 'Rubocop')
|
||||
# unless find_by(name: 'Rubocop')
|
||||
#
|
||||
# # good
|
||||
# if User.exists?(name: 'Rubocop')
|
||||
# if !User.exists?(name: 'Rubocop')
|
||||
# unless User.exists?(name: 'Rubocop')
|
||||
# if exists?(name: 'Rubocop')
|
||||
# if !exists?(name: 'Rubocop')
|
||||
# unless exists?(name: 'Rubocop')
|
||||
class ExistsCondition < Base
|
||||
|
||||
def_node_matcher :find_by_condition?, <<-PATTERN
|
||||
{
|
||||
$(send $_ :find_by ...)
|
||||
(send $(send $_ :find_by ...) :!)
|
||||
}
|
||||
PATTERN
|
||||
|
||||
MSG = 'Use `%{prefer}` instead of `%{current}`.'.freeze
|
||||
|
||||
def on_if(node)
|
||||
check_for_find_by(node)
|
||||
end
|
||||
|
||||
def on_while(node)
|
||||
check_for_find_by(node)
|
||||
end
|
||||
|
||||
def on_while_post(node)
|
||||
check_for_find_by(node)
|
||||
end
|
||||
|
||||
def on_until(node)
|
||||
check_for_find_by(node)
|
||||
end
|
||||
|
||||
def on_until_post(node)
|
||||
check_for_find_by(node)
|
||||
end
|
||||
|
||||
def autocorrect(node)
|
||||
lambda do |corrector|
|
||||
corrector.replace(node.loc.selector, 'exists?')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_for_find_by(node)
|
||||
cond = condition(node)
|
||||
handle_node(cond)
|
||||
end
|
||||
|
||||
def check_node(node)
|
||||
return if !node
|
||||
|
||||
if keyword_bang?(node)
|
||||
receiver, = *node
|
||||
|
||||
handle_node(receiver)
|
||||
elsif node.operator_keyword?
|
||||
node.each_child_node { |op| handle_node(op) }
|
||||
elsif node.begin_type? && node.children.one?
|
||||
handle_node(node.children.first)
|
||||
end
|
||||
end
|
||||
|
||||
def keyword_bang?(node)
|
||||
node.respond_to?(:keyword_bang?) && node.keyword_bang?
|
||||
end
|
||||
|
||||
def handle_node(node)
|
||||
if node.send_type?
|
||||
check_offense(*find_by_condition?(node))
|
||||
elsif %i[and or begin].include?(node.type)
|
||||
check_node(node)
|
||||
end
|
||||
end
|
||||
|
||||
def condition(node)
|
||||
if node.send_type?
|
||||
node.receiver
|
||||
else
|
||||
node.condition
|
||||
end
|
||||
end
|
||||
|
||||
def check_offense(method_call = nil, receiver = nil)
|
||||
return if method_call.nil?
|
||||
|
||||
add_offense(method_call,
|
||||
message: format(MSG,
|
||||
prefer: replacement(receiver),
|
||||
current: current(receiver)))
|
||||
end
|
||||
|
||||
def current(node)
|
||||
node.respond_to?(:source) ? "#{node.source}.find_by" : 'find_by'
|
||||
end
|
||||
|
||||
def replacement(node)
|
||||
node.respond_to?(:source) ? "#{node.source}.exists?" : 'exists?'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ExistsDateTimePrecision < Base
|
||||
def_node_matcher :column?, <<-PATTERN
|
||||
$(send _ {:add_column :change_column} (sym _) (sym _) (sym :datetime) ... )
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :column_limit?, <<-PATTERN
|
||||
(send ... (hash <(pair (sym :limit) $(int 3)) ...> ) )
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :table?, <<-PATTERN
|
||||
$(block
|
||||
(send _ {:create_table :alter_table} ... )
|
||||
...
|
||||
)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :table_column?, <<-PATTERN
|
||||
$(send (:lvar _) {:timestamps :datetime} ... )
|
||||
PATTERN
|
||||
|
||||
MSG = 'Columns of type :timestamps and :datetime needs to have limit: 3.'.freeze
|
||||
|
||||
=begin
|
||||
|
||||
This rubocop will match all change_column/add_column/create_table/alter_table statements
|
||||
and check if there are :datetime or :timestamps column which do not have the limit: 3 setting.
|
||||
|
||||
good:
|
||||
|
||||
change_column :smime_certificates, :not_after_at, :datetime, limit: 3
|
||||
|
||||
create_table :sessions do |t|
|
||||
t.timestamps limit: 3, null: false
|
||||
end
|
||||
|
||||
bad:
|
||||
|
||||
change_column :smime_certificates, :not_after_at, :datetime, limit: 4
|
||||
|
||||
create_table :sessions do |t|
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
=end
|
||||
|
||||
def on_send(node)
|
||||
return add_offense(node) if invalid_column?(node)
|
||||
|
||||
add_offense(node) if invalid_table?(node)
|
||||
end
|
||||
|
||||
def invalid_table?(node)
|
||||
table?(node&.parent&.parent) && table_column?(node) && !column_limit?(node)
|
||||
end
|
||||
|
||||
def invalid_column?(node)
|
||||
column?(node) && !column_limit?(node)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ExistsDbStrategy < Base
|
||||
def_node_matcher :migration_execute?, <<-PATTERN
|
||||
$(send {(const (const _ :ObjectManager ) :Attribute) (... :described_class)} :migration_execute)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :create_attribute?, <<-PATTERN
|
||||
$(send _ :create_attribute ...)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :is_block?, <<-PATTERN
|
||||
$(block ...)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :has_reset?, <<-PATTERN
|
||||
$(send _ {:describe :context :it :shared_examples} (_ ...) (hash <(pair (sym :db_strategy) (sym {:reset})) ...> ))
|
||||
PATTERN
|
||||
|
||||
MSG = 'Add a `db_strategy: :reset` to your context/describe when you are creating object manager attributes!'.freeze
|
||||
|
||||
def on_send(node)
|
||||
return if !migration_execute?(node) && !create_attribute?(node)
|
||||
|
||||
reset = false
|
||||
node_parent = node.parent
|
||||
until node_parent.nil?
|
||||
if is_block?(node_parent) && has_reset?(node_parent.children[0])
|
||||
reset = true
|
||||
break
|
||||
end
|
||||
node_parent = node_parent.parent
|
||||
end
|
||||
|
||||
return if reset
|
||||
|
||||
add_offense(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,92 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ExistsResetColumnInformation < Base
|
||||
def_node_matcher :table?, <<-PATTERN
|
||||
$(send _ :change_table ... )
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :column?, <<-PATTERN
|
||||
$(send _ {:add_column :change_column :rename_column :remove_column} ... )
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :reset?, <<-PATTERN
|
||||
$( ... :reset_column_information )
|
||||
PATTERN
|
||||
|
||||
def message(class_name)
|
||||
"Changes of the schema need to be followed by a #{class_name}.reset_column_information to have the right state of the db in the migration and future migrations."
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
This rubocop will match all add, change, rename and remove column statements
|
||||
and check if there are reset_column_information function calls existing for the model.
|
||||
|
||||
good:
|
||||
|
||||
change_column :smime_certificates, :not_after_at, :datetime, limit: 3
|
||||
|
||||
SMIMECertificate.reset_column_information
|
||||
|
||||
bad:
|
||||
|
||||
change_column :smime_certificates, :not_after_at, :datetime, limit: 3
|
||||
|
||||
=end
|
||||
|
||||
def column_classes
|
||||
@column_classes ||= {}
|
||||
end
|
||||
|
||||
def table_classes
|
||||
@table_classes ||= {}
|
||||
end
|
||||
|
||||
def reset_classes
|
||||
@reset_classes ||= {}
|
||||
end
|
||||
|
||||
def column_class(node)
|
||||
node.children[2].children[0].to_s.classify
|
||||
end
|
||||
|
||||
def reset_class(node)
|
||||
# simplify namespaced class names
|
||||
# Rubocop can't reliably convert table names to namespaced class names
|
||||
node.children[0].const_name.gsub '::', ''
|
||||
end
|
||||
|
||||
def table_class(node)
|
||||
node.children[2].children[0].to_s.classify
|
||||
end
|
||||
|
||||
def on_send(node)
|
||||
if column?(node)
|
||||
column_classes[column_class(node)] = node
|
||||
elsif table?(node)
|
||||
table_classes[table_class(node)] = node
|
||||
elsif reset?(node)
|
||||
reset_classes[reset_class(node)] = node
|
||||
end
|
||||
end
|
||||
|
||||
def on_investigation_end
|
||||
column_classes.each do |key, value|
|
||||
next if reset_classes.key?(key)
|
||||
|
||||
add_offense(value, message: message(key))
|
||||
end
|
||||
table_classes.each do |key, value|
|
||||
next if reset_classes.key?(key)
|
||||
|
||||
add_offense(value, message: message(key))
|
||||
end
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
#
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# Faker::Number.number(...)
|
||||
# Faker::Name.first_name
|
||||
# Faker::Date.between(from: Date.parse("2022-01-01"), to: Date.parse("2024-01-01"))
|
||||
# Faker::Time.between(from: Date.parse("2022-01-01"), to: Date.parse("2024-01-01"))
|
||||
#
|
||||
# # good
|
||||
# Faker::Number.unique.number(...)
|
||||
# Faker::Name.unique.first_name
|
||||
# Faker::Date.unique.between(from: Date.parse("2022-01-01"), to: Date.parse("2024-01-01"))
|
||||
# Faker::Time.unique.between(from: Date.parse("2022-01-01"), to: Date.parse("2024-01-01"))
|
||||
|
||||
class FakerUnique < Base
|
||||
extend AutoCorrector
|
||||
|
||||
def_node_matcher :faker_call?, <<-PATTERN
|
||||
$(send (const (const _ :Faker) {:Name :Number :Date :Time}) _ ...)
|
||||
PATTERN
|
||||
|
||||
MSG = 'Always use Faker::*::.unique to prevent race conditions in tests.'.freeze
|
||||
|
||||
def on_send(node)
|
||||
return if faker_call?(node).nil?
|
||||
|
||||
method = node.children[1]
|
||||
return if method.name == 'unique'
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node.loc.selector, "unique.#{method.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidCallingServiceDirectly < RuboCop::Cop::Base
|
||||
MSG = 'Service classes can only be called with `execute` class method or by chaining `Service.with_current_user(user).execute`. Calling `new` is forbidden.'.freeze
|
||||
|
||||
def on_send(node)
|
||||
receiver = node.receiver
|
||||
return if !receiver
|
||||
return if !receiver.source.start_with?('Service::')
|
||||
|
||||
case node.method_name
|
||||
when :new
|
||||
add_offense(node, message: MSG)
|
||||
when :with_current_user
|
||||
if node.parent&.send_type? && node.parent.method_name == :execute
|
||||
return
|
||||
end
|
||||
|
||||
add_offense(node, message: MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidDefSend < Base
|
||||
MSG = <<~ERROR_MESSAGE.freeze
|
||||
Please avoid 'def send' if possible, as it overlaps with Ruby's built-in 'send' method. Consider alternatives such as 'deliver' instead.
|
||||
ERROR_MESSAGE
|
||||
|
||||
def on_defs(node)
|
||||
children = node.children
|
||||
add_offense(node) if children.first.type == :self && children[1] == :send
|
||||
end
|
||||
|
||||
def on_def(node)
|
||||
return if node.operator_method?
|
||||
|
||||
add_offense(node) if node.method_name.eql?(:send)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidDefaultScope < Base
|
||||
MSG = <<~ERROR_MESSAGE.freeze
|
||||
In zammad it is only allowed to set very simple default scopes which order the rows by default. E.g.
|
||||
|
||||
default_scope { order(:id) }
|
||||
|
||||
Any other complex forms of this are not allowed anymore. For more information see:
|
||||
|
||||
https://rails-bestpractices.com/posts/2013/06/15/default_scope-is-evil/
|
||||
ERROR_MESSAGE
|
||||
|
||||
def_node_matcher :default_scope?, <<-PATTERN
|
||||
$(block $(:send _ :default_scope) ...)
|
||||
PATTERN
|
||||
|
||||
SENDS = %w[default_scope order].freeze
|
||||
|
||||
def on_block(node)
|
||||
return if !default_scope?(node)
|
||||
return if node.to_s.scan(%r{send\snil\s:(.+)\b}).flatten.all? { |name| SENDS.include?(name) }
|
||||
|
||||
add_offense(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidLoofahDocument < Base
|
||||
MSG = 'Do not use Loofah.document. Use explicit parser version'.freeze
|
||||
|
||||
def_node_matcher :loofah_document?, <<~PATTERN
|
||||
(send (const nil? :Loofah) :document)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
add_offense(node, message: MSG) if loofah_document?(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidLoofahFragment < Base
|
||||
MSG = 'Do not use Loofah.fragment. Use explicit parser version'.freeze
|
||||
|
||||
def_node_matcher :loofah_fragment?, <<~PATTERN
|
||||
(send (const nil? :Loofah) :fragment)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
add_offense(node, message: MSG) if loofah_fragment?(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidRand < Base
|
||||
MSG = <<~ERROR_MESSAGE.freeze
|
||||
Please avoid 'rand' if possible. It does not guarantee uniqueness which means that there is a risk of collisions. Possible alternatives:
|
||||
- If you need unique values, consider using 'SecureRandom.uuid'.
|
||||
- To randomly select a value from a list, use [].sample.
|
||||
- To generate random test data that does not need to be unique, you can use 'Faker::*'.
|
||||
ERROR_MESSAGE
|
||||
|
||||
def on_send(node)
|
||||
add_offense(node) if node.method_name.eql? :rand
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidRedisClient < Base
|
||||
MSG = 'Do not use Redis.new directly. Use Zammad::Service::Redis.new instead.'.freeze
|
||||
|
||||
def_node_matcher :redis_client_usage?, <<~PATTERN
|
||||
(send (const nil? :Redis) :new)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
add_offense(node, message: MSG) if redis_client_usage?(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class ForbidTranslatableMarker < Base
|
||||
MSG = <<~ERROR_MESSAGE.freeze
|
||||
Don't use __() in Zammad core migrations. Translatable strings should be marked where they are defined, e.g. in the DB seeds.
|
||||
ERROR_MESSAGE
|
||||
|
||||
def on_send(node)
|
||||
add_offense(node) if node.method_name.eql? :__
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# `Exceptions::UnprocessableEntity` is deprecated and will be removed in Zammad 8.0.
|
||||
# Use `Exceptions::UnprocessableContent` instead.
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# raise Exceptions::UnprocessableEntity, 'message'
|
||||
# rescue Exceptions::UnprocessableEntity => e
|
||||
#
|
||||
# # good
|
||||
# raise Exceptions::UnprocessableContent, 'message'
|
||||
# rescue Exceptions::UnprocessableContent => e
|
||||
class ForbidUnprocessableEntity < Base
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Use `Exceptions::UnprocessableContent` instead of the deprecated `Exceptions::UnprocessableEntity`.'.freeze
|
||||
|
||||
def_node_matcher :unprocessable_entity?, <<~PATTERN
|
||||
(const (const {nil? cbase} :Exceptions) :UnprocessableEntity)
|
||||
PATTERN
|
||||
|
||||
def on_const(node)
|
||||
return if !unprocessable_entity?(node)
|
||||
|
||||
add_offense(node, message: MSG) do |corrector|
|
||||
corrector.replace(node.loc.name, 'UnprocessableContent')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'concerns/can_check_gql_operation'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class GraphqlAuthorizeAllowPublicAccess < Base
|
||||
include CanCheckGqlOperation
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Replace `authorize` method that returns `true` with `allow_public_access!`.'.freeze
|
||||
|
||||
def on_defs(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
# Check for: def self.authorize(...) true end
|
||||
return if node.method_name != :authorize
|
||||
return if !returns_true?(node.body)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, 'allow_public_access!')
|
||||
end
|
||||
end
|
||||
|
||||
def on_def(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
# Check for: def authorize(...) true end
|
||||
return if node.method_name != :authorize
|
||||
return if !returns_true?(node.body)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, 'allow_public_access!')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def returns_true?(body)
|
||||
return false if body.nil?
|
||||
|
||||
# Handle simple case: def authorize(...); true; end
|
||||
return true if body.type == :true # rubocop:disable Lint/BooleanSymbol
|
||||
|
||||
# Handle multi-statement case: def authorize(...); puts "foo"; true; end
|
||||
if body.type == :begin
|
||||
last_statement = body.children.last
|
||||
return last_statement&.type == :true # rubocop:disable Lint/BooleanSymbol
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'concerns/can_check_gql_operation'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class GraphqlAuthorizeDisallowed < Base
|
||||
include CanCheckGqlOperation
|
||||
|
||||
MSG = 'GraphQL operations must not define an `self.authorize` method. Use helpers or authorized?'.freeze
|
||||
|
||||
def on_defs(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
return if node.method_name != :authorize
|
||||
|
||||
add_offense(node.loc.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'concerns/can_check_gql_operation'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class GraphqlAuthorizeRequiresPermission < Base
|
||||
include CanCheckGqlOperation
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Replace `authorize` method with `requires_permission` when checking user permission.'.freeze
|
||||
|
||||
def on_def(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
return if node.method_name != :authorize
|
||||
|
||||
handle_authorize_method(node)
|
||||
end
|
||||
|
||||
def on_defs(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
return if node.method_name != :authorize
|
||||
|
||||
handle_authorize_method(node)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_authorize_method(node)
|
||||
permission_name = extract_permission(node)
|
||||
return if permission_name.nil?
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, "requires_permission #{permission_name.source}")
|
||||
end
|
||||
end
|
||||
|
||||
def extract_permission(method_node)
|
||||
body = method_node.body
|
||||
return nil if body.nil?
|
||||
return nil if !valid_permissions_call?(body)
|
||||
return nil if !valid_receiver_chain?(body)
|
||||
return nil if !valid_method_parameter?(body, method_node)
|
||||
|
||||
arg = body.first_argument
|
||||
arg if arg&.str_type?
|
||||
end
|
||||
|
||||
def valid_permissions_call?(body)
|
||||
body.send_type? && body.method_name == :permissions?
|
||||
end
|
||||
|
||||
def valid_receiver_chain?(body)
|
||||
receiver = body.receiver
|
||||
return false if !receiver&.send_type?
|
||||
return false if receiver.method_name != :current_user
|
||||
|
||||
param_receiver = receiver.receiver
|
||||
param_receiver&.send_type? == false && %i[lvar identifier].include?(param_receiver.type)
|
||||
end
|
||||
|
||||
def valid_method_parameter?(body, method_node)
|
||||
param_receiver = body.receiver.receiver
|
||||
param_name = param_receiver.children.last
|
||||
method_params = method_node.arguments.map(&:name)
|
||||
|
||||
method_params.include?(param_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,100 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'concerns/can_check_gql_operation'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class GraphqlAuthorizedRequiresEnabledSetting < Base
|
||||
include CanCheckGqlOperation
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Replace `authorized?` method with `requires_enabled_setting` when checking Setting.get.'.freeze
|
||||
|
||||
def on_def(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
return if node.method_name != :authorized?
|
||||
|
||||
handle_authorized_method(node)
|
||||
end
|
||||
|
||||
def on_defs(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
return if node.method_name != :authorized?
|
||||
|
||||
handle_authorized_method(node)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_authorized_method(node)
|
||||
setting_names = collect_setting_names(node.body)
|
||||
return if setting_names.empty?
|
||||
return if other_calls?(node.body)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
replacement = setting_names
|
||||
.map { |name| "requires_enabled_setting #{name.source}" }
|
||||
.join("\n#{' ' * node.loc.column}")
|
||||
|
||||
corrector.replace(node, replacement)
|
||||
end
|
||||
end
|
||||
|
||||
def collect_setting_names(body)
|
||||
return [] if body.nil?
|
||||
|
||||
setting_names = []
|
||||
|
||||
traverse_nodes(body) do |node|
|
||||
next if !node.send_type?
|
||||
next if node.method_name != :get
|
||||
next if !node.receiver&.const_type?
|
||||
|
||||
# Get the constant name - for `Setting`, it's the last child
|
||||
const_name = node.receiver.children.last
|
||||
next if const_name != :Setting
|
||||
|
||||
arg = node.first_argument
|
||||
setting_names << arg if arg&.str_type?
|
||||
end
|
||||
|
||||
setting_names
|
||||
end
|
||||
|
||||
def other_calls?(body)
|
||||
return false if body.nil?
|
||||
|
||||
has_other = false
|
||||
|
||||
traverse_nodes(body) do |node|
|
||||
# Allow Setting.get calls
|
||||
if node.send_type? && node.method_name == :get &&
|
||||
node.receiver&.const_type? && node.receiver.children.last == :Setting
|
||||
next
|
||||
end
|
||||
|
||||
# Allow super
|
||||
next if node.zsuper_type?
|
||||
|
||||
# If we see any other send node, it's an additional method call
|
||||
if node.send_type?
|
||||
has_other = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
has_other
|
||||
end
|
||||
|
||||
def traverse_nodes(node, &)
|
||||
yield node if node.is_a?(RuboCop::AST::Node)
|
||||
|
||||
node.children.each do |child|
|
||||
traverse_nodes(child, &) if child.is_a?(RuboCop::AST::Node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'concerns/can_check_gql_operation'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class GraphqlCsrfSkipVerification < Base
|
||||
include CanCheckGqlOperation
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Replace `requires_csrf_verification?` method that returns `false` with `skip_csrf_verification!`.'.freeze
|
||||
|
||||
def on_defs(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
# Check for: def self.requires_csrf_verification?(...) false end
|
||||
return if node.method_name != :requires_csrf_verification?
|
||||
return if !returns_false?(node.body)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, 'skip_csrf_verification!')
|
||||
end
|
||||
end
|
||||
|
||||
def on_def(node)
|
||||
return if !inside_gql_operation?(node)
|
||||
# Check for: def requires_csrf_verification?(...) false end
|
||||
return if node.method_name != :requires_csrf_verification?
|
||||
return if !returns_false?(node.body)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, 'skip_csrf_verification!')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def returns_false?(body)
|
||||
return false if body.nil?
|
||||
|
||||
# Handle simple case: def requires_csrf_verification?; false; end
|
||||
return true if body.type == :false # rubocop:disable Lint/BooleanSymbol
|
||||
|
||||
# Handle multi-statement case: def requires_csrf_verification?; puts "foo"; false; end
|
||||
if body.type == :begin
|
||||
last_statement = body.children.last
|
||||
return last_statement&.type == :false # rubocop:disable Lint/BooleanSymbol
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class GraphqlForbidSensitiveFields < Base
|
||||
MSG = 'Avoid declaring sensitive field `:%{name}` in a GraphQL return type — it may leak secrets to the API consumer. Use `# rubocop:disable Zammad/GraphqlForbidSensitiveFields` with a justification if intentional.'.freeze
|
||||
|
||||
# Inspired by config/initializers/filter_parameter_logging.rb.
|
||||
# Matched as substrings against the (snake_case) field name —
|
||||
# `passw` therefore also catches `password`/`passwd`, and `cert`
|
||||
# also catches `certificate`. The `_key` substring from the Rails
|
||||
# filter list is too broad for a static check (e.g. `cache_key`,
|
||||
# `foreign_key`), so concrete sensitive `_key` names are listed.
|
||||
SENSITIVE_SUBSTRINGS = %w[
|
||||
passw
|
||||
passphrase
|
||||
secret
|
||||
token
|
||||
api_key
|
||||
private_key
|
||||
auth_key
|
||||
access_key
|
||||
master_key
|
||||
signing_key
|
||||
crypt
|
||||
salt
|
||||
cert
|
||||
otp
|
||||
ssn
|
||||
cvv
|
||||
cvc
|
||||
credential
|
||||
bind_pw
|
||||
].freeze
|
||||
|
||||
# Match `field` and the relation helpers in HasModelRelations
|
||||
# (belongs_to/has_one/lookup_field), which all forward to `field`
|
||||
# under the hood and therefore also expose a return-type field.
|
||||
def_node_matcher :graphql_field?, <<~PATTERN
|
||||
(send nil? {:field :belongs_to :has_one :lookup_field} (sym $_) ...)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
graphql_field?(node) do |field_name|
|
||||
name_string = field_name.to_s
|
||||
next if SENSITIVE_SUBSTRINGS.none? { |needle| name_string.include?(needle) }
|
||||
|
||||
add_offense(node.first_argument, message: format(MSG, name: name_string))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/can_check_51_plus_migrations'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class MigrationEnsureIntegerForeignKey < Base
|
||||
extend AutoCorrector
|
||||
include CanCheck51PlusMigrations
|
||||
|
||||
MSG = 'Rails 5.1+ migrations must add type: :integer to references setup.'
|
||||
|
||||
RESTRICT_ON_SEND = %i[
|
||||
references
|
||||
belongs_to
|
||||
add_reference
|
||||
add_references
|
||||
].freeze
|
||||
|
||||
def on_send(node)
|
||||
return if !migration_51_or_newer?(node)
|
||||
|
||||
options = node.arguments.last
|
||||
|
||||
pair_with_type = nil
|
||||
|
||||
if options&.hash_type?
|
||||
options.pairs.each do |pair|
|
||||
next if !(pair.key.sym_type? && pair.key.value == :type)
|
||||
|
||||
pair_with_type = pair
|
||||
next if pair.value.source == ':integer'
|
||||
next if pair.value.source == ':bigint' # allow explicit bigint foreign keys
|
||||
|
||||
add_offense(pair.value) do |corrector|
|
||||
corrector.replace(pair.value, ':integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return if pair_with_type
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.insert_after(node.arguments.last.source_range, ', type: :integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'concerns/can_check_51_plus_migrations'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class MigrationEnsureIntegerPrimaryKey < Base
|
||||
extend AutoCorrector
|
||||
include CanCheck51PlusMigrations
|
||||
|
||||
MSG = 'Rails 5.1+ migrations must add id: :integer to ensure primary key is integer, not bigint.'
|
||||
|
||||
RESTRICT_ON_SEND = %i[create_table].freeze
|
||||
|
||||
def on_send(node)
|
||||
return if !migration_51_or_newer?(node)
|
||||
|
||||
options = node.arguments.last
|
||||
|
||||
pair_with_id = nil
|
||||
|
||||
if options&.hash_type?
|
||||
options.pairs.each do |pair|
|
||||
next if !(pair.key.sym_type? && pair.key.value == :id)
|
||||
|
||||
pair_with_id = pair
|
||||
next if pair.value.source == ':integer'
|
||||
next if pair.value.source == 'false' # allow id: false for ID-less tables
|
||||
next if pair.value.source == ':bigserial' # allow id: :bigserial for explicit bigint primary keys
|
||||
|
||||
add_offense(pair.value) do |corrector|
|
||||
corrector.replace(pair.value, ':integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return if pair_with_id
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.insert_after(node.arguments.first.source_range, ', id: :integer')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class MigrationSchedulerLastRun < Base
|
||||
MSG = "Make sure to set a last_run parameter so the migration will not fail, if it is migrated while the instance is running.\n e.g. last_run: Time.zone.now".freeze
|
||||
|
||||
def_node_matcher :scheduler_last_run_missing?, <<-PATTERN
|
||||
$(send (const _ :Scheduler) {:create_if_not_exists :create_or_update} ...)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
return if !scheduler_last_run_missing?(node)
|
||||
|
||||
params = node.children[2].keys.map(&:value)
|
||||
return if params.include?(:last_run)
|
||||
|
||||
add_offense(node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,42 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# This cop is used to identify usages of `.to_sym` on Strings and
|
||||
# changes them to use the `:` prefix instead.
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# "a-Symbol".to_sym
|
||||
# 'a-Symbol'.to_sym
|
||||
# "a-#{'Symbol'}".to_sym
|
||||
#
|
||||
# # good
|
||||
# :"a-Symbol"
|
||||
# :'a-Symbol'
|
||||
# :"a-#{'Symbol'}"
|
||||
class NoToSymOnString < Base
|
||||
extend AutoCorrector
|
||||
|
||||
def_node_matcher :to_sym?, <<-PATTERN
|
||||
{
|
||||
$(send (str ...) :to_sym ...)
|
||||
$(send (dstr ...) :to_sym ...)
|
||||
}
|
||||
PATTERN
|
||||
|
||||
MSG = "Don't use `.to_sym` on String. Prefer `:` prefix instead.".freeze
|
||||
|
||||
def on_send(node)
|
||||
result = *to_sym?(node)
|
||||
return if result.empty?
|
||||
|
||||
add_offense(node, message: MSG) do |corrector|
|
||||
corrector.replace(node, ":#{result.first.source}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
#
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# ActiveRecord::Base.descendants
|
||||
# ActiveRecord::Base
|
||||
# .descendants
|
||||
#
|
||||
# # good
|
||||
# Models.all.keys
|
||||
|
||||
class PreferModelsAll < Base
|
||||
extend AutoCorrector
|
||||
|
||||
def_node_matcher :active_record_descendants?, <<-PATTERN
|
||||
$(send (const (const {nil? cbase} :ActiveRecord) :Base) :descendants)
|
||||
PATTERN
|
||||
|
||||
MSG = 'Prefer `Models.all.keys` over `ActiveRecord::Base.descendants` to avoid issues with eager loading.'.freeze
|
||||
|
||||
def on_send(node)
|
||||
return if active_record_descendants?(node).nil?
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node, 'Models.all.keys')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# This cop is used to identify usages of `unless` conditionals
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# unless statement
|
||||
# return unless statement
|
||||
#
|
||||
# # good
|
||||
# if !statement
|
||||
# return if !statement
|
||||
class PreferNegatedIfOverUnless < Base
|
||||
include ConfigurableEnforcedStyle
|
||||
include NegativeConditional
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Favor `if !foobar` over `unless foobar` for ' \
|
||||
'negative conditions.'.freeze
|
||||
|
||||
def on_if(node)
|
||||
return if !node.unless?
|
||||
|
||||
add_offense(node, message: MSG)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
class TimezoneDefault < Base
|
||||
def on_str(node)
|
||||
return if !matching_string?(node)
|
||||
return if !matching_node?(node)
|
||||
|
||||
add_offense(node.parent, message: <<~TEXT.chomp)
|
||||
Setting.get('timezone_default_sanitized') is removed.
|
||||
Please use Setting.get('timezone_default') directly.
|
||||
TEXT
|
||||
end
|
||||
|
||||
def matching_string?(node)
|
||||
node.source == "'timezone_default_sanitized'"
|
||||
end
|
||||
|
||||
def matching_node?(node)
|
||||
parent = node.parent
|
||||
|
||||
return false if !parent
|
||||
|
||||
parent.receiver&.const_name == 'Setting' && parent.method_name == :get
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'capybara/rspec/matchers'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# Policy specs using ".not_to permit_actions" will lead to presumably false results,
|
||||
# as that matches when only one of the listed actions is forbidden. Instead,
|
||||
# use the simpler ".to forbid_actions" which ensures all actions are forbidden.
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# expect(page).not_to permit(:action)
|
||||
#
|
||||
# # good
|
||||
# expect(page).to forbid(:action)
|
||||
class ToForbidOverNotToPermit < Base
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Prefer `.to forbid_action[s]` over `.not_to permit_action[s]`.'.freeze
|
||||
|
||||
def_node_matcher :on_not_to_permit, '(send (send nil? {:expect :is_expected} {_ ...}) :not_to (send nil? {:permit_action :permit_actions} ...))'
|
||||
|
||||
def on_send(node)
|
||||
on_not_to_permit(node) do
|
||||
add_offense(node, message: MSG) do |corrector|
|
||||
corrector.replace(original_matcher_node(node).loc.selector, alternative_matcher(node))
|
||||
corrector.replace(node.loc.selector, 'to')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def original_matcher_node(node)
|
||||
node.children[2]
|
||||
end
|
||||
|
||||
def original_matcher(node)
|
||||
original_matcher_node(node).method_name
|
||||
end
|
||||
|
||||
def alternative_matcher(node)
|
||||
original_matcher(node).to_s.sub 'permit_action', 'forbid_action'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# Rails model callbacks that trigger GraphQL subscriptions must do that only in commit callbacks,
|
||||
# as the handlers live in other processes and do not have access to the current transaction.
|
||||
#
|
||||
# @example
|
||||
# # bad
|
||||
# after_create :trigger_subscriptions
|
||||
# after_update :trigger_subscriptions
|
||||
# after_save :trigger_subscriptions
|
||||
# after_destroy :trigger_subscriptions
|
||||
#
|
||||
# # good
|
||||
# after_create_commit :trigger_subscriptions
|
||||
# after_update_commit :trigger_subscriptions
|
||||
# after_save_commit :trigger_subscriptions
|
||||
# after_destroy_commit :trigger_subscriptions
|
||||
|
||||
class TriggerFromCommitHooks < Base
|
||||
extend AutoCorrector
|
||||
|
||||
def_node_matcher :non_commit_callback?, <<-PATTERN
|
||||
$(send _ {:after_create :after_update :after_save :after_destroy} (sym _))
|
||||
PATTERN
|
||||
|
||||
MSG = 'Trigger GraphQL subscriptions only from commit hooks to ensure the data is available in other processes.'.freeze
|
||||
|
||||
def on_send(node)
|
||||
return if non_commit_callback?(node).nil?
|
||||
return if !trigger_callback?(node)
|
||||
|
||||
add_offense(node) do |corrector|
|
||||
corrector.replace(node.loc.selector, correct_callback(node.children[1]))
|
||||
end
|
||||
end
|
||||
|
||||
def trigger_callback?(node)
|
||||
node.children[2].value.start_with? 'trigger'
|
||||
end
|
||||
|
||||
def correct_callback(callback)
|
||||
"#{callback}_commit"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Zammad
|
||||
# This cop updates the copyright information or inserts it if needed.
|
||||
class UpdateCopyright < Base
|
||||
include RangeHelp
|
||||
extend AutoCorrector
|
||||
|
||||
MSG = 'Copyright update required (use auto-correct to rectify this).'.freeze
|
||||
COPYRIGHT = "# Copyright (C) 2012-#{Date.today.year} Zammad Foundation, https://zammad-foundation.org/".freeze # rubocop:disable Rails/Date
|
||||
|
||||
def on_new_investigation
|
||||
if processed_source.raw_source.include? '# Copyright (C) 2012-'
|
||||
update_copyright
|
||||
else
|
||||
insert_copyright
|
||||
end
|
||||
end
|
||||
|
||||
def insert_copyright
|
||||
if processed_source.raw_source.start_with? '#!'
|
||||
# Keep shebang line, obviously.
|
||||
comment = processed_source.comments.first
|
||||
add_offense(comment) do |corrector|
|
||||
corrector.insert_after(
|
||||
comment,
|
||||
"\n#{COPYRIGHT}\n"
|
||||
)
|
||||
end
|
||||
else
|
||||
# Insert at the top if there is no shebang.
|
||||
file_start = range_between(0, 0)
|
||||
add_offense(file_start) do |corrector|
|
||||
corrector.insert_before(file_start, "#{COPYRIGHT}\n\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_copyright
|
||||
processed_source.comments.each do |comment|
|
||||
break if correct_copyright?(comment)
|
||||
next if !comment.text.include?('# Copyright (C) 2012-') # rubocop:disable Rails/NegateInclude
|
||||
|
||||
add_offense(comment) do |corrector|
|
||||
corrector.replace(
|
||||
comment,
|
||||
replace_with(comment)
|
||||
)
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
def correct_copyright?(comment)
|
||||
return false if !comment.text.eql? COPYRIGHT
|
||||
|
||||
newline_after_copyright?(comment)
|
||||
end
|
||||
|
||||
def newline_after_copyright?(comment)
|
||||
processed_source[comment.location.last_line].blank?
|
||||
end
|
||||
|
||||
def replace_with(comment)
|
||||
return COPYRIGHT if newline_after_copyright?(comment)
|
||||
|
||||
"#{COPYRIGHT}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,592 +0,0 @@
|
||||
# Default enabled cops
|
||||
# https://github.com/rubocop-hq/rubocop/blob/master/config/default.yml
|
||||
|
||||
require:
|
||||
- rubocop-inflector
|
||||
- ../../config/initializers/inflections.rb
|
||||
- ./rubocop_zammad.rb
|
||||
|
||||
plugins:
|
||||
- rubocop-capybara
|
||||
- rubocop-factory_bot
|
||||
- rubocop-faker
|
||||
- rubocop-graphql
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rake
|
||||
- rubocop-rspec
|
||||
- rubocop-rspec_rails
|
||||
|
||||
inherit_from:
|
||||
- todo.yml
|
||||
- todo.rspec.yml
|
||||
|
||||
inherit_mode:
|
||||
merge:
|
||||
- Include
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
DisplayCopNames: true
|
||||
Include:
|
||||
- ".gitlab/**/*.rb"
|
||||
- ".github/**/*.rb"
|
||||
- ".dev/rubocop/**/*.rb"
|
||||
Exclude:
|
||||
- "bin/*"
|
||||
- "db/schema.rb"
|
||||
- "vendor/**/*"
|
||||
- "node_modules/**/*"
|
||||
- "public/assets/**/*"
|
||||
- "public/packs/**/*"
|
||||
|
||||
RSpec:
|
||||
Include:
|
||||
- "**/*_spec.rb"
|
||||
- "**/*_examples.rb"
|
||||
Language:
|
||||
Expectations:
|
||||
- expect_no_offenses
|
||||
- expect_offense
|
||||
- adds_foreign_key
|
||||
- sent
|
||||
- not_sent
|
||||
|
||||
Rails:
|
||||
Enabled: true
|
||||
|
||||
# Zammad StyleGuide
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
Description: "Limit lines to 80 characters."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#80-character-limits"
|
||||
Enabled: false
|
||||
|
||||
Style/NegatedIf:
|
||||
Description: >-
|
||||
Favor unless over if for negative conditions
|
||||
(or control flow or).
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#unless-for-negatives"
|
||||
Enabled: false
|
||||
|
||||
Style/IfUnlessModifier:
|
||||
Description: >-
|
||||
Favor modifier if/unless usage when you have a
|
||||
single-line body.
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier"
|
||||
Enabled: false
|
||||
|
||||
Style/TrailingCommaInArrayLiteral:
|
||||
Description: "Checks for trailing comma in array literals."
|
||||
StyleGuide: "#no-trailing-array-commas"
|
||||
Enabled: false
|
||||
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Description: "Checks for trailing comma in hash literals."
|
||||
Enabled: false
|
||||
|
||||
Style/TrailingCommaInArguments:
|
||||
Description: "Checks for trailing comma in argument lists."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas"
|
||||
Enabled: false
|
||||
|
||||
Style/RescueStandardError:
|
||||
Description: "Avoid rescuing without specifying an error class."
|
||||
Enabled: true
|
||||
EnforcedStyle: implicit
|
||||
|
||||
Style/FormatStringToken:
|
||||
Description: Use a consistent style for named format string tokens.
|
||||
EnforcedStyle: template
|
||||
# Prefer %{...} over %<...>s to make it easier for translators.
|
||||
|
||||
Layout/SpaceInsideReferenceBrackets:
|
||||
Description: "Checks the spacing inside referential brackets."
|
||||
Enabled: false
|
||||
|
||||
Layout/SpaceInsideArrayLiteralBrackets:
|
||||
Description: "Checks the spacing inside array literal brackets."
|
||||
Enabled: false
|
||||
|
||||
Style/DefWithParentheses:
|
||||
Description: "Use def with parentheses when there are arguments."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#method-parens"
|
||||
Enabled: false
|
||||
|
||||
Style/HashSyntax:
|
||||
EnforcedShorthandSyntax: either
|
||||
|
||||
Layout/HashAlignment:
|
||||
Description: >-
|
||||
Align the elements of a hash literal if they span more than
|
||||
one line.
|
||||
Enabled: true
|
||||
EnforcedHashRocketStyle: table
|
||||
EnforcedColonStyle: table
|
||||
EnforcedLastArgumentHashStyle: always_inspect
|
||||
|
||||
Layout/EmptyLinesAroundClassBody:
|
||||
Description: "Keeps track of empty lines around class bodies."
|
||||
Enabled: false
|
||||
|
||||
Layout/EmptyLinesAroundMethodBody:
|
||||
Description: "Keeps track of empty lines around method bodies."
|
||||
Enabled: false
|
||||
|
||||
Layout/EmptyLinesAroundBlockBody:
|
||||
Description: "Keeps track of empty lines around block bodies."
|
||||
Enabled: false
|
||||
|
||||
Layout/EmptyLinesAroundModuleBody:
|
||||
Description: "Keeps track of empty lines around module bodies."
|
||||
Enabled: false
|
||||
|
||||
Layout/ExtraSpacing:
|
||||
Description: "Do not use unnecessary spacing."
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "config/routes/**/*"
|
||||
- "db/migrate/20120101000001_create_base.rb"
|
||||
- "db/migrate/20120101000010_create_ticket.rb"
|
||||
|
||||
Style/MultilineBlockChain:
|
||||
Description: "Avoid multi-line chains of blocks."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#single-line-blocks"
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Description: "Avoid classes longer than 100 lines of code."
|
||||
Enabled: false
|
||||
|
||||
Metrics/MethodLength:
|
||||
Description: "Avoid methods longer than 10 lines of code."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#short-methods"
|
||||
Enabled: false
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Description: "Checks for methods with too many parameters."
|
||||
CountKeywordArgs: false
|
||||
|
||||
Style/BlockComments:
|
||||
Description: "Do not use block comments."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-block-comments"
|
||||
Enabled: false
|
||||
|
||||
Style/PerlBackrefs:
|
||||
Description: "Avoid Perl-style regex back references."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers"
|
||||
Enabled: false
|
||||
|
||||
Rails/ApplicationRecord:
|
||||
Exclude:
|
||||
- "app/models/application_model.rb"
|
||||
- "app/models/active_job_lock.rb"
|
||||
|
||||
Rails/BulkChangeTable:
|
||||
Description: "Check whether alter queries are combinable."
|
||||
Enabled: false
|
||||
|
||||
Rails/FindEach:
|
||||
Description: "Prefer all.find_each over all.find."
|
||||
Enabled: false
|
||||
|
||||
Rails/HasAndBelongsToMany:
|
||||
Description: "Prefer has_many :through to has_and_belongs_to_many."
|
||||
# StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through'
|
||||
Enabled: false
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Description: "Enforces use of I18n and locale files instead of locale specific strings."
|
||||
Enabled: false
|
||||
|
||||
Rails/MatchRoute:
|
||||
Description: >-
|
||||
Don't use `match` to define any routes unless there is a need to map multiple request types
|
||||
among [:get, :post, :patch, :put, :delete] to a single action using the `:via` option.
|
||||
StyleGuide: "https://rails.rubystyle.guide/#no-match-routes"
|
||||
Enabled: false
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Description: >-
|
||||
Use methods that skips model validations with caution.
|
||||
See reference for more information.
|
||||
Reference: "http://guides.rubyonrails.org/active_record_validations.html#skipping-validations"
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- test/**/*
|
||||
- "**/*_spec.rb"
|
||||
- "**/*_examples.rb"
|
||||
|
||||
Rails/ReversibleMigration:
|
||||
Description: This cop checks whether the change method of the migration file is reversible.
|
||||
Enabled: false
|
||||
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Description: "This cop looks for direct access to environment variables through the ENV variable within the application code."
|
||||
Enabled: true
|
||||
AllowReads: true
|
||||
Exclude:
|
||||
- "spec/lib/**/*"
|
||||
|
||||
Rails/Output:
|
||||
Exclude:
|
||||
- "lib/tasks/**/*"
|
||||
- "spec/**/*"
|
||||
Include:
|
||||
- "**/*_spec.rb"
|
||||
|
||||
Rails/Delegate:
|
||||
Description: Looks for delegations that could have been created automatically with the delegate method.
|
||||
Enabled: false
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Description: "Checks style of children classes and modules."
|
||||
Enabled: true
|
||||
EnforcedStyle: compact
|
||||
# Avoids nesting of classes and modules in sequencer code and improves readability by decreasing whitespace.
|
||||
# This cop is excluded for the rest of the codebase, due to following problems:
|
||||
# - undefined constants in path components which are not autoloaded
|
||||
# - changes to the lexical scope which require reference to the complete namespace of external classes
|
||||
# - difficult backporting due to extensive whitespace changes on all lines
|
||||
Include:
|
||||
- "lib/sequencer/**/*.rb"
|
||||
|
||||
Naming/FileName:
|
||||
Description: "Use snake_case for source file names."
|
||||
StyleGuide: "https://github.com/bbatsov/ruby-style-guide#snake-case-files"
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "script/websocket-server.rb"
|
||||
|
||||
Naming/VariableNumber:
|
||||
Description: "Use the configured style when numbering variables."
|
||||
Enabled: false
|
||||
|
||||
Naming/MethodParameterName:
|
||||
Description: >-
|
||||
Checks for method parameter names that contain capital letters,
|
||||
end in numbers, or do not meet a minimal length.
|
||||
Enabled: true
|
||||
AllowedNames: e, id, _, ip, to
|
||||
|
||||
Lint/BooleanSymbol:
|
||||
Description: "Check for `:true` and `:false` symbols."
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "db/seeds/object_manager_attributes.rb"
|
||||
- "spec/requests/integration/object_manager_attributes_spec.rb"
|
||||
|
||||
Lint/InterpolationCheck:
|
||||
Description: "Raise warning for interpolation in single q strs"
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "spec/models/trigger/base_spec.rb"
|
||||
|
||||
Lint/UselessConstantScoping:
|
||||
Description: Checks for useless constant scoping.
|
||||
Enabled: false
|
||||
|
||||
Style/StringConcatenation:
|
||||
Description: "Checks for places where string concatenation can be replaced with string interpolation."
|
||||
StyleGuide: "#string-interpolation"
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "config/routes/**/*"
|
||||
|
||||
Style/RegexpLiteral:
|
||||
Description: "This cop enforces using // or %r around regular expressions."
|
||||
EnforcedStyle: percent_r
|
||||
|
||||
Style/RedundantBegin:
|
||||
Description: "This cop checks for redundant `begin` blocks."
|
||||
Enabled: false
|
||||
|
||||
Style/FetchEnvVar:
|
||||
Description: "This cop suggests `ENV.fetch` for the replacement of `ENV[]`."
|
||||
Enabled: false
|
||||
|
||||
Style/SafeNavigationChainLength:
|
||||
Description: "This cops suggests to not use&.optional&.chaining&.too&.much"
|
||||
Enabled: false
|
||||
|
||||
GraphQL/OrderedArguments:
|
||||
Enabled: false
|
||||
|
||||
GraphQL/OrderedFields:
|
||||
Enabled: false
|
||||
|
||||
# Our models can have many fields, and we should not enforce a different GraphQL structure just for the sake of it.
|
||||
GraphQL/ExtractType:
|
||||
Enabled: false
|
||||
|
||||
GraphQL/ExtractInputType:
|
||||
MaxArguments: 3
|
||||
# Run cop only for mutations: https://github.com/DmitryTsepelev/rubocop-graphql/issues/47
|
||||
Include:
|
||||
- "app/graphql/gql/mutations/**/*.rb"
|
||||
|
||||
# Enforcing field descriptions for all model fields will be too verbose/redundant.
|
||||
GraphQL/FieldDescription:
|
||||
Exclude:
|
||||
- "app/graphql/gql/types/**/*"
|
||||
|
||||
GraphQL/ObjectDescription:
|
||||
Exclude:
|
||||
- "app/graphql/gql/fields/**/*" # false positive?
|
||||
- "app/graphql/gql/types/**/base_*"
|
||||
- "app/graphql/gql/types/**/extensions/**/*" # union extension classes are no GraphQL objects
|
||||
|
||||
# RSpec tests
|
||||
Style/NumericPredicate:
|
||||
Description: >-
|
||||
Checks for the use of predicate- or comparison methods for
|
||||
numeric comparisons.
|
||||
StyleGuide: "#predicate-methods"
|
||||
# This will change to a new method call which isn't guaranteed to be on the
|
||||
# object. Switching these methods has to be done with knowledge of the types
|
||||
# of the variables which rubocop doesn't have.
|
||||
AutoCorrect: false
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "**/*_spec.rb"
|
||||
|
||||
Lint/AmbiguousBlockAssociation:
|
||||
Description: >-
|
||||
Checks for ambiguous block association with method when param
|
||||
passed without parentheses.
|
||||
StyleGuide: "#syntax"
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "spec/support/*.rb"
|
||||
- "**/*_spec.rb"
|
||||
- "**/*_examples.rb"
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Description: >-
|
||||
Checks the indentation of the method name part in method calls
|
||||
that span more than one line.
|
||||
EnforcedStyle: indented
|
||||
Include:
|
||||
- "**/*_spec.rb"
|
||||
|
||||
Lint/UnusedMethodArgument:
|
||||
AllowUnusedKeywordArguments: true
|
||||
|
||||
Naming/PredicatePrefix:
|
||||
Exclude:
|
||||
- "app/graphql/gql/types/**/*"
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Description: "Checks for methods returning true/false that do not end with a questionmark."
|
||||
Enabled: false
|
||||
|
||||
RSpec/DescribeMethod:
|
||||
# We want to be able to split tests into subfolders, where several cases are grouped together.
|
||||
Description: "Checks that the second argument to the top level describe is the tested method name."
|
||||
Enabled: false
|
||||
|
||||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
RSpecRails/AvoidSetupHook:
|
||||
Exclude:
|
||||
- "test/**/*"
|
||||
|
||||
RSpec/AlignLeftLetBrace:
|
||||
Description: "Checks that left braces for adjacent single line lets are aligned."
|
||||
Enabled: true
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Description: "Prefer instance doubles over stubbing any instance of a class."
|
||||
Enabled: false
|
||||
|
||||
RSpec/SubjectStub:
|
||||
Enabled: false
|
||||
|
||||
RSpecRails/InferredSpecType:
|
||||
Description: "Identifies redundant spec type."
|
||||
Enabled: false # We use types to add DSL to rspec.
|
||||
|
||||
RSpec/IndexedLet:
|
||||
Description: "Do not set up test data using indexes (e.g., `item_1`, `item_2`)."
|
||||
Enabled: false
|
||||
|
||||
Capybara/SpecificFinders:
|
||||
Description: "Checks if there is a more specific finder offered by Capybara."
|
||||
Enabled: false
|
||||
# False positives, autocorrection makes weird suggestions.
|
||||
|
||||
Capybara/SpecificActions:
|
||||
Description: "Checks for there is a more specific actions offered by Capybara."
|
||||
Enabled: false
|
||||
|
||||
Capybara/RSpec/NegationMatcher:
|
||||
Description: Enforces use of have_no_* or not_to for negated expectations.
|
||||
EnforcedStyle: have_no
|
||||
|
||||
RSpec/ExampleLength:
|
||||
inherit_mode:
|
||||
merge:
|
||||
- Exclude
|
||||
CountAsOne:
|
||||
- "array"
|
||||
- "hash"
|
||||
- "heredoc"
|
||||
Max: 25
|
||||
Exclude:
|
||||
- "spec/system/**/*.rb"
|
||||
|
||||
FactoryBot/ExcessiveCreateList:
|
||||
Enabled: false
|
||||
|
||||
Zammad/PreferNegatedIfOverUnless:
|
||||
Exclude:
|
||||
- "bin/rspec"
|
||||
|
||||
Zammad/ExistsDateTimePrecision:
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
- "**/db/addon/**/*.rb"
|
||||
|
||||
Zammad/ExistsResetColumnInformation:
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
- "**/db/addon/**/*.rb"
|
||||
Exclude:
|
||||
- "db/migrate/201*_*.rb"
|
||||
- "db/migrate/2020*_*.rb"
|
||||
|
||||
Zammad/MigrationSchedulerLastRun:
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
- "**/db/addon/**/*.rb"
|
||||
Exclude:
|
||||
- "db/migrate/201*_*.rb"
|
||||
- "db/migrate/2020*_*.rb"
|
||||
|
||||
Zammad/DetectTranslatableString:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/**/*.rb"
|
||||
- "db/**/*.rb"
|
||||
- "lib/**/*.rb"
|
||||
Exclude:
|
||||
- "db/migrate/**/*.rb"
|
||||
- "db/addon/**/*.rb"
|
||||
- "lib/generators/**/*.rb"
|
||||
- "lib/sequencer/**/*.rb"
|
||||
- "lib/import/**/*.rb"
|
||||
- "lib/tasks/**/*.rb"
|
||||
- "lib/fill_db.rb"
|
||||
|
||||
Zammad/ForbidTranslatableMarker:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
|
||||
Zammad/ExistsDbStrategy:
|
||||
Include:
|
||||
- "spec/**/*.rb"
|
||||
Exclude:
|
||||
- "spec/support/capybara/*.rb"
|
||||
|
||||
Zammad/EnsuresTimeZoneAtSpecTopLevel:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "spec/system/**/*.rb"
|
||||
|
||||
Zammad/GraphqlAuthorizeAllowPublicAccess:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/graphql/gql/**/*.rb"
|
||||
|
||||
Zammad/GraphqlAuthorizeDisallowed:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/graphql/gql/**/*.rb"
|
||||
|
||||
Zammad/GraphqlAuthorizeRequiresPermission:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/graphql/gql/**/*.rb"
|
||||
|
||||
Zammad/GraphqlAuthorizedRequiresEnabledSetting:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/graphql/gql/**/*.rb"
|
||||
|
||||
Zammad/GraphqlCsrfSkipVerification:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/graphql/gql/**/*.rb"
|
||||
|
||||
Zammad/GraphqlForbidSensitiveFields:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "app/graphql/gql/types/**/*.rb"
|
||||
- "app/graphql/gql/mutations/**/*.rb"
|
||||
- "app/graphql/gql/queries/**/*.rb"
|
||||
- "app/graphql/gql/subscriptions/**/*.rb"
|
||||
Exclude:
|
||||
- "app/graphql/gql/types/input/**/*.rb"
|
||||
|
||||
Zammad/MigrationEnsureIntegerPrimaryKey:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
|
||||
Zammad/MigrationEnsureIntegerForeignKey:
|
||||
Enabled: true
|
||||
Include:
|
||||
- "db/migrate/*.rb"
|
||||
|
||||
Capybara/RSpec/NegationMatcherAfterVisit:
|
||||
Description: "Allow to use negated matcher after visit"
|
||||
Enabled: false
|
||||
|
||||
RSpec/IncludeExamples:
|
||||
Description: "Allow to use both it_behaves_like and include_examples"
|
||||
Enabled: false
|
||||
|
||||
Rails/FindByOrAssignmentMemoization:
|
||||
Description: "Allow to use ||= for memoization of find_by returns"
|
||||
Enabled: false
|
||||
|
||||
# Looks like this cop is currently broken.
|
||||
# Allowing shorthands in one-line blocks would be fine.
|
||||
# But as of now it mis-detects chained blocks as multiliners.
|
||||
Style/ItBlockParameter:
|
||||
Description: "Allow to use it shorthand in blocks"
|
||||
EnforcedStyle: only_numbered_parameters
|
||||
|
||||
Style/OneClassPerFile:
|
||||
Description: "Allow to have multiple classes in one file"
|
||||
Enabled: false
|
||||
|
||||
# There is a warning in the cop about possible performance issues,
|
||||
# see https://docs.rubocop.org/rubocop-rails/cops_rails.html#safety-railspluckinwhere.
|
||||
Rails/PluckInWhere:
|
||||
Description: "Identifies places where pluck is used in where query methods and can be replaced with select."
|
||||
Enabled: false
|
||||
|
||||
Rails/Exit:
|
||||
Exclude:
|
||||
- "lib/tasks/**/*"
|
||||
|
||||
RSpec/Output:
|
||||
Enabled: false
|
||||
|
||||
Style/StderrPuts:
|
||||
Exclude:
|
||||
- "lib/tasks/**/*"
|
||||
|
||||
Lint/MissingSuper:
|
||||
Description: "Checks for missing super calls in overridden methods. Skips Service classes."
|
||||
Exclude:
|
||||
- "app/services/**/*"
|
||||
|
||||
Rails/StrongParametersExpect:
|
||||
Enabled: false
|
||||
@@ -1,37 +0,0 @@
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require_relative 'cop/zammad/active_record_reorder'
|
||||
require_relative 'cop/zammad/correct_migration_timestamp'
|
||||
require_relative 'cop/zammad/detect_translatable_string'
|
||||
require_relative 'cop/zammad/enforce_in_modal'
|
||||
require_relative 'cop/zammad/exists_condition'
|
||||
require_relative 'cop/zammad/exists_date_time_precision'
|
||||
require_relative 'cop/zammad/exists_db_strategy'
|
||||
require_relative 'cop/zammad/exists_reset_column_information'
|
||||
require_relative 'cop/zammad/faker_unique'
|
||||
require_relative 'cop/zammad/forbid_def_send'
|
||||
require_relative 'cop/zammad/forbid_default_scope'
|
||||
require_relative 'cop/zammad/forbid_loofah_document'
|
||||
require_relative 'cop/zammad/forbid_loofah_fragment'
|
||||
require_relative 'cop/zammad/forbid_rand'
|
||||
require_relative 'cop/zammad/forbid_redis_client'
|
||||
require_relative 'cop/zammad/forbid_translatable_marker'
|
||||
require_relative 'cop/zammad/forbid_unprocessable_entity'
|
||||
require_relative 'cop/zammad/graphql_authorize_allow_public_access'
|
||||
require_relative 'cop/zammad/graphql_authorize_requires_permission'
|
||||
require_relative 'cop/zammad/graphql_authorize_disallowed'
|
||||
require_relative 'cop/zammad/graphql_authorized_requires_enabled_setting'
|
||||
require_relative 'cop/zammad/graphql_csrf_skip_verification'
|
||||
require_relative 'cop/zammad/graphql_forbid_sensitive_fields'
|
||||
require_relative 'cop/zammad/migration_scheduler_last_run'
|
||||
require_relative 'cop/zammad/no_to_sym_on_string'
|
||||
require_relative 'cop/zammad/prefer_models_all'
|
||||
require_relative 'cop/zammad/prefer_negated_if_over_unless'
|
||||
require_relative 'cop/zammad/timezone_default'
|
||||
require_relative 'cop/zammad/to_forbid_over_not_to_permit'
|
||||
require_relative 'cop/zammad/trigger_from_commit_hooks'
|
||||
require_relative 'cop/zammad/update_copyright'
|
||||
require_relative 'cop/zammad/ensures_time_zone_at_spec_top_level'
|
||||
require_relative 'cop/zammad/migration_ensure_integer_primary_key'
|
||||
require_relative 'cop/zammad/migration_ensure_integer_foreign_key'
|
||||
require_relative 'cop/zammad/forbid_calling_service_directly'
|
||||
@@ -1,476 +0,0 @@
|
||||
RSpec/ContextWording:
|
||||
Exclude:
|
||||
- 'spec/db/migrate/active_job_lock_cleanup_job_scheduler_spec.rb'
|
||||
- 'spec/db/migrate/check_for_object_attributes_spec.rb'
|
||||
- 'spec/db/migrate/forget_insecure_sessions_spec.rb'
|
||||
- 'spec/db/migrate/issue_1219_zhtw_locale_typo_spec.rb'
|
||||
- 'spec/db/migrate/issue_1905_exchange_login_from_remote_id_spec.rb'
|
||||
- 'spec/db/migrate/issue_1977_remove_invalid_user_foreign_keys_spec.rb'
|
||||
- 'spec/db/migrate/issue_2333_object_country_already_exists_spec.rb'
|
||||
- 'spec/db/migrate/issue_2345_es_attachment_max_size_in_mb_setting_lower_default_spec.rb'
|
||||
- 'spec/db/migrate/issue_2368_add_indices_to_histories_and_tickets_spec.rb'
|
||||
- 'spec/db/migrate/issue_2541_fix_notification_email_without_body_spec.rb'
|
||||
- 'spec/jobs/concerns/has_active_job_lock_spec.rb'
|
||||
- 'spec/jobs/concerns/has_collection_update_spec.rb'
|
||||
- 'spec/jobs/imap_authentication_migration_cleanup_job_spec.rb'
|
||||
- 'spec/jobs/ticket_article_communicate_email_job_spec.rb'
|
||||
- 'spec/lib/application_handle_info_spec.rb'
|
||||
- 'spec/lib/core_ext/string_spec.rb'
|
||||
- 'spec/lib/external_credential/google_spec.rb'
|
||||
- 'spec/lib/external_credential/microsoft365_spec.rb'
|
||||
- 'spec/lib/import/exchange/folder_spec.rb'
|
||||
- 'spec/lib/import/helper_spec.rb'
|
||||
- 'spec/lib/import/ldap_spec.rb'
|
||||
- 'spec/lib/import/otrs/article_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_user_spec.rb'
|
||||
- 'spec/lib/import/otrs/priority_spec.rb'
|
||||
- 'spec/lib/import/otrs/queue_spec.rb'
|
||||
- 'spec/lib/import/otrs/requester_spec.rb'
|
||||
- 'spec/lib/import/otrs/state_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/state_spec.rb'
|
||||
- 'spec/lib/import/otrs/ticket_spec.rb'
|
||||
- 'spec/lib/import/otrs/user_spec.rb'
|
||||
- 'spec/lib/ldap/group_spec.rb'
|
||||
- 'spec/lib/ldap/user_spec.rb'
|
||||
- 'spec/lib/ldap_spec.rb'
|
||||
- 'spec/lib/notification_factory/mailer_spec.rb'
|
||||
- 'spec/lib/notification_factory/template_spec.rb'
|
||||
- 'spec/lib/notification_factory_spec.rb'
|
||||
- 'spec/lib/password_hash_spec.rb'
|
||||
- 'spec/lib/search_index_backend_spec.rb'
|
||||
- 'spec/lib/secure_mailing/smime_spec.rb'
|
||||
- 'spec/lib/sequencer/sequence/import/ldap/users_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/associations/assign_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/save_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/object_attribute/sanitized_name_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/ldap/user/attributes/role_ids/unassigned_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/sub_sequence/base_examples.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/ticket/comment/source_based_spec.rb'
|
||||
- 'spec/lib/sessions/backend/base_spec.rb'
|
||||
- 'spec/lib/sessions/backend/ticket_overview_list_spec.rb'
|
||||
- 'spec/lib/signature_detection_spec.rb'
|
||||
- 'spec/lib/stats/ticket_waiting_time_spec.rb'
|
||||
- 'spec/models/application_model/can_assets_examples.rb'
|
||||
- 'spec/models/application_model/checks_import_examples.rb'
|
||||
- 'spec/models/calendar_spec.rb'
|
||||
- 'spec/models/channel/email_parser_spec.rb'
|
||||
- 'spec/models/channel/filter/follow_up_merged_spec.rb'
|
||||
- 'spec/models/channel/filter/follow_up_possible_check_spec.rb'
|
||||
- 'spec/models/channel/filter/out_of_office_check_spec.rb'
|
||||
- 'spec/models/concerns/can_csv_import_examples.rb'
|
||||
- 'spec/models/concerns/checks_kb_client_notification_examples.rb'
|
||||
- 'spec/models/concerns/has_collection_update_examples.rb'
|
||||
- 'spec/models/concerns/has_group_relation_definition_examples.rb'
|
||||
- 'spec/models/concerns/has_groups_examples.rb'
|
||||
- 'spec/models/concerns/has_groups_permissions_examples.rb'
|
||||
- 'spec/models/concerns/has_history_examples.rb'
|
||||
- 'spec/models/concerns/has_roles_examples.rb'
|
||||
- 'spec/models/concerns/has_search_index_backend_examples.rb'
|
||||
- 'spec/models/cti/caller_id_spec.rb'
|
||||
- 'spec/models/cti/driver/base_spec.rb'
|
||||
- 'spec/models/cti/log_spec.rb'
|
||||
- 'spec/models/email_address_spec.rb'
|
||||
- 'spec/models/history_spec.rb'
|
||||
- 'spec/models/job_spec.rb'
|
||||
- 'spec/models/knowledge_base/answer/translation/search_with_attachment_spec.rb'
|
||||
- 'spec/models/knowledge_base/category_spec.rb'
|
||||
- 'spec/models/knowledge_base/menu_item_spec.rb'
|
||||
- 'spec/models/knowledge_base_spec.rb'
|
||||
- 'spec/models/object_lookup_spec.rb'
|
||||
- 'spec/models/object_manager/attribute_spec.rb'
|
||||
- 'spec/models/overview_spec.rb'
|
||||
- 'spec/models/role_spec.rb'
|
||||
- 'spec/models/smime_certificate_spec.rb'
|
||||
- 'spec/models/store/file_spec.rb'
|
||||
- 'spec/models/store/provider/file_spec.rb'
|
||||
- 'spec/models/store_spec.rb'
|
||||
- 'spec/models/tag/item_spec.rb'
|
||||
- 'spec/models/tag_spec.rb'
|
||||
- 'spec/models/taskbar_spec.rb'
|
||||
- 'spec/models/ticket/article_spec.rb'
|
||||
- 'spec/models/ticket/number/date_spec.rb'
|
||||
- 'spec/models/ticket/number/increment_spec.rb'
|
||||
- 'spec/models/ticket/state_spec.rb'
|
||||
- 'spec/models/ticket_spec.rb'
|
||||
- 'spec/models/token_spec.rb'
|
||||
- 'spec/models/translation_spec.rb'
|
||||
- 'spec/models/trigger/sms_spec.rb'
|
||||
- 'spec/models/trigger_spec.rb'
|
||||
- 'spec/models/type_lookup_spec.rb'
|
||||
- 'spec/models/user_device_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/policies/ticket_policy_spec.rb'
|
||||
- 'spec/policies/user_policy_spec.rb'
|
||||
- 'spec/requests/cors_preflight_check_spec.rb'
|
||||
- 'spec/requests/error_spec.rb'
|
||||
- 'spec/requests/external_credentials_spec.rb'
|
||||
- 'spec/requests/integration/check_mk_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
- 'spec/requests/integration/smime_spec.rb'
|
||||
- 'spec/requests/knowledge_base/attachments_spec.rb'
|
||||
- 'spec/requests/knowledge_base/search_with_details_spec.rb'
|
||||
- 'spec/requests/session_spec.rb'
|
||||
- 'spec/requests/tag_spec.rb'
|
||||
- 'spec/requests/ticket/article_spec.rb'
|
||||
- 'spec/requests/ticket_spec.rb'
|
||||
- 'spec/requests/time_accounting_spec.rb'
|
||||
- 'spec/requests/upload_cache_spec.rb'
|
||||
- 'spec/requests/user/permission_spec.rb'
|
||||
- 'spec/requests/user_spec.rb'
|
||||
- 'spec/system/admin/knowledge_base/public_menu_spec.rb'
|
||||
- 'spec/system/admin/knowledge_base/theme_spec.rb'
|
||||
- 'spec/system/basic/assets_spec.rb'
|
||||
- 'spec/system/basic/richtext_spec.rb'
|
||||
- 'spec/system/channels/email_spec.rb'
|
||||
- 'spec/system/cti_spec.rb'
|
||||
- 'spec/system/dashboard_spec.rb'
|
||||
- 'spec/system/keyboard_shortcuts_spec.rb'
|
||||
- 'spec/system/knowledge_base/locale/answer/edit_spec.rb'
|
||||
- 'spec/system/knowledge_base/locale/answer/read_spec.rb'
|
||||
- 'spec/system/knowledge_base/locale/answer/read_spec.rb'
|
||||
- 'spec/system/knowledge_base/locale/answer/reader_spec.rb'
|
||||
- 'spec/system/knowledge_base_public/answer_spec.rb'
|
||||
- 'spec/system/knowledge_base_public/editor_spec.rb'
|
||||
- 'spec/system/knowledge_base_public/guest_spec.rb'
|
||||
- 'spec/system/knowledge_base_public/menu_items_spec.rb'
|
||||
- 'spec/system/manage/calendars_spec.rb'
|
||||
- 'spec/system/manage/groups_spec.rb'
|
||||
- 'spec/system/manage/jobs_spec.rb'
|
||||
- 'spec/system/manage/macros_spec.rb'
|
||||
- 'spec/system/manage/organizations_spec.rb'
|
||||
- 'spec/system/manage/overviews_spec.rb'
|
||||
- 'spec/system/manage/report_profiles_spec.rb'
|
||||
- 'spec/system/manage/roles_spec.rb'
|
||||
- 'spec/system/manage/text_modules_spec.rb'
|
||||
- 'spec/system/manage/trigger_spec.rb'
|
||||
- 'spec/system/manage/users_spec.rb'
|
||||
- 'spec/system/organization_profile_spec.rb'
|
||||
- 'spec/system/overview_spec.rb'
|
||||
- 'spec/system/profile_spec.rb'
|
||||
- 'spec/system/report_spec.rb'
|
||||
- 'spec/system/search_spec.rb'
|
||||
- 'spec/system/settings/proxy_spec.rb'
|
||||
- 'spec/system/settings/security_spec.rb'
|
||||
- 'spec/system/system/integration/clearbit_spec.rb'
|
||||
- 'spec/system/system/integration/icinga_spec.rb'
|
||||
- 'spec/system/system/integration/sipgate_spec.rb'
|
||||
- 'spec/system/system/integration/smime_spec.rb'
|
||||
- 'spec/system/system/object_manager_spec.rb'
|
||||
- 'spec/system/ticket/create_spec.rb'
|
||||
- 'spec/system/ticket/shared_draft_start_spec.rb'
|
||||
- 'spec/system/ticket/shared_draft_zoom_spec.rb'
|
||||
- 'spec/system/ticket/update/full_quote_header_spec.rb'
|
||||
- 'spec/system/ticket/view_spec.rb'
|
||||
- 'spec/system/ticket/zoom/article_actions_spec.rb'
|
||||
- 'spec/system/ticket/zoom/article_creation_spec.rb'
|
||||
- 'spec/system/ticket/zoom/object_attributes_spec.rb'
|
||||
- 'spec/system/ticket/zoom/taskbar_spec.rb'
|
||||
- 'spec/system/ticket/zoom_spec.rb'
|
||||
- 'spec/system/user/profile_spec.rb'
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Exclude:
|
||||
- 'spec/integration/otrs_spec.rb'
|
||||
- 'spec/lib/sequencer/sequence/import/ldap/users_spec.rb'
|
||||
- 'spec/models/taskbar_spec.rb'
|
||||
- 'spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb'
|
||||
- 'spec/models/ticket/escalation_examples.rb'
|
||||
- 'spec/models/translation_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/requests/api_auth_from_spec.rb'
|
||||
- 'spec/requests/api_auth_spec.rb'
|
||||
- 'spec/requests/calendar_spec.rb'
|
||||
- 'spec/requests/form_spec.rb'
|
||||
- 'spec/requests/integration/check_mk_spec.rb'
|
||||
- 'spec/requests/integration/cti_spec.rb'
|
||||
- 'spec/requests/integration/idoit_spec.rb'
|
||||
- 'spec/requests/integration/message_bird_sms_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
- 'spec/requests/integration/placetel_spec.rb'
|
||||
- 'spec/requests/integration/sipgate_spec.rb'
|
||||
- 'spec/requests/integration/telegram_spec.rb'
|
||||
- 'spec/requests/integration/twilio_sms_spec.rb'
|
||||
- 'spec/requests/integration/user_device_spec.rb'
|
||||
- 'spec/requests/long_polling_spec.rb'
|
||||
- 'spec/requests/organization_spec.rb'
|
||||
- 'spec/requests/report_spec.rb'
|
||||
- 'spec/requests/search_spec.rb'
|
||||
- 'spec/requests/settings_spec.rb'
|
||||
- 'spec/requests/taskbar_spec.rb'
|
||||
- 'spec/requests/text_module_spec.rb'
|
||||
- 'spec/requests/ticket/article_spec.rb'
|
||||
- 'spec/requests/ticket_spec.rb'
|
||||
- 'spec/requests/user/organization_spec.rb'
|
||||
- 'spec/requests/user_spec.rb'
|
||||
|
||||
RSpec/ExpectInHook:
|
||||
Exclude:
|
||||
- 'spec/models/calendar_spec.rb'
|
||||
- 'spec/models/concerns/has_search_index_backend_examples.rb'
|
||||
- 'spec/models/ticket/article_spec.rb'
|
||||
|
||||
RSpec/SpecFilePathFormat:
|
||||
Exclude:
|
||||
- 'spec/db/migrate/20171023000001_fixed_store_upgrade_ror_45_spec.rb'
|
||||
- 'spec/db/migrate/drop_api_suffix_from_third_party_login_gitlab_site_parameter_spec.rb'
|
||||
- 'spec/db/migrate/issue_*_spec.rb'
|
||||
- 'spec/integration/github_spec.rb'
|
||||
- 'spec/integration/gitlab_spec.rb'
|
||||
- 'spec/lib/core_ext/active_record/calculations/pluck_as_hash_spec.rb'
|
||||
- 'spec/jobs/issue_*_spec.rb'
|
||||
- 'spec/lib/import/base_factory_spec.rb'
|
||||
- 'spec/models/knowledge_base/answer/translation/search_with_attachment_spec.rb'
|
||||
- 'spec/models/trigger/base_spec.rb'
|
||||
- 'spec/models/trigger/sms_spec.rb'
|
||||
|
||||
# This was split up into RSpec/SpecFilePathSuffix and RSpec/SpecFilePathFormat,
|
||||
# and should now be disabled.
|
||||
Rails/FilePath:
|
||||
Enabled: false
|
||||
|
||||
RSpec/InstanceVariable:
|
||||
Exclude:
|
||||
- 'spec/lib/notification_factory/renderer_spec.rb'
|
||||
|
||||
RSpec/LetSetup:
|
||||
Exclude:
|
||||
- 'spec/jobs/ticket_online_notification_seen_job_spec.rb'
|
||||
- 'spec/lib/external_credential/google_spec.rb'
|
||||
- 'spec/lib/external_credential/microsoft365_spec.rb'
|
||||
- 'spec/lib/secure_mailing/smime_spec.rb'
|
||||
- 'spec/lib/sessions/backend/ticket_overview_list_spec.rb'
|
||||
- 'spec/models/channel/email_parser_spec.rb'
|
||||
- 'spec/models/cti/caller_id_spec.rb'
|
||||
- 'spec/models/cti/driver/base_spec.rb'
|
||||
- 'spec/models/cti/log_spec.rb'
|
||||
- 'spec/models/organization_spec.rb'
|
||||
- 'spec/models/role_group_spec.rb'
|
||||
- 'spec/models/tag_spec.rb'
|
||||
- 'spec/models/ticket/article_spec.rb'
|
||||
- 'spec/models/trigger_spec.rb'
|
||||
- 'spec/models/user_group_spec.rb'
|
||||
- 'spec/requests/external_credentials_spec.rb'
|
||||
- 'spec/requests/integration/cti_spec.rb'
|
||||
- 'spec/requests/integration/idoit_spec.rb'
|
||||
- 'spec/requests/integration/placetel_spec.rb'
|
||||
- 'spec/requests/integration/sipgate_spec.rb'
|
||||
- 'spec/requests/search_spec.rb'
|
||||
- 'spec/requests/ticket/escalation_spec.rb'
|
||||
- 'spec/requests/time_accounting_spec.rb'
|
||||
- 'spec/requests/user/organization_spec.rb'
|
||||
- 'spec/requests/user_spec.rb'
|
||||
|
||||
RSpec/MessageSpies:
|
||||
Exclude:
|
||||
- 'spec/db/migrate/check_for_object_attributes_spec.rb'
|
||||
- 'spec/db/migrate/issue_1660_fix_tree_select_configurations_spec.rb'
|
||||
- 'spec/db/migrate/issue_1905_exchange_login_from_remote_id_spec.rb'
|
||||
- 'spec/db/migrate/object_manager_attribute_date_remove_future_past_spec.rb'
|
||||
- 'spec/jobs/search_index_job_spec.rb'
|
||||
- 'spec/lib/import/base_factory_examples.rb'
|
||||
- 'spec/lib/import/helper_spec.rb'
|
||||
- 'spec/lib/import/otrs/article/attachment_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/article_customer_spec.rb'
|
||||
- 'spec/lib/import/otrs/article_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_user_spec.rb'
|
||||
- 'spec/lib/import/otrs/dynamic_field_examples.rb'
|
||||
- 'spec/lib/import/otrs/dynamic_field_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/dynamic_field_spec.rb'
|
||||
- 'spec/lib/import/otrs/history_examples.rb'
|
||||
- 'spec/lib/import/otrs/priority_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/priority_spec.rb'
|
||||
- 'spec/lib/import/otrs/queue_spec.rb'
|
||||
- 'spec/lib/import/otrs/requester_spec.rb'
|
||||
- 'spec/lib/import/otrs/state_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/state_spec.rb'
|
||||
- 'spec/lib/import/otrs/ticket_spec.rb'
|
||||
- 'spec/lib/import/otrs/user_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/user_spec.rb'
|
||||
- 'spec/lib/ldap/group_spec.rb'
|
||||
- 'spec/lib/ldap/guid_spec.rb'
|
||||
- 'spec/lib/ldap/user_spec.rb'
|
||||
- 'spec/lib/ldap_spec.rb'
|
||||
- 'spec/lib/sequencer/sequence/import/ldap/users_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/common/attribute_mapper_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/mapping/flat_keys_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/attributes/remote_id_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/reset_primary_key_sequence_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/save_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/object_attribute/sanitized_name_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/sub_sequence/base_examples.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/ticket/comment/attachment/request_spec.rb'
|
||||
- 'spec/models/channel/email_parser_spec.rb'
|
||||
- 'spec/models/concerns/has_groups_examples.rb'
|
||||
- 'spec/models/concerns/has_history_examples.rb'
|
||||
- 'spec/models/concerns/has_roles_examples.rb'
|
||||
- 'spec/models/concerns/has_tags_examples.rb'
|
||||
- 'spec/models/cti/caller_id_spec.rb'
|
||||
- 'spec/models/cti/log_spec.rb'
|
||||
- 'spec/models/import_job_spec.rb'
|
||||
- 'spec/models/ticket/number_spec.rb'
|
||||
- 'spec/models/ticket_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Exclude:
|
||||
- 'spec/db/migrate/20171023000001_fixed_store_upgrade_ror_45_spec.rb'
|
||||
- 'spec/db/migrate/check_for_object_attributes_spec.rb'
|
||||
- 'spec/db/migrate/issue_1660_fix_tree_select_configurations_spec.rb'
|
||||
- 'spec/db/migrate/issue_1905_exchange_login_from_remote_id_spec.rb'
|
||||
- 'spec/db/migrate/object_manager_attribute_date_remove_future_past_spec.rb'
|
||||
- 'spec/db/migrate/rename_locale_on_users_spec.rb'
|
||||
- 'spec/integration/ldap_spec.rb'
|
||||
- 'spec/integration/otrs_spec.rb'
|
||||
- 'spec/jobs/concerns/has_active_job_lock_spec.rb'
|
||||
- 'spec/jobs/search_index_job_spec.rb'
|
||||
- 'spec/jobs/ticket_user_ticket_counter_job_spec.rb'
|
||||
- 'spec/lib/core_ext/string_spec.rb'
|
||||
- 'spec/lib/email_address_validation_spec.rb'
|
||||
- 'spec/lib/external_sync_spec.rb'
|
||||
- 'spec/lib/import/base_factory_examples.rb'
|
||||
- 'spec/lib/import/helper_spec.rb'
|
||||
- 'spec/lib/import/import_job_backend_examples.rb'
|
||||
- 'spec/lib/import/ldap_spec.rb'
|
||||
- 'spec/lib/import/otrs/article_customer_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_spec.rb'
|
||||
- 'spec/lib/import/otrs/dynamic_field_spec.rb'
|
||||
- 'spec/lib/import/otrs/history_examples.rb'
|
||||
- 'spec/lib/import/otrs/priority_factory_spec.rb'
|
||||
- 'spec/lib/import/otrs/requester_spec.rb'
|
||||
- 'spec/lib/import/otrs/state_factory_spec.rb'
|
||||
- 'spec/lib/ldap/group_spec.rb'
|
||||
- 'spec/lib/ldap/guid_spec.rb'
|
||||
- 'spec/lib/ldap/user_spec.rb'
|
||||
- 'spec/lib/ldap_spec.rb'
|
||||
- 'spec/lib/notification_factory/mailer_spec.rb'
|
||||
- 'spec/lib/password_hash_spec.rb'
|
||||
- 'spec/lib/secure_mailing/smime_spec.rb'
|
||||
- 'spec/lib/sequencer/sequence/import/ldap/users_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/common/attribute_mapper_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/mapping/flat_keys_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/associations/assign_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/model/attributes/remote_id_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/common/object_attribute/sanitized_name_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/ldap/user/attributes/role_ids/unassigned_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/ldap/sources/lost/deactivate_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/sub_sequence/base_examples.rb'
|
||||
- 'spec/lib/sessions/event/chat_session_start_spec.rb'
|
||||
- 'spec/lib/sessions/event/chat_transfer_spec.rb'
|
||||
- 'spec/lib/upload_cache_spec.rb'
|
||||
- 'spec/lib/user_info_spec.rb'
|
||||
- 'spec/models/calendar_spec.rb'
|
||||
- 'spec/models/channel/driver/sms/twilio_spec.rb'
|
||||
- 'spec/models/channel/email_parser_spec.rb'
|
||||
- 'spec/models/concerns/has_groups_examples.rb'
|
||||
- 'spec/models/concerns/has_history_examples.rb'
|
||||
- 'spec/models/cti/caller_id_spec.rb'
|
||||
- 'spec/models/cti/log_spec.rb'
|
||||
- 'spec/models/history_spec.rb'
|
||||
- 'spec/models/import_job_spec.rb'
|
||||
- 'spec/models/job_spec.rb'
|
||||
- 'spec/models/object_lookup_spec.rb'
|
||||
- 'spec/models/object_manager/attribute_spec.rb'
|
||||
- 'spec/models/overview_spec.rb'
|
||||
- 'spec/models/session_spec.rb'
|
||||
- 'spec/models/sla/has_escalation_calculation_impact_examples.rb'
|
||||
- 'spec/models/smime_certificate_spec.rb'
|
||||
- 'spec/models/store/file_spec.rb'
|
||||
- 'spec/models/store/provider/file_spec.rb'
|
||||
- 'spec/models/store_spec.rb'
|
||||
- 'spec/models/taskbar_spec.rb'
|
||||
- 'spec/models/ticket/article_spec.rb'
|
||||
- 'spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb'
|
||||
- 'spec/models/ticket/escalation_examples.rb'
|
||||
- 'spec/models/ticket/number/increment_spec.rb'
|
||||
- 'spec/models/ticket/number_spec.rb'
|
||||
- 'spec/models/ticket/overviews_spec.rb'
|
||||
- 'spec/models/ticket_spec.rb'
|
||||
- 'spec/models/translation_spec.rb'
|
||||
- 'spec/models/trigger/base_spec.rb'
|
||||
- 'spec/models/trigger_spec.rb'
|
||||
- 'spec/models/type_lookup_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/requests/api_auth_from_spec.rb'
|
||||
- 'spec/requests/api_auth_spec.rb'
|
||||
- 'spec/requests/calendar_spec.rb'
|
||||
- 'spec/requests/error_spec.rb'
|
||||
- 'spec/requests/external_credentials_spec.rb'
|
||||
- 'spec/requests/form_spec.rb'
|
||||
- 'spec/requests/integration/check_mk_spec.rb'
|
||||
- 'spec/requests/integration/cti_spec.rb'
|
||||
- 'spec/requests/integration/idoit_spec.rb'
|
||||
- 'spec/requests/integration/github_spec.rb'
|
||||
- 'spec/requests/integration/gitlab_spec.rb'
|
||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||
- 'spec/requests/integration/placetel_spec.rb'
|
||||
- 'spec/requests/integration/sipgate_spec.rb'
|
||||
- 'spec/requests/integration/smime_spec.rb'
|
||||
- 'spec/requests/integration/telegram_spec.rb'
|
||||
- 'spec/requests/integration/twilio_sms_spec.rb'
|
||||
- 'spec/requests/integration/message_bird_sms_spec.rb'
|
||||
- 'spec/requests/integration/user_device_spec.rb'
|
||||
- 'spec/requests/links_spec.rb'
|
||||
- 'spec/requests/long_polling_spec.rb'
|
||||
- 'spec/requests/o_auth_spec.rb'
|
||||
- 'spec/requests/organization_spec.rb'
|
||||
- 'spec/requests/overview_spec.rb'
|
||||
- 'spec/requests/package_spec.rb'
|
||||
- 'spec/requests/report_spec.rb'
|
||||
- 'spec/requests/search_spec.rb'
|
||||
- 'spec/requests/session_spec.rb'
|
||||
- 'spec/requests/settings_spec.rb'
|
||||
- 'spec/requests/sla_spec.rb'
|
||||
- 'spec/requests/tag_spec.rb'
|
||||
- 'spec/requests/taskbar_spec.rb'
|
||||
- 'spec/requests/text_module_spec.rb'
|
||||
- 'spec/requests/ticket/article_attachments_spec.rb'
|
||||
- 'spec/requests/ticket/article_spec.rb'
|
||||
- 'spec/requests/ticket/escalation_spec.rb'
|
||||
- 'spec/requests/ticket_spec.rb'
|
||||
- 'spec/requests/time_accounting_spec.rb'
|
||||
- 'spec/requests/user/organization_spec.rb'
|
||||
- 'spec/requests/user/permission_spec.rb'
|
||||
- 'spec/requests/user_spec.rb'
|
||||
- 'spec/system/**/*'
|
||||
|
||||
RSpec/NestedGroups:
|
||||
Exclude:
|
||||
- 'spec/lib/secure_mailing/smime_spec.rb'
|
||||
- 'spec/models/channel/email_parser_spec.rb'
|
||||
- 'spec/models/job_spec.rb'
|
||||
- 'spec/models/store_spec.rb'
|
||||
- 'spec/models/token_spec.rb'
|
||||
- 'spec/models/trigger_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/system/ticket/create_spec.rb'
|
||||
|
||||
RSpec/VerifiedDoubles:
|
||||
Exclude:
|
||||
- 'spec/lib/external_sync_spec.rb'
|
||||
- 'spec/lib/ldap_spec.rb'
|
||||
- 'spec/lib/sequencer/sequence/import/ldap/users_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/sub_sequence/base_examples.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/ticket/comment/attachment/request_spec.rb'
|
||||
- 'spec/lib/sequencer/unit/import/zendesk/ticket/comment/source_based_spec.rb'
|
||||
- 'spec/models/ticket/number_spec.rb'
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Description: Checks if example groups contain too many `let` and `subject` calls.
|
||||
Enabled: false
|
||||
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/MultipleMemoizedHelpers
|
||||
|
||||
RSpec/NoExpectationExample:
|
||||
Description: Checks if an example contains any expectation.
|
||||
Exclude:
|
||||
- spec/system/**/*
|
||||
- spec/lib/import/**/*
|
||||
- spec/lib/ldap_spec.rb
|
||||
- spec/db/migrate/issue_1573_multi_orga_spec.rb
|
||||
- spec/lib/monitoring_helper/health_checker/delayed_job_spec.rb
|
||||
- spec/requests/user/permission_spec.rb
|
||||
|
||||
RSpec/MetadataStyle:
|
||||
Description: Use consistent metadata style.
|
||||
Enabled: false
|
||||
@@ -1,523 +0,0 @@
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Lint/MissingCopEnableDirective:
|
||||
Exclude:
|
||||
- 'lib/import/factory.rb'
|
||||
- 'lib/import/helper.rb'
|
||||
- 'lib/import/otrs/async.rb'
|
||||
- 'lib/import/otrs/history.rb'
|
||||
- 'lib/import/otrs/priority_factory.rb'
|
||||
- 'lib/import/otrs/state_factory.rb'
|
||||
- 'lib/import/otrs/user_factory.rb'
|
||||
- 'lib/import/transaction_factory.rb'
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 34
|
||||
Exclude:
|
||||
- 'app/controllers/application_controller/authenticates.rb'
|
||||
- 'app/controllers/application_controller/handles_devices.rb'
|
||||
- 'app/controllers/application_controller/handles_errors.rb'
|
||||
- 'app/controllers/application_controller/logs_http_access.rb'
|
||||
- 'app/controllers/channels_email_controller.rb'
|
||||
- 'app/controllers/concerns/creates_ticket_articles.rb'
|
||||
- 'app/controllers/first_steps_controller.rb'
|
||||
- 'app/controllers/form_controller.rb'
|
||||
- 'app/controllers/getting_started_controller.rb'
|
||||
- 'app/controllers/import_otrs_controller.rb'
|
||||
- 'app/controllers/import_zendesk_controller.rb'
|
||||
- 'app/controllers/integration/check_mk_controller.rb'
|
||||
- 'app/controllers/knowledge_base/search_controller.rb'
|
||||
- 'app/controllers/object_manager_attributes_controller.rb'
|
||||
- 'app/controllers/organizations_controller.rb'
|
||||
- 'app/controllers/reports_controller.rb'
|
||||
- 'app/controllers/sessions_controller.rb'
|
||||
- 'app/controllers/ticket_articles_controller.rb'
|
||||
- 'app/controllers/ticket_overviews_controller.rb'
|
||||
- 'app/controllers/tickets_controller.rb'
|
||||
- 'app/controllers/time_accountings_controller.rb'
|
||||
- 'app/controllers/user_access_token_controller.rb'
|
||||
- 'app/controllers/users_controller.rb'
|
||||
- 'app/jobs/communicate_facebook_job.rb'
|
||||
- 'app/jobs/communicate_sms_job.rb'
|
||||
- 'app/jobs/communicate_telegram_job.rb'
|
||||
- 'app/jobs/ticket_article_communicate_email_job.rb'
|
||||
- 'app/models/activity_stream/assets.rb'
|
||||
- 'app/models/application_model/can_associations.rb'
|
||||
- 'app/models/application_model/can_cleanup_param.rb'
|
||||
- 'app/models/application_model/checks_attribute_values_and_length.rb'
|
||||
- 'app/models/authorization.rb'
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/calendar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
- 'app/models/channel/filter/bounce_delivery_permanent_failed.rb'
|
||||
- 'app/models/channel/filter/bounce_follow_up_check.rb'
|
||||
- 'app/models/channel/filter/database.rb'
|
||||
- 'app/models/channel/filter/follow_up_check.rb'
|
||||
- 'app/models/channel/filter/identify_sender.rb'
|
||||
- 'app/models/channel/filter/monitoring_base.rb'
|
||||
- 'app/models/channel/filter/reply_to_based_sender.rb'
|
||||
- 'app/models/channel/filter/sender_is_system_address.rb'
|
||||
- 'app/models/chat/session/assets.rb'
|
||||
- 'app/models/concerns/can_clone_attachments.rb'
|
||||
- 'app/models/concerns/can_csv_import.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/concerns/has_rich_text.rb'
|
||||
- 'app/models/cti/caller_id.rb'
|
||||
- 'app/models/cti/driver/base.rb'
|
||||
- 'app/models/cti/driver/placetel.rb'
|
||||
- 'app/models/cti/driver/sipgate_io.rb'
|
||||
- 'app/models/cti/log.rb'
|
||||
- 'app/models/history.rb'
|
||||
- 'app/models/link.rb'
|
||||
- 'app/models/object_manager/attribute.rb'
|
||||
- 'app/models/online_notification/assets.rb'
|
||||
- 'app/models/organization/assets.rb'
|
||||
- 'app/models/organization/search.rb'
|
||||
- 'app/models/overview/assets.rb'
|
||||
- 'app/models/package.rb'
|
||||
- 'app/models/package/migration.rb'
|
||||
- 'app/models/report.rb'
|
||||
- 'app/models/setting.rb'
|
||||
- 'app/models/tag/item.rb'
|
||||
- 'app/models/ticket.rb'
|
||||
- 'app/models/ticket/article.rb'
|
||||
- 'app/models/ticket/article/assets.rb'
|
||||
- 'app/models/ticket/article/has_ticket_contact_attributes_impact.rb'
|
||||
- 'app/models/ticket/overviews.rb'
|
||||
- 'app/models/ticket/screen_options.rb'
|
||||
- 'app/models/ticket/search.rb'
|
||||
- 'app/models/ticket/search_index.rb'
|
||||
- 'app/models/ticket/sets_last_owner_update_time.rb'
|
||||
- 'app/models/transaction/notification.rb'
|
||||
- 'app/models/transaction/signature_detection.rb'
|
||||
- 'app/models/translation.rb'
|
||||
- 'app/models/trigger/assets.rb'
|
||||
- 'app/models/user.rb'
|
||||
- 'app/models/user/assets.rb'
|
||||
- 'app/models/user/search.rb'
|
||||
- 'app/models/user_device.rb'
|
||||
- 'db/migrate/20120101000001_create_base.rb'
|
||||
- 'db/migrate/20120101000010_create_ticket.rb'
|
||||
- 'db/migrate/20161117000001_job_unable_to_create_issue_432.rb'
|
||||
- 'db/migrate/20170403000001_fixed_admin_user_permission_920.rb'
|
||||
- 'db/migrate/20171023000001_fixed_store_upgrade_ror_45.rb'
|
||||
- 'db/migrate/20180806000001_fixed_twitter_ticket_article_preferences7.rb'
|
||||
- 'db/migrate/20181017000001_cti_generic_api2.rb'
|
||||
- 'db/migrate/20190531180304_initialize_knowledge_base.rb'
|
||||
- 'lib/auto_wizard.rb'
|
||||
- 'lib/calendar_subscriptions.rb'
|
||||
- 'lib/calendar_subscriptions/tickets.rb'
|
||||
- 'lib/core_ext/string.rb'
|
||||
- 'lib/email_helper/probe.rb'
|
||||
- 'lib/email_helper/verify.rb'
|
||||
- 'lib/excel_sheet.rb'
|
||||
- 'lib/excel_sheet/ticket.rb'
|
||||
- 'lib/external_credential/exchange.rb'
|
||||
- 'lib/external_credential/facebook.rb'
|
||||
- 'lib/external_credential/google.rb'
|
||||
- 'lib/external_credential/microsoft_base.rb'
|
||||
- 'lib/facebook.rb'
|
||||
- 'lib/fill_db.rb'
|
||||
- 'lib/import/otrs.rb'
|
||||
- 'lib/models.rb'
|
||||
- 'lib/notification_factory/mailer.rb'
|
||||
- 'lib/notification_factory/renderer.rb'
|
||||
- 'lib/report/article_by_type_sender.rb'
|
||||
- 'lib/report/base.rb'
|
||||
- 'lib/report/ticket_first_solution.rb'
|
||||
- 'lib/report/ticket_generic_time.rb'
|
||||
- 'lib/report/ticket_moved.rb'
|
||||
- 'lib/search_index_backend.rb'
|
||||
- 'lib/secure_mailing/smime/incoming.rb'
|
||||
- 'lib/selector/search_index.rb'
|
||||
- 'lib/selector/sql.rb'
|
||||
- 'lib/sequencer/state.rb'
|
||||
- 'lib/sequencer/unit/exchange/connection.rb'
|
||||
- 'lib/session_helper/collection_base.rb'
|
||||
- 'lib/session_helper/collection_ticket.rb'
|
||||
- 'lib/sessions.rb'
|
||||
- 'lib/sessions/backend/ticket_overview_list.rb'
|
||||
- 'lib/sessions/client.rb'
|
||||
- 'lib/sessions/event/chat_session_start.rb'
|
||||
- 'lib/signature_detection.rb'
|
||||
- 'lib/stats.rb'
|
||||
- 'lib/stats/ticket_channel_distribution.rb'
|
||||
- 'lib/stats/ticket_in_process.rb'
|
||||
- 'lib/stats/ticket_waiting_time.rb'
|
||||
- 'lib/telegram_helper.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'lib/user_agent.rb'
|
||||
- 'lib/websocket_server.rb'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- 'Gemfile'
|
||||
- 'app/models/ticket/can_selector.rb'
|
||||
- 'app/controllers/application_controller/authenticates.rb'
|
||||
- 'app/controllers/reports_controller.rb'
|
||||
- 'app/controllers/time_accountings_controller.rb'
|
||||
- 'app/models/application_model/can_assets.rb'
|
||||
- 'app/models/application_model/can_associations.rb'
|
||||
- 'app/models/application_model/can_cleanup_param.rb'
|
||||
- 'app/models/application_model/can_creates_and_updates.rb'
|
||||
- 'app/models/application_model/can_lookup.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/bounce_delivery_permanent_failed.rb'
|
||||
- 'app/models/channel/filter/database.rb'
|
||||
- 'app/models/channel/filter/identify_sender.rb'
|
||||
- 'app/models/chat/session/search.rb'
|
||||
- 'app/models/concerns/can_search.rb'
|
||||
- 'app/models/concerns/can_be_published.rb'
|
||||
- 'app/models/concerns/can_clone_attachments.rb'
|
||||
- 'app/models/concerns/can_csv_import.rb'
|
||||
- 'app/models/concerns/has_audit_logs.rb'
|
||||
- 'app/models/concerns/has_groups.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/concerns/has_rich_text.rb'
|
||||
- 'app/models/job.rb'
|
||||
- 'app/models/object_manager/attribute.rb'
|
||||
- 'app/models/organization/search.rb'
|
||||
- 'app/models/package.rb'
|
||||
- 'app/models/package/migration.rb'
|
||||
- 'app/models/ticket.rb'
|
||||
- 'app/models/ticket/overviews.rb'
|
||||
- 'app/models/ticket/search.rb'
|
||||
- 'lib/selector/search_index.rb'
|
||||
- 'app/models/transaction/notification.rb'
|
||||
- 'app/models/translation.rb'
|
||||
- 'app/models/user/search.rb'
|
||||
- 'config/environments/development.rb'
|
||||
- 'config/initializers/omniauth.rb'
|
||||
- 'config/routes/knowledge_base.rb'
|
||||
- 'config/routes/user.rb'
|
||||
- 'config/routes/test.rb'
|
||||
- 'config/routes/ticket.rb'
|
||||
- 'db/migrate/20120101000001_create_base.rb'
|
||||
- 'db/migrate/20120101000010_create_ticket.rb'
|
||||
- 'db/migrate/20180806000001_fixed_twitter_ticket_article_preferences7.rb'
|
||||
- 'lib/can_be_published/state_machine.rb'
|
||||
- 'lib/core_ext/string.rb'
|
||||
- 'lib/email_helper.rb'
|
||||
- 'lib/facebook.rb'
|
||||
- 'lib/fill_db.rb'
|
||||
- 'lib/notification_factory/renderer.rb'
|
||||
- 'lib/report/ticket_first_solution.rb'
|
||||
- 'lib/report/ticket_generic_time.rb'
|
||||
- 'lib/report/ticket_moved.rb'
|
||||
- 'lib/report/ticket_reopened.rb'
|
||||
- 'lib/search_index_backend.rb'
|
||||
- 'lib/sequencer/unit/exchange/connection.rb'
|
||||
- 'lib/sessions.rb'
|
||||
- 'lib/sessions/backend/ticket_overview_list.rb'
|
||||
- 'lib/sessions/client.rb'
|
||||
- 'lib/tasks/cache/measure_io_performance.rake'
|
||||
- 'lib/tasks/zammad/search_index_es.rake'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'script/websocket-server.rb'
|
||||
- 'spec/**/*'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- 'app/controllers/import_otrs_controller.rb'
|
||||
- 'app/models/chat.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/object_manager/attribute.rb'
|
||||
- 'lib/notification_factory/mailer.rb'
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 12
|
||||
Exclude:
|
||||
- 'app/controllers/application_controller/authenticates.rb'
|
||||
- 'app/controllers/application_controller/handles_devices.rb'
|
||||
- 'app/controllers/application_controller/handles_errors.rb'
|
||||
- 'app/controllers/channels_email_controller.rb'
|
||||
- 'app/controllers/concerns/creates_ticket_articles.rb'
|
||||
- 'app/controllers/form_controller.rb'
|
||||
- 'app/controllers/getting_started_controller.rb'
|
||||
- 'app/controllers/import_otrs_controller.rb'
|
||||
- 'app/controllers/integration/check_mk_controller.rb'
|
||||
- 'app/controllers/object_manager_attributes_controller.rb'
|
||||
- 'app/controllers/reports_controller.rb'
|
||||
- 'app/controllers/tickets_controller.rb'
|
||||
- 'app/controllers/time_accountings_controller.rb'
|
||||
- 'app/controllers/users_controller.rb'
|
||||
- 'app/jobs/communicate_sms_job.rb'
|
||||
- 'app/jobs/communicate_telegram_job.rb'
|
||||
- 'app/jobs/ticket_article_communicate_email_job.rb'
|
||||
- 'app/models/application_model/can_associations.rb'
|
||||
- 'app/models/application_model/can_cleanup_param.rb'
|
||||
- 'app/models/application_model/checks_attribute_values_and_length.rb'
|
||||
- 'app/models/authorization.rb'
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/calendar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
- 'app/models/channel/filter/bounce_delivery_permanent_failed.rb'
|
||||
- 'app/models/channel/filter/follow_up_check.rb'
|
||||
- 'app/models/channel/filter/identify_sender.rb'
|
||||
- 'app/models/channel/filter/monitoring_base.rb'
|
||||
- 'app/models/channel/filter/sender_is_system_address.rb'
|
||||
- 'app/models/concerns/can_clone_attachments.rb'
|
||||
- 'app/models/concerns/can_csv_import.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/cti/caller_id.rb'
|
||||
- 'app/models/cti/driver/base.rb'
|
||||
- 'app/models/cti/driver/placetel.rb'
|
||||
- 'app/models/cti/driver/sipgate_io.rb'
|
||||
- 'app/models/cti/log.rb'
|
||||
- 'app/models/history.rb'
|
||||
- 'app/models/object_manager/attribute.rb'
|
||||
- 'app/models/organization/search.rb'
|
||||
- 'app/models/package/migration.rb'
|
||||
- 'app/models/ticket.rb'
|
||||
- 'app/models/ticket/article/has_ticket_contact_attributes_impact.rb'
|
||||
- 'app/models/ticket/screen_options.rb'
|
||||
- 'app/models/ticket/search.rb'
|
||||
- 'app/models/ticket/sets_last_owner_update_time.rb'
|
||||
- 'app/models/transaction/notification.rb'
|
||||
- 'app/models/user.rb'
|
||||
- 'app/models/user/assets.rb'
|
||||
- 'app/models/user/search.rb'
|
||||
- 'app/models/user_device.rb'
|
||||
- 'db/migrate/20180806000001_fixed_twitter_ticket_article_preferences7.rb'
|
||||
- 'lib/auto_wizard.rb'
|
||||
- 'lib/core_ext/string.rb'
|
||||
- 'lib/email_helper/probe.rb'
|
||||
- 'lib/excel_sheet.rb'
|
||||
- 'lib/external_credential/google.rb'
|
||||
- 'lib/external_credential/microsoft_base.rb'
|
||||
- 'lib/facebook.rb'
|
||||
- 'lib/fill_db.rb'
|
||||
- 'lib/models.rb'
|
||||
- 'lib/notification_factory/mailer.rb'
|
||||
- 'lib/notification_factory/renderer.rb'
|
||||
- 'lib/report/base.rb'
|
||||
- 'lib/report/ticket_first_solution.rb'
|
||||
- 'lib/report/ticket_generic_time.rb'
|
||||
- 'lib/report/ticket_moved.rb'
|
||||
- 'lib/search_index_backend.rb'
|
||||
- 'lib/secure_mailing/smime/incoming.rb'
|
||||
- 'lib/selector/search_index.rb'
|
||||
- 'lib/selector/sql.rb'
|
||||
- 'lib/sequencer/unit/exchange/connection.rb'
|
||||
- 'lib/sessions.rb'
|
||||
- 'lib/sessions/backend/ticket_overview_list.rb'
|
||||
- 'lib/sessions/client.rb'
|
||||
- 'lib/signature_detection.rb'
|
||||
- 'lib/stats.rb'
|
||||
- 'lib/stats/ticket_channel_distribution.rb'
|
||||
- 'lib/telegram_helper.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'lib/user_agent.rb'
|
||||
- 'test/**/*'
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- 'app/models/ticket/can_selector.rb'
|
||||
- 'app/controllers/application_controller/authenticates.rb'
|
||||
- 'app/controllers/application_controller/handles_errors.rb'
|
||||
- 'app/controllers/application_controller/renders_models.rb'
|
||||
- 'app/models/application_model/can_assets.rb'
|
||||
- 'app/models/application_model/can_associations.rb'
|
||||
- 'app/models/application_model/can_creates_and_updates.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/filter/identify_sender.rb'
|
||||
- 'app/models/concerns/can_search.rb'
|
||||
- 'app/models/concerns/can_be_published.rb'
|
||||
- 'app/models/concerns/can_csv_import.rb'
|
||||
- 'app/models/concerns/has_audit_logs.rb'
|
||||
- 'app/models/concerns/has_groups.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/concerns/has_rich_text.rb'
|
||||
- 'app/models/concerns/has_search_index_backend.rb'
|
||||
- 'app/models/ticket/escalation.rb'
|
||||
- 'app/models/ticket/overviews.rb'
|
||||
- 'app/models/ticket/screen_options.rb'
|
||||
- 'app/models/ticket/search.rb'
|
||||
- 'app/models/user/search.rb'
|
||||
- 'lib/auto_wizard.rb'
|
||||
- 'lib/fill_db.rb'
|
||||
- 'lib/import/otrs.rb'
|
||||
- 'lib/sessions.rb'
|
||||
- 'lib/sessions/node.rb'
|
||||
- 'lib/signature_detection.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'spec/support/capybara/common_actions.rb'
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 12
|
||||
Exclude:
|
||||
- 'app/controllers/application_controller/authenticates.rb'
|
||||
- 'app/controllers/application_controller/handles_devices.rb'
|
||||
- 'app/controllers/application_controller/handles_errors.rb'
|
||||
- 'app/controllers/channels_email_controller.rb'
|
||||
- 'app/controllers/concerns/creates_ticket_articles.rb'
|
||||
- 'app/controllers/form_controller.rb'
|
||||
- 'app/controllers/getting_started_controller.rb'
|
||||
- 'app/controllers/import_otrs_controller.rb'
|
||||
- 'app/controllers/integration/check_mk_controller.rb'
|
||||
- 'app/controllers/object_manager_attributes_controller.rb'
|
||||
- 'app/controllers/reports_controller.rb'
|
||||
- 'app/controllers/tickets_controller.rb'
|
||||
- 'app/controllers/time_accountings_controller.rb'
|
||||
- 'app/controllers/users_controller.rb'
|
||||
- 'app/jobs/communicate_sms_job.rb'
|
||||
- 'app/jobs/communicate_telegram_job.rb'
|
||||
- 'app/jobs/ticket_article_communicate_email_job.rb'
|
||||
- 'app/models/application_model/can_associations.rb'
|
||||
- 'app/models/application_model/can_cleanup_param.rb'
|
||||
- 'app/models/application_model/checks_attribute_values_and_length.rb'
|
||||
- 'app/models/authorization.rb'
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/calendar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/channel/filter/auto_response_check.rb'
|
||||
- 'app/models/channel/filter/bounce_delivery_permanent_failed.rb'
|
||||
- 'app/models/channel/filter/follow_up_check.rb'
|
||||
- 'app/models/channel/filter/identify_sender.rb'
|
||||
- 'app/models/channel/filter/monitoring_base.rb'
|
||||
- 'app/models/channel/filter/sender_is_system_address.rb'
|
||||
- 'app/models/concerns/can_clone_attachments.rb'
|
||||
- 'app/models/concerns/can_csv_import.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/cti/caller_id.rb'
|
||||
- 'app/models/cti/driver/base.rb'
|
||||
- 'app/models/cti/driver/placetel.rb'
|
||||
- 'app/models/cti/driver/sipgate_io.rb'
|
||||
- 'app/models/cti/log.rb'
|
||||
- 'app/models/history.rb'
|
||||
- 'app/models/object_manager/attribute.rb'
|
||||
- 'app/models/organization/search.rb'
|
||||
- 'app/models/package.rb'
|
||||
- 'app/models/package/migration.rb'
|
||||
- 'app/models/setting.rb'
|
||||
- 'app/models/ticket.rb'
|
||||
- 'app/models/ticket/article/has_ticket_contact_attributes_impact.rb'
|
||||
- 'app/models/ticket/screen_options.rb'
|
||||
- 'app/models/ticket/search.rb'
|
||||
- 'app/models/ticket/sets_last_owner_update_time.rb'
|
||||
- 'app/models/transaction/notification.rb'
|
||||
- 'app/models/user.rb'
|
||||
- 'app/models/user/assets.rb'
|
||||
- 'app/models/user/search.rb'
|
||||
- 'app/models/user_device.rb'
|
||||
- 'db/migrate/20180806000001_fixed_twitter_ticket_article_preferences7.rb'
|
||||
- 'db/migrate/20181017000001_cti_generic_api2.rb'
|
||||
- 'lib/auto_wizard.rb'
|
||||
- 'lib/core_ext/string.rb'
|
||||
- 'lib/email_helper/probe.rb'
|
||||
- 'lib/excel_sheet.rb'
|
||||
- 'lib/external_credential/google.rb'
|
||||
- 'lib/external_credential/microsoft_base.rb'
|
||||
- 'lib/facebook.rb'
|
||||
- 'lib/fill_db.rb'
|
||||
- 'lib/models.rb'
|
||||
- 'lib/notification_factory/mailer.rb'
|
||||
- 'lib/notification_factory/renderer.rb'
|
||||
- 'lib/report/base.rb'
|
||||
- 'lib/report/ticket_generic_time.rb'
|
||||
- 'lib/search_index_backend.rb'
|
||||
- 'lib/secure_mailing/smime/incoming.rb'
|
||||
- 'lib/selector/search_index.rb'
|
||||
- 'lib/selector/sql.rb'
|
||||
- 'lib/sequencer/unit/exchange/connection.rb'
|
||||
- 'lib/sessions.rb'
|
||||
- 'lib/sessions/backend/ticket_overview_list.rb'
|
||||
- 'lib/sessions/client.rb'
|
||||
- 'lib/signature_detection.rb'
|
||||
- 'lib/stats.rb'
|
||||
- 'lib/stats/ticket_channel_distribution.rb'
|
||||
- 'lib/telegram_helper.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'lib/user_agent.rb'
|
||||
|
||||
Rails/CreateTableWithTimestamps:
|
||||
Exclude:
|
||||
- 'db/migrate/20120101000001_create_base.rb'
|
||||
- 'db/migrate/20120101000010_create_ticket.rb'
|
||||
- 'db/migrate/20170419000002_overview_role_ids.rb'
|
||||
- 'db/migrate/20170608151442_enhanced_permissions.rb'
|
||||
|
||||
Rails/HasManyOrHasOneDependent:
|
||||
Exclude:
|
||||
- 'app/models/chat/session.rb'
|
||||
- 'app/models/concerns/has_groups.rb'
|
||||
- 'app/models/email_address.rb'
|
||||
- 'app/models/organization.rb'
|
||||
- 'app/models/signature.rb'
|
||||
- 'app/models/ticket/state_type.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/models/application_model/can_activity_stream_log.rb'
|
||||
- 'app/models/application_model/can_cleanup_param.rb'
|
||||
- 'app/models/avatar.rb'
|
||||
- 'app/models/channel.rb'
|
||||
- 'app/models/channel/driver/facebook.rb'
|
||||
- 'app/models/channel/driver/sendmail.rb'
|
||||
- 'app/models/channel/driver/sms/massenversand.rb'
|
||||
- 'app/models/channel/driver/sms/message_bird.rb'
|
||||
- 'app/models/channel/driver/sms/twilio.rb'
|
||||
- 'app/models/channel/driver/telegram.rb'
|
||||
- 'app/models/channel/driver/whatsapp.rb'
|
||||
- 'app/models/channel/email_build.rb'
|
||||
- 'app/models/channel/email_parser.rb'
|
||||
- 'app/models/chat/session.rb'
|
||||
- 'app/models/concerns/has_history.rb'
|
||||
- 'app/models/job.rb'
|
||||
- 'app/models/object_manager/attribute.rb'
|
||||
- 'app/models/package.rb'
|
||||
- 'app/models/setting.rb'
|
||||
- 'app/models/store/file.rb'
|
||||
- 'app/models/text_module.rb'
|
||||
- 'app/models/ticket/escalation.rb'
|
||||
- 'app/models/translation.rb'
|
||||
- 'app/models/user.rb'
|
||||
- 'lib/app_version.rb'
|
||||
- 'lib/core_ext/string.rb'
|
||||
- 'lib/external_credential/facebook.rb'
|
||||
- 'lib/external_credential/google.rb'
|
||||
- 'lib/external_credential/microsoft_base.rb'
|
||||
- 'lib/external_credential/exchange.rb'
|
||||
- 'lib/html_sanitizer.rb'
|
||||
- 'lib/models.rb'
|
||||
- 'lib/sessions/backend/ticket_overview_list.rb'
|
||||
- 'lib/sessions/node.rb'
|
||||
- 'spec/support/system_init_done.rb'
|
||||
|
||||
Style/ReturnNilInPredicateMethodDefinition:
|
||||
Exclude:
|
||||
- app/models/channel/filter/import_archive.rb
|
||||
- app/models/core_workflow/attributes.rb
|
||||
- app/models/core_workflow/condition.rb
|
||||
- app/models/ticket/article/adds_metadata_email.rb
|
||||
- app/models/ticket/article/adds_metadata_general.rb
|
||||
- app/models/ticket/article/adds_metadata_origin_by_id.rb
|
||||
- lib/selector/base.rb
|
||||
- app/models/trigger.rb
|
||||
- app/models/user/avatar.rb
|
||||
- app/policies/ticket/shared_draft_start_policy.rb
|
||||
- app/services/service/ticket/create.rb
|
||||
- lib/auth/backend/ldap.rb
|
||||
- lib/enrichment/clearbit/organization.rb
|
||||
- lib/enrichment/clearbit/user.rb
|
||||
- lib/html_sanitizer/scrubber/link.rb
|
||||
- lib/html_sanitizer/scrubber/remove_last_empty_node.rb
|
||||
- lib/html_sanitizer/scrubber/wipe.rb
|
||||
- lib/import/otrs/diff.rb
|
||||
- lib/search_index_backend.rb
|
||||
- lib/sequencer/unit/import/kayako/sub_sequence/generic.rb
|
||||
- lib/sessions/event/base.rb
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'yaml'
|
||||
|
||||
# This script validates Rubocop's todo*.yml files and complains if there
|
||||
# are any exclude entries that point to nonexisting files.
|
||||
|
||||
obsolete_excludes = []
|
||||
|
||||
puts 'Checking for obsolete rubocop excludes...'
|
||||
|
||||
Dir.glob("#{__dir__}/todo*.yml").each do |f|
|
||||
YAML.load_file(f).each_value do |section|
|
||||
next if !section.key? 'Exclude'
|
||||
|
||||
section['Exclude'].each do |file|
|
||||
next if file.include? '*'
|
||||
|
||||
obsolete_excludes.push(file) if !File.exist? "#{__dir__}/../../#{file}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if obsolete_excludes.any?
|
||||
puts 'Obsolete rubocop todo*.yml entries found for these files:'
|
||||
obsolete_excludes.sort.uniq.each do |file|
|
||||
puts " - #{file}"
|
||||
end
|
||||
exit false
|
||||
end
|
||||
|
||||
puts 'done.'
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
gitlab:
|
||||
enabled: true
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
name: "Update Zammad dependencies via updatecli"
|
||||
|
||||
sources:
|
||||
heroku-buildpack-nodejs:
|
||||
name: heroku-buildpack-nodejs
|
||||
kind: gittag
|
||||
spec:
|
||||
url: "https://github.com/heroku/heroku-buildpack-nodejs.git"
|
||||
versionfilter:
|
||||
kind: "semver"
|
||||
heroku-buildpack-ruby:
|
||||
name: heroku-buildpack-ruby
|
||||
kind: gitbranch
|
||||
spec:
|
||||
url: "https://github.com/pkgr/heroku-buildpack-ruby.git"
|
||||
versionfilter:
|
||||
kind: "regex"
|
||||
pattern: 'v\d{3}-\d+'
|
||||
|
||||
conditions: {}
|
||||
|
||||
targets:
|
||||
buildpacks-nodejs:
|
||||
name: buildpacks-nodejs
|
||||
kind: file
|
||||
sourceid: heroku-buildpack-nodejs
|
||||
# {{ if .gitlab.enabled }}
|
||||
scmid: zammad
|
||||
# {{ end }}
|
||||
spec:
|
||||
file: .buildpacks
|
||||
matchpattern: 'https://github.com/heroku/heroku-buildpack-nodejs.git#.*'
|
||||
replacepattern: 'https://github.com/heroku/heroku-buildpack-nodejs.git#{{ source `heroku-buildpack-nodejs` }}'
|
||||
buildpacks-ruby:
|
||||
name: buildpacks-ruby
|
||||
kind: file
|
||||
sourceid: heroku-buildpack-ruby
|
||||
# {{ if .gitlab.enabled }}
|
||||
scmid: zammad
|
||||
# {{ end }}
|
||||
spec:
|
||||
file: .buildpacks
|
||||
matchpattern: 'https://github.com/pkgr/heroku-buildpack-ruby.git#.*'
|
||||
replacepattern: 'https://github.com/pkgr/heroku-buildpack-ruby.git#{{ source `heroku-buildpack-ruby` }}'
|
||||
|
||||
# {{ if .gitlab.enabled }}
|
||||
scms:
|
||||
zammad:
|
||||
kind: "gitlab"
|
||||
spec:
|
||||
url: '{{ requiredEnv "CI_SERVER_URL" }}'
|
||||
owner: "zammad"
|
||||
repository: "zammad"
|
||||
token: '{{ requiredEnv "RENOVATE_TOKEN" }}'
|
||||
username: 'renovate'
|
||||
branch: "develop"
|
||||
workingbranchprefix: "private/updatecli"
|
||||
workingbranchseparator: "/"
|
||||
|
||||
actions:
|
||||
gitlab:
|
||||
kind: gitlab/mergerequest
|
||||
scmid: zammad
|
||||
spec:
|
||||
automerge: false
|
||||
title: "Maintenance: Update Zammad dependencies via updatecli"
|
||||
assignees:
|
||||
- 115 # fl
|
||||
labels:
|
||||
- "dependencies"
|
||||
# {{ end }}
|
||||
@@ -1,47 +0,0 @@
|
||||
# rubocop:disable all
|
||||
class YARD::CustomExtensions
|
||||
# Automatically create modules for every parent namespace for the given name.
|
||||
#
|
||||
# For exampe, if `name` is `Foo::Bar::Baz`, code objects for `Foo` and `Foo::Bar` will be created
|
||||
# and added to YARD's registry before continuing to process `Foo::Bar::Baz` as normal.
|
||||
# This is a copy-paste from the following Github comment
|
||||
# Full credit goes to @jeraki.
|
||||
# https://github.com/lsegal/yard/issues/1002#issuecomment-1201852997
|
||||
def self.autovivify_parents(name)
|
||||
parts = []
|
||||
|
||||
while name =~ /(?:::)([^:]+)$/
|
||||
parts.push($1)
|
||||
name = $`
|
||||
end
|
||||
|
||||
return if parts.empty?
|
||||
|
||||
ns = :root
|
||||
n = name
|
||||
|
||||
loop do
|
||||
ns = YARD::CodeObjects::ModuleObject.new(ns, name)
|
||||
ns.add_file("(autovivifed)")
|
||||
break if parts.empty?
|
||||
name = parts.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class YARD::Handlers::Ruby::ModuleHandler
|
||||
def process
|
||||
modname = statement[0].source
|
||||
YARD::CustomExtensions.autovivify_parents(modname)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
class YARD::Handlers::Ruby::ClassHandler
|
||||
def process
|
||||
classname = statement[0].source.gsub(/\s/, '')
|
||||
YARD::CustomExtensions.autovivify_parents(classname)
|
||||
super
|
||||
end
|
||||
end
|
||||
# rubocop:enable all
|
||||
@@ -1,80 +0,0 @@
|
||||
name: "zammad-devcontainer"
|
||||
|
||||
services:
|
||||
devcontainer:
|
||||
"image": "ghcr.io/rails/devcontainer/images/ruby:3.4.9"
|
||||
|
||||
environment:
|
||||
MEMCACHE_SERVERS: memcached:11211
|
||||
# Don't specify the postgresql database here. This will get merged with an existing database.yml.
|
||||
DATABASE_URL: postgres://zammad:zammad@postgresql:5432
|
||||
REDIS_URL: redis://redis:6379
|
||||
# Performance / DX / tests tuning
|
||||
CI_SKIP_ASSETS_PRECOMPILE: 'true'
|
||||
CI_SKIP_DB_RESET: 'true'
|
||||
RAILS_SERVE_STATIC_FILES: 'true'
|
||||
TZ: 'Europe/London'
|
||||
Z_LOCALES: en-us:en-gb:de-de
|
||||
ES_URL: http://elasticsearch:9200
|
||||
ES_INDEX: estest.local_zammad
|
||||
VITE_RUBY_HOST: '127.0.0.1'
|
||||
BETA_UI_FEEDBACK_API_HOST: 'http://localhost:3000'
|
||||
|
||||
depends_on:
|
||||
- memcached
|
||||
- postgresql
|
||||
- redis
|
||||
|
||||
volumes:
|
||||
- ../../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
elasticsearch:
|
||||
image: elasticsearch:9.5.3
|
||||
environment:
|
||||
- "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
- http.cors.enabled=true
|
||||
- http.cors.allow-origin=/https:\/\/app.elasticvue.com/
|
||||
# Uncomment and change the following settings if you need to allow larger payloads, e.g. for debugging or testing.
|
||||
# Max length of 100MB translates to ~40MB for allowed attachments, due to base64 encoding overhead.
|
||||
# Indexing pressure limit of 10% of heap memory is enough for ~50MB attachments (see a ES_JAVA_OPTS above).
|
||||
# Keep in mind to keep these limits aligned with the following Zammad settings:
|
||||
# `es_attachment_max_size_in_mb=10` and `es_total_max_size_in_mb=300`.
|
||||
# - http.max_content_length=100mb
|
||||
# - indexing_pressure.memory.limit=10%
|
||||
# - indexing_pressure.memory.coordinating.limit=10%
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Expose the ES service for direct access if needed, but avoid conflicts with host services.
|
||||
- 127.0.0.1:19200:9200
|
||||
volumes:
|
||||
- elasticsearch-data:/usr/share/elasticsearch/data
|
||||
|
||||
memcached:
|
||||
command: memcached -m 256M
|
||||
image: memcached:1.6.45-alpine
|
||||
restart: unless-stopped
|
||||
|
||||
postgresql:
|
||||
environment:
|
||||
POSTGRES_DB: zammad_development
|
||||
POSTGRES_USER: zammad
|
||||
POSTGRES_PASSWORD: zammad
|
||||
image: postgres:17.5-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgresql-data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7.0
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
elasticsearch-data:
|
||||
driver: local
|
||||
postgresql-data:
|
||||
driver: local
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/anthropics/devcontainer-features/claude-code:latest": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a",
|
||||
"integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/apt-get-packages": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "ghcr.io/devcontainers-extra/features/apt-get-packages@sha256:776fa11c30c50fa91d5e1bf050444b0f130827457109c349db92e8e126b750ec",
|
||||
"integrity": "sha256:776fa11c30c50fa91d5e1bf050444b0f130827457109c349db92e8e126b750ec"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/shellcheck:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers-extra/features/shellcheck@sha256:8e6d050e4ead6a0350dbe4c70611e351f32cb61a8cc5cb2b39da0cf29ff3bed5",
|
||||
"integrity": "sha256:8e6d050e4ead6a0350dbe4c70611e351f32cb61a8cc5cb2b39da0cf29ff3bed5"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e",
|
||||
"integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/go:1": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032",
|
||||
"integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/node@sha256:586c9a6f7dd40bd3ba2cd41e7f2f88dcc31fbe5d1442afcbf07ffbc66b686857",
|
||||
"integrity": "sha256:586c9a6f7dd40bd3ba2cd41e7f2f88dcc31fbe5d1442afcbf07ffbc66b686857"
|
||||
},
|
||||
"ghcr.io/rails/devcontainer/features/postgres-client": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "ghcr.io/rails/devcontainer/features/postgres-client@sha256:7e6b118646d6e0d82a9ec06e81ad1b5406f7042f0279d35fff3a7a612be7a050",
|
||||
"integrity": "sha256:7e6b118646d6e0d82a9ec06e81ad1b5406f7042f0279d35fff3a7a612be7a050"
|
||||
},
|
||||
"ghcr.io/rapidsai/devcontainers/features/gitlab-cli:latest": {
|
||||
"version": "26.8.2",
|
||||
"resolved": "ghcr.io/rapidsai/devcontainers/features/gitlab-cli@sha256:45b15ff1035cb7c2ac657ee069f9c548744dd6d70311f7abbd3aea12c5ec0c92",
|
||||
"integrity": "sha256:45b15ff1035cb7c2ac657ee069f9c548744dd6d70311f7abbd3aea12c5ec0c92"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// For format details, see https://containers.dev/implementors/json_reference/.
|
||||
// For config options, see the README at: https://github.com/devcontainers/templates/tree/main/src/ruby
|
||||
{
|
||||
"name": "Zammad (default)",
|
||||
"dockerComposeFile": "compose.yaml",
|
||||
"service": "devcontainer",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
|
||||
"moby": false
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/shellcheck:1": {},
|
||||
"ghcr.io/devcontainers/features/node:2": {
|
||||
"installYarnUsingApt": "false"
|
||||
},
|
||||
"ghcr.io/rails/devcontainer/features/postgres-client": {},
|
||||
"ghcr.io/devcontainers-extra/features/apt-get-packages": {
|
||||
"packages": "curl gnupg libimlib2-dev"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/go:1": {},
|
||||
"ghcr.io/rapidsai/devcontainers/features/gitlab-cli:latest": {},
|
||||
"ghcr.io/anthropics/devcontainer-features/claude-code:latest": {},
|
||||
"../features/zammad-deps": {},
|
||||
"../features/add-bin-path": {
|
||||
"addBinPath": "/workspaces/${localWorkspaceFolderBasename}/bin"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
// Avoid conflicts with host-installed node_modules with a different platform/architecture.
|
||||
"source": "node-modules",
|
||||
"target": "/workspaces/${localWorkspaceFolderBasename}/node_modules",
|
||||
"type": "volume"
|
||||
},
|
||||
{
|
||||
"source": "pnpm-store",
|
||||
"target": "/workspaces/${localWorkspaceFolderBasename}/.pnpm-store",
|
||||
"type": "volume"
|
||||
}
|
||||
],
|
||||
"containerEnv": {},
|
||||
"forwardPorts": [
|
||||
3000,
|
||||
3001,
|
||||
3036,
|
||||
6042
|
||||
],
|
||||
"postCreateCommand": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/post_create.sh",
|
||||
"postStartCommand": "git config --global --add safe.directory /workspaces/${localWorkspaceFolderBasename}"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"id": "add-bin-path",
|
||||
"version": "1.0.0",
|
||||
"name": "Add Bin Path",
|
||||
"description": "Adds a custom bin path to the container environment.",
|
||||
"options": {
|
||||
"addBinPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "The path to add to the container's PATH."
|
||||
}
|
||||
},
|
||||
"privileged": true
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
echo "export PATH=$ADDBINPATH:\$PATH" > /etc/profile.d/add_bin_path.sh
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"id": "zammad-deps",
|
||||
"version": "1.0.0",
|
||||
"name": "Zammad Dependency Installer",
|
||||
"description": "Installs dependencies required for Zammad.",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"andyyaldoo.vscode-json",
|
||||
"anthropic.claude-code",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"dariofuzinato.vue-peek",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dotjoshjohnson.xml",
|
||||
"eamodio.gitlens",
|
||||
"ezforo.copy-relative-path-and-line-numbers",
|
||||
"jock.svg",
|
||||
"kaiwood.endwise",
|
||||
"mde.select-highlight-minimap",
|
||||
"oxc.oxc-vscode",
|
||||
"redhat.vscode-yaml",
|
||||
"shardulm94.trailing-spaces",
|
||||
"shopify.ruby-lsp",
|
||||
"slb235.vscode-coffeelint",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"timonwong.shellcheck",
|
||||
"vitest.explorer",
|
||||
"vue.volar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"privileged": true,
|
||||
"installsAfter": ["ghcr.io/devcontainers/features/go"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
export PATH="/usr/local/go/bin:$PATH"
|
||||
|
||||
go install github.com/ddollar/forego@master
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user