Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 190 additions & 14 deletions .editorconfig
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
# https://editorconfig.org

# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
# https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview

# https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md
# https://github.com/dotnet/runtime/blob/main/.editorconfig

# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format
# Verify with: dotnet format style --verify-no-changes --severity=info --verbosity=detailed

# Root config
root = true

# The default is LF rather than the fleet CRLF default.
# The `[*]` default follows the consuming application's native platform.
# Everything here is consumed by Linux.
# - Hugo builds the site in CI.
# - Caddy and OpenSSH read their config on an Ubuntu VPS.
# - The deploy scripts run there too.
# Taking the CRLF default would mean an LF override for nearly every file.
# That is the over-normalization the rule exists to prevent.
# Because LF is the default, the per-file pins the fleet baseline carries are unnecessary.
# Git's own enforcement for execution-sensitive files lives in `.gitattributes`.
# Defaults: LF is the default, and only the CRLF exception below is declared.
# `.gitattributes` mirrors these two defaults as Git's normalization fallback.
# CI verifies the committed bytes against this file.
[*]
charset = utf-8
end_of_line = lf
Expand All@@ -26,19 +29,192 @@ trim_trailing_whitespace = true
[*.sh]
indent_style = tab

# Two trailing spaces are a hard line break in Markdown.
# Trimming them silently rewrites the content.
# Markdown files
# Two trailing spaces are a hard line break in Markdown, so trimming them silently rewrites content.
[*.md]
trim_trailing_whitespace = false

# Xml files
[*.xml]
[*.{xml,csproj,props,targets}]
indent_size = 2

# Yaml files. `hugo.yaml` and the workflows are the bulk of this repo's configuration.
# Yaml files
[*.{yml,yaml}]
indent_size = 2

# Windows batch and command scripts: the one CRLF exception to the `[*]` LF default above.
[*.{bat,cmd}]
end_of_line = crlf

# .NET-only below, covering C# and ReSharper style.
# Everything above is the line-ending governance every derived repo carries, and a non-.NET repo may drop from here down.
# This repo ships no .NET, so the block below is inert and costs nothing.

# C# files
[*.cs]
# Suppressions follow CODESTYLE.md "Analyzer Diagnostics and Suppressions".
# Prefer a [SuppressMessage] attribute, or the owning project's .editorconfig.
# Relax a rule repo-wide here only when it applies to every project, never for a brownfield batch.
dotnet_diagnostic.IDE0055.severity = none
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
csharp_prefer_braces = true
csharp_prefer_simple_default_expression = true
csharp_prefer_simple_using_statement = true
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_prefer_system_threading_lock = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,abstract,sealed,virtual,override,readonly,unsafe,volatile,async,extern,new,partial:warning
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = false
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
csharp_style_conditional_delegate_call = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = true
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = true
csharp_style_expression_bodied_methods = true
csharp_style_expression_bodied_operators = true
csharp_style_expression_bodied_properties = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_namespace_declarations = file_scoped
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_implicitly_typed_lambda_expression = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
csharp_style_prefer_switch_expression = true
csharp_style_prefer_top_level_statements = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
csharp_using_directive_placement = outside_namespace
dotnet_code_quality_unused_parameters = all
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
dotnet_naming_style.static_prefix_style.capitalization = camel_case
dotnet_naming_style.static_prefix_style.required_prefix = s_
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
Comment thread
coderabbitai[bot] marked this conversation as resolved.
dotnet_prefer_system_hash_code = true
dotnet_property_generation_behavior = prefer_throwing_properties
dotnet_remove_unnecessary_suppression_exclusions = none
dotnet_search_reference_assemblies = true
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
dotnet_style_readonly_field = true
dotnet_style_require_accessibility_modifiers = for_non_interface_members

# ReSharper settings
resharper_csharp_trailing_comma_in_multiline_lists = true
resharper_csharp_var_for_built_in_types = false
resharper_csharp_var_when_type_is_apparent = false
resharper_csharp_var_when_type_is_not_apparent = false

# Repo-specific below: this repo's own additions layered on the fleet baseline above.

