Compare commits

..

16 Commits

Author SHA1 Message Date
Martin Edenhofer
911896f558 Update for 3.5 release. 2020-09-22 08:11:28 +02:00
Martin Edenhofer
89eae934bf Fixed issue #3198 - Unable to open ticket/content not loading. 2020-09-16 22:53:49 +02:00
Rolf Schmidt
c4c43b3df4 Fixes #3191 - data privacy obsolete column name. 2020-09-15 13:18:03 +02:00
Mantas
28a31b241a Fixes #3186 - Endpoint api/v1/ticket_articles/by_ticket ignores X-On-Behalf-Of 2020-09-15 09:13:30 +02:00
Rolf Schmidt
9b93987f99 Peformance - Fixed missing ignored attributes for data privacy feature. 2020-09-14 14:56:25 +02:00
Mantas
6c4956a748 CVE-2020-15169 2020-09-10 18:01:51 +02:00
Thorsten Eckel
60c96c8dff Revert "Follow up - f54df50 - Fixes #3040: Use focusout instead of click event to stabilize tests."
This reverts commit b828c7bda0.

It introduced a bug in the behavior of the reply input field. E.g. toggling the article type was no longer possible.
2020-09-08 18:56:11 +02:00
Thorsten Eckel
b91a77f87f Refactoring: Use more explicit methods to reason about code. 2020-09-08 17:09:52 +02:00
Rolf Schmidt
b8481838cf Fixes #2074 - Ability of deleting customers and / or all ticket at once. 2020-09-08 17:06:23 +02:00
Thorsten Eckel
3d9b2eb1b9 Maintenance: Ensured HTTP request verbs according to REST paradigm. 2020-09-07 15:02:18 +02:00
Mantas Masalskis
a18c613c7b Fixes #2943 (raise default password security) + #2907 (Password strength settings are ignored when creating new customer accounts) 2020-09-07 15:02:18 +02:00
Marcel Herrguth
ae38999b17 Fixes #3128 - Add SSO login button to login page. 2020-09-07 15:02:18 +02:00
Thorsten Eckel
afa4183421 Refactoring: Reverted unneeded HTML option customization of bootstrap-tokenfield.js. 2020-09-07 15:02:17 +02:00
Thorsten Eckel
0fe4f37808 Maintenance: Improve logging of failures when sending SMS via Massenversand.de. 2020-09-07 15:02:17 +02:00
Mantas Masalskis
61facbe7f1 Maintenance: Show less error details to non-admin users 2020-09-07 15:02:17 +02:00
Mantas Masalskis
70b55ac288 Maintenance: Enhance KB global search result scope. 2020-09-07 15:02:17 +02:00
14362 changed files with 241198 additions and 2649261 deletions

View File

@@ -1,2 +0,0 @@
https://github.com/heroku/heroku-buildpack-nodejs.git#v371
https://github.com/pkgr/heroku-buildpack-ruby.git#v361-1

View File

@@ -1 +0,0 @@
../.dev/ai-agent-instructions.md

View File

@@ -1 +0,0 @@
../../.dev/agents/code-reviewer.md

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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/

View File

@@ -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
View File

@@ -0,0 +1,2 @@
--exclude-exts=.min.css
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes

View File

@@ -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 | &lt;verbatim text&gt; | Met | `app/foo.rb:42` |
| 2 | &lt;verbatim text&gt; | Not met | not addressed |
| 3 | &lt;verbatim text&gt; | 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.

View File

