generate json schema

Signed-off-by: cinereal <cinereal@riseup.net>
This commit is contained in:
cinereal
2026-03-29 14:07:31 +02:00
parent cb2e770584
commit a5f77b6eac
8 changed files with 6812 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
name: Check config schema
on:
push:
branches:
- 'main'
- 'stable-*'
paths:
- 'lib/mastodon/configuration/annotations.yml'
- 'lib/mastodon/configuration/schema.rb'
- 'lib/mastodon/configuration/env_scanner.rb'
- 'lib/mastodon/configuration/docs_generator.rb'
- 'lib/tasks/config.rake'
- 'mastodon-config.schema.json'
- 'config/**/*.rb'
- 'config/**/*.yml'
- 'lib/mastodon/**/*.rb'
- '.github/workflows/check-config-schema.yml'
pull_request:
paths:
- 'lib/mastodon/configuration/annotations.yml'
- 'lib/mastodon/configuration/schema.rb'
- 'lib/mastodon/configuration/env_scanner.rb'
- 'lib/mastodon/configuration/docs_generator.rb'
- 'lib/tasks/config.rake'
- 'mastodon-config.schema.json'
- 'config/**/*.rb'
- 'config/**/*.yml'
- 'lib/mastodon/**/*.rb'
- '.github/workflows/check-config-schema.yml'
permissions:
contents: read
jobs:
check-config-schema:
runs-on: ubuntu-latest
env:
BUNDLE_ONLY: development
steps:
- name: Clone repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Ruby
uses: ruby/setup-ruby@4eb9f110bac952a8b68ecf92e3b5c7a987594ba6 # v1
with:
bundler-cache: true
- name: Check all ENV vars are documented
run: bundle exec rails mastodon:config:lint
- name: Regenerate config schema
run: bundle exec rails mastodon:config:schema > mastodon-config.schema.json
- name: Check schema is up to date
run: |
if ! git diff --exit-code mastodon-config.schema.json; then
echo ""
echo "mastodon-config.schema.json is out of date."
echo "Run the following command and commit the result:"
echo ""
echo " bundle exec rails mastodon:config:schema > mastodon-config.schema.json"
echo ""
exit 1
fi
- name: Verify docs generation runs without error
run: bundle exec rails mastodon:config:docs > /dev/null

193
docs/CONFIG_SCHEMA.md Normal file
View File