# JSON, including the parity-gate fixtures and `version.json`
[*.json]
indent_size = 2
Expand Down
5 changes: 5 additions & 0 deletions .editorconfig-checker.json
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
{
"Exclude": [
"(^|/)__pycache__/",
"(^|/)\\.mypy_cache/",
"(^|/)\\.pytest_cache/",
"(^|/)\\.ruff_cache/",
"(^|/)\\.venv/",
"^static/media/",
"^static/external/",
"^themes/",
Expand Down
61 changes: 10 additions & 51 deletions .gitattributes
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,17 @@
# Git stays passive on line endings by default.
# The .editorconfig end_of_line rules guide what the editor writes.
# The pins below are git's own enforcement for execution-sensitive files.
# - git config --global core.autocrlf false
# - git add --renormalize .
# - git ls-files --eol
* -text
# Normalize every detected text file to LF in the index and on checkout.
# `text=auto` leaves binary files byte-preserved.
* text=auto eol=lf

# This repo declares LF, so .editorconfig already defaults every file to LF.
# That does not make the pins below redundant.
# `* -text` keeps git passive, so .editorconfig binds only the editor.
# An explicit `text eol=lf` is what binds git on checkout and on renormalize.
# Execution-sensitive files therefore get both.
# Windows command scripts require CRLF.
*.bat text eol=crlf
*.cmd text eol=crlf

# A CRLF shebang breaks execution, so scripts stay LF regardless of the default.
*.sh text eol=lf

# Shebang-executable files carrying no extension, which every rule above matches by
# extension and therefore misses. `ops/vps-backup-pull` is run by systemd on the backup
# host. A named line per file, as with the Python rules below, and `check-eol-pins.py`
# fails if a tracked shebang file ever lands without one.
ops/vps-backup-pull text eol=lf

# Shebang-executable Python, where a CRLF on line one is a broken interpreter line rather
# than a cosmetic difference. `capture/` is pinned whole, because everything there is a
# script; under `checks/` the two executables are named, because that directory also holds
# lists and fixtures. Neither is a blanket `*.py` rule, so a future non-executable module
# elsewhere is not swept in by accident, and a new executable needs a line here.
capture/*.py text eol=lf
checks/check-url-parity.py text eol=lf
checks/check-env-docs.py text eol=lf
checks/check-eol-pins.py text eol=lf

# Caddy config is parsed line by line by a daemon rather than by a shell, and a CRLF file
# is rejected or silently mis-parsed. Both files are named: the bundle's `Caddyfile`, and
# the bootstrap that is installed into the container's config directory and is the only
# Caddy file outside the release.
#
# The restricted `authorized_keys` and the forced-command deploy shell were pinned here
# too, and this repository has never carried either: they live on the server, described in
# OPERATIONS.md "Server Hardening". A pin binds nothing for a file that does not exist,
# and the comment claiming to cover "the extensionless shebang script" is what let the
# real one, `ops/vps-backup-pull`, sit unpinned above. `check-eol-pins.py` now fails on a
# pattern matching no tracked file, so neither can come back silently.
deploy/Caddyfile text eol=lf
deploy/bootstrap.Caddyfile text eol=lf

# Caddy map files are tabular data read by `map` directives.
# They stay LF for the same reason as the Caddyfile.
deploy/maps/*.map text eol=lf
# Repo-specific below: this repo's own additions layered on the fleet baseline above.

# Media is 566 MB of images, video, and archives across two trees.
# `* -text` already stops git normalizing them.
# `binary` additionally suppresses textual diff and merge.
# That keeps a `git log -p` or a bad merge from rendering a 4 MB PNG as text.
# `binary` suppresses normalization plus textual diff and merge.
# A `git log -p` or a bad merge never renders a 4 MB PNG as text.
# The text=auto heuristic itself is never consulted for these paths regardless of what it would guess.
# Extensions present are png, jpg, gif, webp, mov, zip, and ico, with nothing textual.
static/media/** binary
static/external/** binary
Expand Down
59 changes: 59 additions & 0 deletions .github/actions/validate/action.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
# Blog's own validate hook, run by the hub's validate-task.yml validate job.
# The fleet doc-lint block and the generic unit-test job in validate-task.yml do not cover this repo's own domain checks.
name: Validate repository (Blog)
description: Config/env-docs checks and the Hugo build/URL-parity gate.

runs:
using: composite
steps:

- name: Validate config step
shell: bash
run: |
set -Eeuo pipefail
for f in spec/*.json version.json .editorconfig-checker.json; do
jq empty "$f"
done
python3 -c 'import yaml,sys; yaml.safe_load(open("hugo.yaml"))'

# Every configuration value is described once, in ENVIRONMENT.md.
# A new value gets added wherever its author is working, and nothing else notices a missing row.
# This check runs both directions: undocumented values, and rows describing nothing.
- name: Check environment docs step
shell: bash
run: python3 checks/check-env-docs.py

# The hub's validate job checkout is shallow.
# The mtime restore below needs full history to date each file from its last commit.
# This hook deepens the checkout before the build.
- name: Deepen checkout step
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0

# The pin lives in the action, so validation and the deploy cannot install different generators.
- name: Install Hugo step
uses: ./.github/actions/install-hugo

# The same action the deploy uses, so the two agree on who restores the mtimes.
- name: Restore file mtimes step
uses: chetan/git-restore-mtime-action@d186aca54f8760da4dec55313195e51ed3ebb0b3 # v2.3
with:
args: static

# This is the build and URL-contract gate, not an extra step beside one.
# The script runs `hugo --panicOnWarning` and check-url-parity itself, so a separate build would repeat both.
# --panicOnWarning is the real gate, and two PaperMod templates are overridden in layouts/ so it can stay on.
# Every local asset reference must resolve, and every legacy image URL from the WordPress import must too.
#
# Running the script here is also the point: it was linted and never run, so a broken caller contract reached a deploy.
# An empty scratch root exercises the deploy's path short of the transport, with no previous release for the hard-link guard.
# REQUIRE_BROTLI stays unset, since installing brotli would cost an apt round trip per pull request.
- name: Assemble release bundle step
shell: bash
env:
MTIME_RESTORED: '1'
run: |
set -Eeuo pipefail
deploy/make-release.sh "${RUNNER_TEMP}/validate-bundle" "$(date -u +%Y%m%d-%H%M%S)"
Loading