@@ -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)
```

View File

@@ -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.

View File

@@ -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.

View File

@@ -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/`)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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, ...`.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View 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`

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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/*": "./*"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
---
allowBuilds:
cypress: true
sharp: true
# Restore old default behavior like in pnpm v10.
verifyDepsBeforeRun: false

View File

@@ -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)
},
)

View File

@@ -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>

View File

@@ -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)

View File

@@ -1,4 +0,0 @@
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
// eslint-disable-next-line no-underscore-dangle
window.__ = (str) => str

View File

@@ -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 {}

View File

@@ -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,
})
})
})
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.*'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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.'

View File

@@ -1,3 +0,0 @@
---
gitlab:
enabled: true

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}
}

View File

@@ -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}"
}

View File

@@ -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
}

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env sh
echo "export PATH=$ADDBINPATH:\$PATH" > /etc/profile.d/add_bin_path.sh

View File

@@ -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"]
}

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env sh
export PATH="/usr/local/go/bin:$PATH"
go install github.com/ddollar/forego@master

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env sh
env
# Ensure proper permissions on root-mounted volumes.
sudo chown -R "${USER}" node_modules .pnpm-store
export SHELL=/bin/bash
pnpm setup
export PNPM_HOME="${HOME}/.local/share/pnpm"
export PATH="${PNPM_HOME}/bin:${PATH}"
pnpm config set store-dir "$(pwd)/.pnpm-store" --global
bin/setup --skip-server
echo "== Initialize CodeGraph index =="
pnpm run codegraph:init
echo "== Precompile Ruby cache =="
bundle exec bootsnap precompile --gemfile app/ lib/
echo "== Configure Elasticsearch URL =="
bundle exec rails r "Setting.set('es_url', 'http://elasticsearch:9200')"
echo "== Run auto_wizard setup =="
bundle exec rails zammad:setup:auto_wizard
echo "== Rebuild search index =="
bundle exec rails zammad:searchindex:rebuild
echo "== Precompile frontend assets =="
bundle exec rails assets:precompile
echo "== Prepare test database =="
RAILS_ENV='test' bundle exec rails db:drop db:create zammad:ci:test:prepare || exit 1

View File

@@ -1,89 +0,0 @@
# Zammad devcontainer - with LDAP
A devcontainer variant that adds an OpenLDAP sidecar pre-configured for
Zammad LDAP integration testing.
## Ports
| Service | Host port | Protocol |
|---------|-----------|----------|
| LDAP | 1389 | plain / STARTTLS |
| LDAPS | 1636 | TLS |
## Pre-seeded directory
Base DN: `dc=foo,dc=example,dc=com`
Admin DN: `cn=admin,dc=foo,dc=example,dc=com` - password `test`
### Users (`uid` / password)
| uid | Name | Password |
|-----------|---------------|-----------|
| nb | Nicole Braun | testnb |
| ab | Albert Braun | testab |
| bb | Berta Braun | testbb |
| fb | Frida Braun | testfb |
| lb | Lena Braun | testlb |
| eb | Elke Braun | testeb |
| customer1 | Customer 1 | testc1 |
| … | … | … |
| customer8 | Customer 8 | testc8 |
### Groups (`ou=groups,dc=foo,dc=example,dc=com`)
`admin`, `1st level`, `2nd level`, `3rd level`, `4th level`, `sales`
The post-create script maps `cn=admin,ou=groups,…` → Zammad role Admin and
`cn=1st level,ou=groups,…` → Zammad role Agent.
## Configuration
Edit the `environment:` block in `.devcontainer/with-ldap/compose.yaml` and
rebuild the devcontainer (VSCode will prompt you automatically when the file
changes).
Key variables:
| Variable | Default | Description |
|------------------------|------------------------------|-------------|
| `LDAP_BASE_DN` | `dc=foo,dc=example,dc=com` | Directory base DN |
| `LDAP_ADMIN_DN` | `cn=admin,<LDAP_BASE_DN>` | Admin bind DN (derived if unset) |
| `LDAP_ADMIN_PASSWORD` | `test` | Admin password |
| `LDAP_DISALLOW_BIND_ANON` | `false` | Reject anonymous binds when `true` |
> **Note:** `.devcontainer/with-ldap/scripts/post_create.sh` also contains the
> base DN, admin DN, and admin password as hardcoded strings (in the
> `LdapSource.create!` call and `group_role_map` keys). If you change any of
> the values above in `compose.yaml`, update the corresponding values in
> `post_create.sh` as well, then rebuild the devcontainer.
## TLS certificates
A self-signed CA and server certificate are generated automatically on first
container startup into `ldap/certs/` (gitignored, only a `.gitkeep` is
committed). The post-create script imports the CA from that path into Zammad's
SSL store.
To use your own certificates, place `ca.crt`, `ldap.crt`, and `ldap.key` in
`ldap/certs/` before starting the container. The entrypoint skips generation
when `ldap.key` is already present, and the post-create script imports
`ca.crt` from that path automatically — no manual steps needed.
If you need to swap certificates on an already-running devcontainer, import
the new CA manually:
```sh
bundle exec rails r "SSLCertificate.create!(certificate: Rails.root.join('.devcontainer/with-ldap/ldap/certs/ca.crt').read)"
```
## Customising the directory seed
Edit `.devcontainer/with-ldap/ldap/bootstrap/ldif/zammad.ldif` before the
first container start. The LDIF is loaded only once (on initialisation). To
re-seed, remove the container and restart:
```sh
docker compose -f .devcontainer/with-ldap/compose.yaml rm -sv ldap
docker compose -f .devcontainer/with-ldap/compose.yaml up -d ldap
```

View File

@@ -1,18 +0,0 @@
name: "zammad-devcontainer"
include: [../default/compose.yaml]
services:
ldap:
build: ./ldap
environment:
LDAP_BASE_DN: dc=foo,dc=example,dc=com
LDAP_ADMIN_DN: cn=admin,dc=foo,dc=example,dc=com
LDAP_ADMIN_PASSWORD: test
LDAP_DISALLOW_BIND_ANON: "false"
volumes:
- ./ldap/certs:/certs
ports:
- 127.0.0.1:1389:389 # LDAP (no TLS/STARTTLS)
- 127.0.0.1:1636:636 # LDAP (TLS)
restart: unless-stopped

View File

@@ -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"
}
}
}

View File

@@ -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 (with LDAP)",
"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/with-ldap/scripts/post_create.sh",
"postStartCommand": "git config --global --add safe.directory /workspaces/${localWorkspaceFolderBasename}"
}

View File

@@ -1,23 +0,0 @@
FROM alpine:3
RUN apk add --no-cache \
openldap \
openldap-back-mdb \
openldap-clients \
openssl
ENV LDAP_BASE_DN="dc=foo,dc=example,dc=com"
# LDAP_ADMIN_PASSWORD defaults to "test" in the entrypoint — override at runtime
ENV LDAP_PORT="389"
ENV LDAP_LDAPS_PORT="636"
COPY docker-entrypoint.sh /usr/local/bin/
COPY bootstrap/ldif/ /ldifs/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh \
&& mkdir -p /var/lib/openldap/openldap-data \
&& chown -R ldap:ldap /ldifs /var/lib/openldap
EXPOSE 389 636
ENTRYPOINT ["docker-entrypoint.sh"]

View File

@@ -1,302 +0,0 @@
dn: dc=foo,dc=example,dc=com
objectClass: top
objectClass: domain
dc: foo
dn: cn=nb,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Nicole
sn: Braun
uid: nb
uidNumber: 5000
gidNumber: 5000
userPassword: testnb
homeDirectory: /home/nb
loginShell: /bin/sh
gecos: Comments
mail: nb@example.com
telephoneNumber: 110
roomNumber: 0200
dn: cn=ab,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Albert
sn: Braun
uid: ab
uidNumber: 5001
gidNumber: 5000
userPassword: testab
homeDirectory: /home/ab
loginShell: /bin/sh
gecos: Comments
mail: ab@example.com
telephoneNumber: 111
roomNumber: 0201
dn: cn=bb,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Berta
sn: Braun
uid: bb
uidNumber: 5002
gidNumber: 5000
userPassword: testbb
homeDirectory: /home/bb
loginShell: /bin/sh
gecos: Comments
mail: bb@example.com
telephoneNumber: 112
roomNumber: 0202
dn: cn=fb,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Frida
sn: Braun
uid: fb
uidNumber: 5003
gidNumber: 5000
userPassword: testfb
homeDirectory: /home/fb
loginShell: /bin/sh
gecos: Comments
mail: fb@example.com
telephoneNumber: 113
roomNumber: 0203
dn: cn=lb,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Lena
sn: Braun
uid: lb
uidNumber: 5004
gidNumber: 5000
userPassword: testlb
homeDirectory: /home/lb
loginShell: /bin/sh
gecos: Comments
mail: lb@example.com
telephoneNumber: 114
roomNumber: 0204
dn: cn=eb,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Elke
sn: Braun
uid: eb
uidNumber: 5005
gidNumber: 5000
userPassword: testeb
homeDirectory: /home/eb
loginShell: /bin/sh
gecos: Comments
mail: eb@example.com
telephoneNumber: 115
roomNumber: 0205
dn: cn=customer1,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 1
uid: customer1
uidNumber: 6001
gidNumber: 6000
userPassword: testc1
homeDirectory: /home/customer1
loginShell: /bin/sh
gecos: Comments
mail: customer1@example.com
telephoneNumber: 1001
roomNumber: 2001
dn: cn=customer2,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 2
uid: customer2
uidNumber: 6002
gidNumber: 6000
userPassword: testc2
homeDirectory: /home/customer2
loginShell: /bin/sh
gecos: Comments
mail: customer2@example.com
telephoneNumber: 1002
roomNumber: 2002
dn: cn=customer3,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 3
uid: customer3
uidNumber: 6003
gidNumber: 6000
userPassword: testc3
homeDirectory: /home/customer3
loginShell: /bin/sh
gecos: Comments
mail: customer3@example.com
telephoneNumber: 1003
roomNumber: 2003
dn: cn=customer4,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 4
uid: customer4
uidNumber: 6004
gidNumber: 6000
userPassword: testc4
homeDirectory: /home/customer4
loginShell: /bin/sh
gecos: Comments
mail: customer4@example.com
telephoneNumber: 1004
roomNumber: 2004
dn: cn=customer5,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 5
uid: customer5
uidNumber: 6005
gidNumber: 6000
userPassword: testc5
homeDirectory: /home/customer5
loginShell: /bin/sh
gecos: Comments
mail: customer5@example.com
telephoneNumber: 1005
roomNumber: 2005
dn: cn=customer6,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 6
uid: customer6
uidNumber: 6006
gidNumber: 6000
userPassword: testc6
homeDirectory: /home/customer6
loginShell: /bin/sh
gecos: Comments
mail: customer6@example.com
telephoneNumber: 1000
roomNumber: 2000
dn: cn=customer7,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 7
uid: customer7
uidNumber: 6007
gidNumber: 6000
userPassword: testc7
homeDirectory: /home/customer7
loginShell: /bin/sh
gecos: Comments
mail: customer7@example.com
telephoneNumber: 1007
roomNumber: 2007
dn: cn=customer8,dc=foo,dc=example,dc=com
objectClass: posixAccount
objectClass: shadowAccount
objectClass: inetOrgPerson
cn: Customer
sn: 8
uid: customer8
uidNumber: 6008
gidNumber: 6000
userPassword: testc8
homeDirectory: /home/customer8
loginShell: /bin/sh
gecos: Comments
mail: customer8@example.com
telephoneNumber: 1008
roomNumber: 2008
dn: ou=groups,dc=foo,dc=example,dc=com
objectClass: organizationalUnit
ou: groups
dn: cn=admin,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=ab,dc=foo,dc=example,dc=com
member: cn=bb,dc=foo,dc=example,dc=com
dn: cn=1st level,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=eb,dc=foo,dc=example,dc=com
member: cn=lb,dc=foo,dc=example,dc=com
dn: cn=2nd level,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=fb,dc=foo,dc=example,dc=com
member: cn=lb,dc=foo,dc=example,dc=com
dn: cn=3rd level,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=3rd level - north,ou=groups,dc=foo,dc=example,dc=com
member: cn=3rd level - west,ou=groups,dc=foo,dc=example,dc=com
dn: cn=3rd level - north,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=fb,dc=foo,dc=example,dc=com
dn: cn=3rd level - west,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=lb,dc=foo,dc=example,dc=com
dn: cn=4th level,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=4th level - north,ou=groups,dc=foo,dc=example,dc=com
member: cn=4th level - west,ou=groups,dc=foo,dc=example,dc=com
dn: cn=4th level - north,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=ab,dc=foo,dc=example,dc=com
dn: cn=4th level - west,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=bb,dc=foo,dc=example,dc=com
dn: cn=sales,ou=groups,dc=foo,dc=example,dc=com
objectClass: top
objectClass: groupOfNames
member: cn=ab,dc=foo,dc=example,dc=com
member: cn=nb,dc=foo,dc=example,dc=com

Some files were not shown because too many files have changed in this diff Show More