@@ -0,0 +1,193 @@
# Configuration schema
Mastodon ships a machine-readable JSON Schema (draft 2020-12)
that describes every environment variable the application reads.
`annotations.yml` is the canonical source; both the JSON schema and the
admin docs page are generated from it.
## Generating the schema
With a working Ruby environment and all gems installed, run:
```shell
bundle exec rails mastodon:config:schema > mastodon-config.schema.json
```
## Generating the admin docs page
The Hugo-flavored Markdown for `content/en/admin/config.md` in
`mastodon/documentation` is generated from the committed JSON schema:
```shell
bundle exec rails mastodon:config:docs > /path/to/documentation/content/en/admin/config.md
```
The task reads `mastodon-config.schema.json` in the project root by default.
Pass an explicit path as an argument if needed:
```shell
bundle exec rails 'mastodon:config:docs[/path/to/mastodon-config.schema.json]'
```
The generated Markdown should be committed in the documentation repository.
Regenerate it whenever `annotations.yml` changes and a new Mastodon release is
cut.
## Inter-repo workflow
`mastodon/mastodon` owns `annotations.yml` and `mastodon-config.schema.json`.
`mastodon/documentation` consumes the generated Markdown. Two delivery options:
- **Manual**: a docs maintainer runs `mastodon:config:docs` against a tagged
Mastodon release and commits the output.
- **Automated**: a docs-repo workflow checks out a Mastodon release, runs the
task, and opens a PR.
## Schema structure
The top-level object is a JSON Schema `object` whose `properties` are the
environment-variable names. Each property carries:
| Field | Purpose |
|-------|---------|
| `type` | Semantic type (`string`, `integer`, `boolean`, or `number`). |
| `description` | Human-readable explanation of the variable, including its effect and any caveats. |
| `default` | The value Mastodon uses when the variable is absent. Omitted when there is no meaningful default. |
| `enum` | Allowed values for constrained strings. |
| `minimum` / `maximum` | Numeric bounds. |
| `format` | JSON Schema semantic format hint (e.g. `uri`, `email`). |
| `examples` | Representative values shown in UIs and documentation. |
| `x-group` | *(Extension)* Logical grouping name — used by UIs to cluster related settings. |
| `x-secret` | *(Extension)* `true` when the value is a cryptographic secret that should never be displayed or logged. |
| `x-restart-required` | *(Extension)* `false` when a change can take effect without restarting Mastodon processes (rare). Absent on most properties, meaning a restart is always required. |
| `x-status` | *(Extension)* `"deprecated"` or `"removed"`. Absent on active variables. |
| `x-version-history` | *(Extension)* Ordered list of `{version, change}` objects describing when the variable was added or changed. |
| `x-example-value` | *(Extension)* A single representative value rendered as `Example value: \`…\`` in docs. |
| `x-anchor` | *(Extension)* Explicit HTML anchor override; pass `""` to suppress the anchor entirely on a `removed` variable. |
| `x-hints` | *(Extension)* List of `{style, body}` Hugo hint shortcode blocks (`style` is `info`, `warning`, or `danger`). Emitted after the description. |
| `x-extra` | *(Extension)* Prose paragraph rendered after the hints and before the version-history block. |
| `x-trailing` | *(Extension)* Prose paragraph rendered after the example value (the very last body element). |
| `x-show-default` | *(Extension)* When `true`, emit a `**Default:** \`…\`` block in the rendered docs (most defaults are described inline in prose). |
| `x-suppress-removed-hint` | *(Extension)* When `true`, render a `removed` variable's description as plain prose instead of wrapping it in a danger hint. |
The schema also carries a top-level `x-docs-layout` object (not a per-property
field) that encodes the Hugo frontmatter and section tree used to generate the
admin docs page. It is consumed by `mastodon:config:docs` and is not
meaningful to standard JSON Schema validators. `x-docs-layout.docs_only_variables`
holds annotation entries for variables that should appear in the rendered docs
but are not part of the live configuration surface (tombstones for removed
variables, Rails-internal vars upstream documents).
### Subsection fields
Subsections inside `docs.sections[*].subsections[*]` accept:
| Field | Purpose |
|-------|---------|
| `title`, `anchor` | Header label and explicit anchor ID. |
| `page_refs` | List of pages rendered as `{{< page-ref page="…" >}}` shortcodes (emitted first). |
| `pre_version_history` | Version-history block rendered before any prose (matches upstream's "Fetch All Replies" ordering). |
| `pre_hints` | Hint shortcodes rendered before the intro paragraph. |
| `intro` | Multi-paragraph Markdown intro. |
| `version_history` | Version-history block rendered after the intro. |
| `hints` | Hint shortcodes rendered after the intro and version history. |
| `variables` | Ordered list of variable names. |
| `subsections` | Nested sibling subsections (rendered at the same heading level — upstream uses a flat structure under SMTP). |
### Groups
| Group | Variables covered |
|-------|------------------|
| `federation` | Domain name, federation mode, single-user mode |
| `database` | PostgreSQL primary and read-replica connections |
| `redis` | Main, Sidekiq, and cache Redis connections including Sentinel |
| `email` | SMTP and bulk-mail SMTP settings |
| `storage` | S3, OpenStack Swift, Azure Blob Storage, local filesystem |
| `search` | Elasticsearch / OpenSearch |
| `authentication` | LDAP, PAM, OIDC, SAML, CAS, SSO behaviour |
| `web-server` | Puma, Sidekiq, proxy, and CDN settings |
| `secrets` | Cryptographic keys and tokens |
| `features` | Behavioural feature flags |
| `retention` | IP, session, and user-activity retention periods |
| `translation` | DeepL and LibreTranslate integration |
| `captcha` | hCaptcha |
| `cache-buster` | CDN cache purge integration |
| `observability` | Prometheus exporter and OpenTelemetry |
| `media` | ffmpeg paths and S3 batch-delete tuning |
## Adding new variables
Property metadata lives in
[`lib/mastodon/configuration/annotations.yml`](../lib/mastodon/configuration/annotations.yml).
Add a new top-level key for the variable name. Minimum required fields:
```yaml
MY_NEW_VAR:
type: string # string / integer / boolean / number
group: features
description: What this variable does.
default: some-value # omit if there is no meaningful default
enum: [a, b, c] # omit if values are unconstrained
secret: true # set when the value must not be logged or displayed
```
Optional docs-specific fields:
```yaml
MY_NEW_VAR:
# ... required fields above ...
description: |
Long-form Markdown prose for the docs page. Multi-paragraph, code blocks,
and inline links are allowed.
version_history:
- version: 4.4.0
change: Added.
example_value: my-value # rendered as `Example value: \`my-value\``
status: active # active (default) | deprecated | removed
anchor: my-anchor-override # rare
show_default: true # emit a "**Default:** \`…\`" block
hints:
- style: warning # info | warning | danger
body: |
Markdown body of the Hugo hint shortcode.
extra: |
Additional prose rendered after the hints (e.g. an inline "Defaults to false." note).
trailing: |
Additional prose rendered after the example value (e.g. supplementary links).
```
Also add the variable to the appropriate subsection in the `docs.sections`
tree at the bottom of `annotations.yml` so it appears in the generated docs
page.
The `EnvScanner` will catch any variable that appears in the source but is
absent from `annotations.yml` (see [Lint check](#lint-check) below).
Run `bundle exec rails mastodon:config:schema > mastodon-config.schema.json`
after editing and commit the updated JSON file.
### Tombstone variables
Variables that have been removed from the codebase should be kept in
`annotations.yml` with `status: removed` (and a `version_history` entry
recording when they were removed) so that the generated docs preserves
historical anchors and version-history blocks for users upgrading from old
installations. They do not need to be moved to `EnvScanner::EXCLUDED_VARS`.
## Lint check
`bundle exec rails mastodon:config:lint` statically scans the source tree for
literal `ENV.fetch` / `ENV[]` accesses and reports any variable that is absent
from `annotations.yml` and not in the explicit exclusion list
(`EnvScanner::EXCLUDED_VARS`). CI runs this check automatically on every PR
that touches `config/`, `lib/mastodon/`, or the schema files.
If you add a new env var without updating `annotations.yml`, CI will fail and
tell you exactly which file uses the undocumented variable.
Variables that are intentionally undocumented (deprecated aliases, Rails
internals, CI/dev-only vars) belong in `EnvScanner::EXCLUDED_VARS` rather than
in `annotations.yml`.
Regenerate `mastodon-config.schema.json` whenever you upgrade Mastodon to pick
up newly added variables.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
# frozen_string_literal: true
require 'yaml'
module Mastodon
module Configuration
# Generates Hugo-flavored Markdown for `content/en/admin/config.md` from
# a JSON Schema hash that contains `x-docs-layout` (the section tree) and
# per-property `x-*` fields populated from `annotations.yml`.
#
# Usage:
# schema = JSON.parse(File.read('mastodon-config.schema.json'))
# puts Mastodon::Configuration::DocsGenerator.render(schema)
#
# Or via the rake task:
# bundle exec rails mastodon:config:docs > /path/to/content/en/admin/config.md
module DocsGenerator
def self.render(schema)
layout = schema['x-docs-layout'] || {}
props = schema['properties'] || {}
docs_only = layout['docs_only_variables'] || {}
all_vars = props.merge(docs_only)
warn_unlisted(layout, props)
out = []
out << emit_frontmatter(layout['frontmatter'])
out << layout['intro'].to_s.strip unless layout['intro'].to_s.strip.empty?
out << ''
(layout['sections'] || []).each do |section|
out << emit_section(section, all_vars)
end
out.join("\n").rstrip + "\n"
end
def self.warn_unlisted(layout, props)
listed = collect_listed(layout)
props.each_key do |var|
next if listed.include?(var)
group = props[var]['x-group']
warn "WARNING: #{var} (group: #{group}) is not listed in any docs section tree subsection"
end
end
private_class_method :warn_unlisted
def self.collect_listed(layout)
listed = []
(layout['sections'] || []).each do |section|
(section['subsections'] || []).each do |sub|
listed.concat(sub['variables'] || [])
(sub['subsections'] || []).each do |nested|
listed.concat(nested['variables'] || [])
end
end
end
listed.to_set
end
private_class_method :collect_listed
def self.emit_frontmatter(fm)
return '' unless fm
"---\n#{fm.to_yaml.sub(/\A---\n/, '')}---\n"
end
private_class_method :emit_frontmatter
def self.emit_section(section, props)
out = []
anchor = section['anchor'] ? " {##{section['anchor']}}" : ''
out << "## #{section['title']}#{anchor}"
out << ''
(section['page_refs'] || []).each { |p| out << emit_page_ref(p) }
if section['intro']
out << section['intro'].strip
out << ''
end
(section['hints'] || []).each { |h| out << emit_hint(h) }
(section['subsections'] || []).each do |sub|
out << emit_subsection(sub, props)
end
out.join("\n")
end
private_class_method :emit_section
def self.emit_subsection(sub, props, level: 3)
out = []
anchor = sub['anchor'] ? " {##{sub['anchor']}}" : ''
out << "#{'#' * level} #{sub['title']}#{anchor}"
out << ''
(sub['page_refs'] || []).each { |p| out << emit_page_ref(p) }
if sub['pre_version_history']
out << version_history_block(sub['pre_version_history'])
out << ''
end
(sub['pre_hints'] || []).each { |h| out << emit_hint(h) }
if sub['intro']
out << sub['intro'].strip
out << ''
end
if sub['version_history']
out << version_history_block(sub['version_history'])
out << ''
end
(sub['hints'] || []).each { |h| out << emit_hint(h) }
(sub['variables'] || []).each do |var|
prop = props[var]
unless prop
warn "WARNING: variable #{var} listed in docs section tree but not found in schema properties or docs_only_variables"
next
end
out << emit_variable(var, prop)
end
# subsections share the parent's heading level (siblings), matching upstream's flat structure.
(sub['subsections'] || []).each do |nested|
out << emit_subsection(nested, props, level: level)
end
out.join("\n")
end
private_class_method :emit_subsection
def self.emit_variable(name, prop)
out = []
status = prop['x-status'] || 'active'
badge = case status
when 'removed' then ' {{%removed%}}'
when 'deprecated' then ' {{%deprecated%}}'
else ''
end
explicit_anchor = prop['x-anchor']
anchor_part =
if explicit_anchor && !explicit_anchor.empty?
" {##{explicit_anchor}}"
elsif explicit_anchor == ''
''
elsif status == 'removed'
" {##{name.downcase}}"
else
''
end
out << "#### `#{name}`#{badge}#{anchor_part}"
out << ''
body = []
if status == 'removed' && !prop['x-suppress-removed-hint'] && !prop['description'].to_s.strip.empty?
body << emit_hint({ 'style' => 'danger', 'body' => prop['description'].strip }).rstrip
body << ''
elsif !prop['description'].to_s.strip.empty?
body << prop['description'].strip
body << ''
end
if prop['x-show-default'] && prop.key?('default') && !prop['default'].nil?
body << "**Default:** `#{prop['default']}`"
body << ''
end
(prop['x-hints'] || []).each { |h| body << emit_hint(h).rstrip; body << '' }
if prop['x-extra']
body << prop['x-extra'].strip
body << ''
end
if prop['x-version-history']&.any?
body << version_history_block(prop['x-version-history'])
body << ''
end
if prop.key?('x-example-value')
body << "Example value: `#{prop['x-example-value']}`"
body << ''
end
if prop['x-trailing']
body << prop['x-trailing'].strip
body << ''
end
out.concat(body)
out.join("\n")
end
private_class_method :emit_variable
def self.version_history_block(entries)
lines = ['**Version history:**\\']
entries.each_with_index do |entry, i|
suffix = i == entries.length - 1 ? '' : '\\'
lines << "#{entry['version']} - #{entry['change']}#{suffix}"
end
lines.join("\n")
end
private_class_method :version_history_block
def self.emit_hint(hint)
style = hint['style'] || 'info'
body = hint['body'].to_s.strip
"{{< hint style=\"#{style}\" >}}\n#{body}\n{{</ hint >}}\n\n"
end
private_class_method :emit_hint
def self.emit_page_ref(page)
"{{< page-ref page=\"#{page}\" >}}\n\n"
end
private_class_method :emit_page_ref
end
end
end

View File

@@ -0,0 +1,137 @@
# frozen_string_literal: true
require 'pathname'
require 'set'
module Mastodon
module Configuration
# Scans the Mastodon source tree for every *literal* environment variable
# key that is read via ENV[], ENV.fetch(), ENV.key?(), or ENV.include?().
#
# Keys with dynamic names (e.g. ENV.fetch("#{prefix}REDIS_URL")) cannot be
# statically determined and are therefore omitted; they must be documented
# in the schema by the code that generates them.
#
# Usage:
# results = Mastodon::Configuration::EnvScanner.scan # → Hash
# results['REDIS_HOST'] # => ["config/initializers/...", ...]
module EnvScanner
# Directories and individual files to scan, relative to the project root.
SCAN_PATHS = %w[
config
lib/mastodon
app/lib
app/workers/scheduler
].freeze
# Files/directories inside SCAN_PATHS to skip.
EXCLUDE_PATHS = %w[
lib/mastodon/configuration
spec
test
].freeze
# Variables that are deliberately absent from the schema.
#
# Two categories:
# :deprecated old names superseded by a documented replacement
# :internal framework, tooling, or dev-only vars that are not
# meaningful Mastodon configuration knobs
EXCLUDED_VARS = {
# Deprecated aliases document the canonical name instead
'WHITELIST_MODE' => :deprecated, # → LIMITED_FEDERATION_MODE
'EMAIL_DOMAIN_BLACKLIST' => :deprecated, # → EMAIL_DOMAIN_DENYLIST
'EMAIL_DOMAIN_WHITELIST' => :deprecated, # → EMAIL_DOMAIN_ALLOWLIST
# Standard Rails / Rack / system variables
'OTHER_DATABASE_URL' => :internal,
'RACK_ENV' => :internal,
'SECRET_KEY_BASE_DUMMY' => :internal, # used only during asset pre-compilation
'USER' => :internal,
# Third-party gem internals
'PGHERO_STATS_DATABASE_URL' => :internal,
# Development / CI / test variables
'CI' => :internal,
'GITHUB_ACTIONS' => :internal,
'GITHUB_RSPEC' => :internal,
'VAGRANT' => :internal,
'HEROKU' => :internal,
'REMOTE_DEV' => :internal,
'COVERAGE' => :internal,
'TEST_ENV_NUMBER' => :internal,
'VITE_DEV_SERVER_PUBLIC' => :internal,
'DISABLE_FORGERY_REQUEST_PROTECTION' => :internal,
'ANNOTATERB_SKIP_ON_DB_TASKS' => :internal,
'IGNORE_ALREADY_SET_SECRETS' => :internal,
'MIGRATION_IGNORE_INVALID_OTP_SECRET' => :internal,
'RAILS_LOG_TO_STDOUT' => :internal,
}.freeze
# Matches literal ENV key accesses; does NOT match interpolated keys.
# Captures group 1 from ENV['KEY'] / ENV["KEY"],
# or group 2 from ENV.fetch('KEY') / ENV.key?('KEY') / etc.
LITERAL_KEY_PATTERN = /\bENV(?:\[['"]([A-Z][A-Z0-9_]*)["']\]|\.(?:fetch|key\?|include\?|has_key\?)\(\s*['"]([A-Z][A-Z0-9_]*)["'])/
# Returns a Hash of { 'VAR_NAME' => ['relative/path', ...] } for every
# literal ENV key found in SCAN_PATHS, excluding EXCLUDE_PATHS.
# rubocop:disable Metrics/MethodLength
def self.scan(root = nil)
root = resolve_root(root)
results = Hash.new { |h, k| h[k] = [] }
each_file(root) do |abs_path|
rel = abs_path.relative_path_from(root).to_s
File.read(abs_path).scan(LITERAL_KEY_PATTERN) do |bracket_key, method_key|
key = bracket_key || method_key
results[key] << rel unless results[key].include?(rel)
end
end
results
end
# rubocop:enable Metrics/MethodLength
# Returns the subset of scan results whose keys are absent from the
# schema *and* not in EXCLUDED_VARS. These are the undocumented vars
# that the lint task should report.
def self.undocumented(root = nil)
require_relative 'schema'
schema_keys = Schema.generate['properties'].keys.to_set
scan(root).reject { |key, _| schema_keys.include?(key) || EXCLUDED_VARS.key?(key) }
end
# ---------------------------------------------------------------------------
def self.resolve_root(root)
return Pathname.new(root) if root
# Walk up from this file to find the Rails root (the directory that
# contains Gemfile), so this module works without Rails being loaded.
Pathname.new(__dir__).ascend do |dir|
return dir if dir.join('Gemfile').exist?
end
raise 'Cannot determine project root: no Gemfile found in parent directories'
end
private_class_method :resolve_root
def self.each_file(root)
SCAN_PATHS.each do |scan_path|
full = root.join(scan_path)
next unless full.exist?
candidates = full.directory? ? full.glob('**/*.{rb,erb,yml}') : [full]
candidates.each do |path|
next if EXCLUDE_PATHS.any? { |ex| path.to_s.include?(root.join(ex).to_s) }
next unless path.file?
yield path
end
end
end
private_class_method :each_file
end
end
end

View File

@@ -0,0 +1,111 @@
# frozen_string_literal: true
require 'yaml'
require_relative 'env_scanner'
module Mastodon
module Configuration
# Generates a JSON Schema (draft 2020-12) describing every environment
# variable that Mastodon reads at start-up or run-time.
#
# Property metadata lives in annotations.yml next to this file. The
# EnvScanner is the authoritative list of which variables actually exist
# in the codebase; annotations provide the human-readable descriptions,
# types, groups, and constraints on top.
#
# To document a new environment variable:
# 1. Add it to annotations.yml with at minimum `type`, `group`, and
# `description` fields.
# 2. Run `bundle exec rails mastodon:config:schema > mastodon-config.schema.json`
# to regenerate the committed schema file.
#
# Custom JSON Schema extensions used here:
# x-group logical grouping name for UI clustering
# x-secret true when the value must never be displayed or logged
# x-restart-required false when a live reload is sufficient (rare)
# x-status "deprecated" or "removed" (absent means active)
# x-version-history ordered list of {version, change} entries
# x-example-value representative value shown in docs
# x-anchor explicit anchor override (rare; "" to suppress)
# x-hints list of {style, body} Hugo hint shortcodes
# x-extra prose paragraph rendered after the hints
# x-trailing prose paragraph rendered after the example value
# x-show-default when true, emit a "**Default:** `…`" line
# x-suppress-removed-hint when true, render "removed" prose plain (no danger hint)
# x-docs-layout top-level docs structure (frontmatter + section tree)
#
# The `docs_only_variables` key inside the layout holds annotation entries
# for variables that should appear in the rendered Markdown but are not
# part of the live configuration surface (tombstones for removed vars,
# external/Rails-internal vars upstream documents).
module Schema
ANNOTATIONS_FILE = File.join(__dir__, 'annotations.yml')
RESERVED_KEYS = %w(docs docs_only_variables).freeze
# Returns the full JSON Schema as a Ruby Hash.
def self.generate
annotations = YAML.load_file(ANNOTATIONS_FILE, aliases: true)
docs_layout = annotations['docs']
docs_only = annotations['docs_only_variables'] || {}
schema = {
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
'$id' => 'https://joinmastodon.org/schemas/environment-config',
'title' => 'Mastodon environment configuration',
'description' => 'Environment variables recognised by a Mastodon instance.',
'type' => 'object',
'properties' => build_properties(annotations),
}
if docs_layout
layout = docs_layout.dup
unless docs_only.empty?
layout['docs_only_variables'] = docs_only.transform_values do |meta|
annotation_to_property(meta)
end
end
schema['x-docs-layout'] = layout
end
schema
end
# ---------------------------------------------------------------------------
def self.build_properties(annotations)
annotations.each_with_object({}) do |(key, meta), h|
next if RESERVED_KEYS.include?(key)
h[key] = annotation_to_property(meta)
end
end
private_class_method :build_properties
def self.annotation_to_property(meta)
h = {
'type' => meta['type'],
'description' => meta['description'],
'x-group' => meta['group'],
}
h['default'] = meta['default'] if meta.key?('default')
h['x-secret'] = true if meta['secret']
h['x-restart-required'] = false if meta['restart_not_required']
h['enum'] = meta['enum'] if meta['enum']
h['format'] = meta['format'] if meta['format']
h['examples'] = meta['examples'] if meta['examples']
h['minimum'] = meta['minimum'] if meta.key?('minimum')
h['maximum'] = meta['maximum'] if meta.key?('maximum')
h['x-status'] = meta['status'] if meta['status'] && meta['status'] != 'active'
h['x-version-history'] = meta['version_history'] if meta['version_history']
h['x-example-value'] = meta['example_value'] if meta.key?('example_value')
h['x-anchor'] = meta['anchor'] if meta['anchor']
h['x-hints'] = meta['hints'] if meta['hints']
h['x-show-default'] = true if meta['show_default']
h['x-extra'] = meta['extra'] if meta['extra']
h['x-trailing'] = meta['trailing'] if meta['trailing']
h['x-suppress-removed-hint'] = true if meta['suppress_removed_hint']
h
end
private_class_method :annotation_to_property
end
end
end

74
lib/tasks/config.rake Normal file
View File

@@ -0,0 +1,74 @@
# frozen_string_literal: true
require_relative '../mastodon/configuration/schema'
require_relative '../mastodon/configuration/env_scanner'
require_relative '../mastodon/configuration/docs_generator'
namespace :mastodon do
namespace :config do
desc <<~DESC
Print the JSON Schema for Mastodon environment-variable configuration.
The schema describes every environment variable recognised by this
Mastodon instance, including its type, default value, and a human-readable description.
Offers a machine-readable description of the configuration surface.
Usage:
bundle exec rails mastodon:config:schema
bundle exec rails mastodon:config:schema > mastodon-config.schema.json
DESC
task :schema do
require 'json'
puts JSON.pretty_generate(Mastodon::Configuration::Schema.generate)
end
desc <<~DESC
Generate Hugo-flavored Markdown for content/en/admin/config.md from the JSON schema.
Reads `mastodon-config.schema.json` (or the path supplied as an argument)
and emits Markdown to stdout. The docs structure is driven by the
`x-docs-layout` key, which is populated from the `docs:` block in
`lib/mastodon/configuration/annotations.yml`.
Usage:
bundle exec rails mastodon:config:docs
bundle exec rails mastodon:config:docs > /path/to/documentation/content/en/admin/config.md
DESC
task :docs, [:schema_path] do |_t, args|
require 'json'
path = args[:schema_path] || File.expand_path('../../../mastodon-config.schema.json', __dir__)
schema = JSON.parse(File.read(path))
puts Mastodon::Configuration::DocsGenerator.render(schema)
end
desc <<~DESC
Check that every environment variable used in the source code is
documented in the JSON Schema.
The task statically scans #{Mastodon::Configuration::EnvScanner::SCAN_PATHS.join(', ')} for
literal ENV.fetch / ENV[] accesses and reports any key that is absent
from the schema and not listed in EnvScanner::EXCLUDED_VARS.
Exits non-zero if undocumented variables are found.
Usage:
bundle exec rails mastodon:config:lint
DESC
task :lint do
undocumented = Mastodon::Configuration::EnvScanner.undocumented
if undocumented.empty?
puts 'All environment variables are documented in the schema.'
else
warn "#{undocumented.size} environment variable(s) are used in the source code but not documented in the schema:\n"
undocumented.sort.each do |key, files|
warn " #{key}"
files.each { |f| warn " #{f}" }
end
warn "\nTo fix, add entries for these variables to lib/mastodon/configuration/annotations.yml"
warn 'then regenerate: bundle exec rails mastodon:config:schema > mastodon-config.schema.json'
exit 1
end
end
end
end

3138
mastodon-config.schema.json Normal file

File diff suppressed because it is too large Load Diff