From f90bd4c474da684f49dd400fb6f909297f76663b Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 14 Jul 2026 02:18:30 +0200 Subject: [PATCH] Adopt the estate CodeScene coverage pattern in generated repos Bring generated projects into line with the proven estate coverage recipe (the Python leg pathfound in cmd-mox#190) so repos are born with the CodeScene wiring rather than needing a later onboarding PR. Templated ci.yml (pull-request coverage): - Check out with fetch-depth 0 so a future changed-line cs-coverage check can reach the merge base. - Bump the shared generate-coverage and setup-rust references to 927edd45ae77be4251a8a18ca9eb5613a2e32cbd, keeping a single repo-wide pin. - Guard coverage generation to pull requests (ci.yml also triggers on push, and coverage-main.yml now owns the push-to-main run) and enable the coverage ratchet. - Retain cobertura and coverage.xml, which the shared action mandates for Python and mixed projects. - Drop the bespoke CodeScene CLI install and cs-coverage upload steps from the pull-request job; leave a deferred-gate note explaining that the changed-line check awaits per-project CodeScene enablement. New templated coverage-main.yml (push-to-main upload): - Trigger on push to main plus workflow_dispatch, the latter being mandatory because automerge pushes do not fire push-event workflows. - Set up Python and uv explicitly, generate cobertura coverage with the ratchet, and upload to CodeScene through the guarded shared action so token-less repos skip the upload rather than fail. Add a codescene_project_id copier variable (default empty) surfaced in the deferred-gate note, so the pattern degrades gracefully until a generated repo is onboarded to CodeScene. Update the CI and coverage-main workflow contract tests and their synthetic fixtures to match the new pin, pull-request guard, ratchet input, and push-to-main upload wiring. Incidentally regenerate the committed typos.toml from the current shared en-GB-oxendict dictionary. The upstream dictionary had drifted, which left the required test gate latently red on main and would otherwise block this pull request; the regeneration is what `make spelling` produces in CI. --- copier.yml | 8 ++ template/.github/workflows/ci.yml.jinja | 53 +++---- .../.github/workflows/coverage-main.yml.jinja | 72 ++++++++++ tests/helpers/ci_contracts.py | 134 +++++++++++++++++- tests/helpers/tooling_contracts.py | 11 ++ tests/test_helpers.py | 113 ++++++++++++++- tests/test_template.py | 4 + typos.toml | 62 ++++++++ 8 files changed, 429 insertions(+), 28 deletions(-) create mode 100644 template/.github/workflows/coverage-main.yml.jinja diff --git a/copier.yml b/copier.yml index 65a53e7..7f861d9 100644 --- a/copier.yml +++ b/copier.yml @@ -20,3 +20,11 @@ python_version: type: str default: "3.10" help: Minimum supported Python version + +codescene_project_id: + type: str + default: "" + help: >- + CodeScene project id for the coverage gate (leave blank until this repo + is onboarded to CodeScene). When set, the value is surfaced in the CI + deferred-gate note; token-less repos stay green regardless. diff --git a/template/.github/workflows/ci.yml.jinja b/template/.github/workflows/ci.yml.jinja index b071af7..d65d93c 100644 --- a/template/.github/workflows/ci.yml.jinja +++ b/template/.github/workflows/ci.yml.jinja @@ -21,6 +21,9 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: persist-credentials: false + # Full history so a future `cs-coverage check` can reach the + # pull request's merge base (see the deferred gate note below). + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 @@ -32,7 +35,7 @@ jobs: {% if use_rust %} - name: Set up Rust - uses: leynos/shared-actions/.github/actions/setup-rust@455d9ed03477c0026da96c2541ca26569a74acac + uses: leynos/shared-actions/.github/actions/setup-rust@927edd45ae77be4251a8a18ca9eb5613a2e32cbd with: toolchain: stable @@ -82,36 +85,34 @@ jobs: - name: Audit dependencies run: make audit + # Coverage runs on pull requests only. Pushes to main upload their + # own coverage (and advance the ratchet baseline) via + # coverage-main.yml, so generating it here as well would duplicate + # the work and the CodeScene upload for the same commit. - name: Test and Measure Coverage - uses: leynos/shared-actions/.github/actions/generate-coverage@296dc4aa07acf3a7f60a54fb6bccb5672a597479 + if: ${{ "{{" }} github.event_name == 'pull_request' {{ "}}" }} + uses: leynos/shared-actions/.github/actions/generate-coverage@927edd45ae77be4251a8a18ca9eb5613a2e32cbd with: output-path: coverage.xml format: cobertura artefact-name-suffix: {{ package_name | lower | replace("_", "-") }} + with-ratchet: 'true' {% if use_rust %} cargo-manifest: rust_extension/Cargo.toml {% endif %} - - - name: Install CodeScene Coverage CLI - if: env.CS_ACCESS_TOKEN != '' - run: | - curl -fsSL --output install-cs-coverage-tool.sh \ - https://downloads.codescene.io/enterprise/cli/install-cs-coverage-tool.sh - if [ -n "${CODESCENE_CLI_SHA256:-}" ]; then - echo "${CODESCENE_CLI_SHA256} install-cs-coverage-tool.sh" | sha256sum -c - - else - echo "::notice::CODESCENE_CLI_SHA256 is not set; skipping installer checksum verification" - fi - bash install-cs-coverage-tool.sh -y - - - name: Upload coverage to CodeScene - if: env.CS_ACCESS_TOKEN != '' - run: | - if [ ! -f coverage.xml ]; then - echo "coverage.xml not found!" - exit 1 - fi - cs-coverage upload \ - --format "cobertura" \ - --metric "line-coverage" \ - coverage.xml + # The changed-line `cs-coverage check` gate is deliberately not wired + # in yet: CodeScene rejects `cs-coverage check` for any project whose + # coverage-gates configuration is absent ("Lacks the gates + # configuration."), which is a CodeScene-side, per-project setting + # rather than a CI defect. Once this repo is onboarded to CodeScene + {% if codescene_project_id -%} + # (project {{ codescene_project_id }}) and coverage-main.yml has + # uploaded to main at least once, add a `mode: check` step guarded on + # `CS_ACCESS_TOKEN != ''` with + # `project-url: https://api.codescene.io/v2/projects/{{ codescene_project_id }}`. + {%- else -%} + # — set the `codescene_project_id` copier answer — and coverage-main.yml + # has uploaded to main at least once, add a `mode: check` step guarded + # on `CS_ACCESS_TOKEN != ''` with the project's + # `project-url: https://api.codescene.io/v2/projects/`. + {%- endif %} diff --git a/template/.github/workflows/coverage-main.yml.jinja b/template/.github/workflows/coverage-main.yml.jinja new file mode 100644 index 0000000..79971c3 --- /dev/null +++ b/template/.github/workflows/coverage-main.yml.jinja @@ -0,0 +1,72 @@ +name: Coverage (main) + +# CodeScene accepts `cs-coverage upload` only for analysed branches, so +# main-branch coverage is uploaded here on push; pull requests generate +# coverage (and, in a future fast-follow, run the changed-line +# `cs-coverage check` gate) in ci.yml instead. This workflow also +# advances the coverage ratchet baseline: caches saved on main are +# readable by every pull-request run, so the baseline written here is the +# one PR ratchet checks compare against. +# +# `workflow_dispatch` is mandatory, not a convenience: merges performed by +# the automerge workflow's GITHUB_TOKEN do not fire push-event workflows, +# so automerged changes to main only receive coverage via a manual +# dispatch. + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + coverage-upload: + runs-on: ubuntu-latest + permissions: + contents: read + env: + CS_ACCESS_TOKEN: ${{ "{{" }} secrets.CS_ACCESS_TOKEN || '' {{ "}}" }} + CODESCENE_CLI_SHA256: ${{ "{{" }} vars.CODESCENE_CLI_SHA256 || '' {{ "}}" }} + {% if use_rust %} + CARGO_TERM_COLOR: always + BUILD_PROFILE: debug + {% endif %} + steps: + - name: Check out repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.13' + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 + + {% if use_rust %} + - name: Set up Rust + uses: leynos/shared-actions/.github/actions/setup-rust@927edd45ae77be4251a8a18ca9eb5613a2e32cbd + with: + toolchain: stable + + {% endif %} + - name: Generate coverage + uses: leynos/shared-actions/.github/actions/generate-coverage@927edd45ae77be4251a8a18ca9eb5613a2e32cbd + with: + output-path: coverage.xml + format: cobertura + artefact-name-suffix: {{ package_name | lower | replace("_", "-") }} + with-ratchet: 'true' + {% if use_rust %} + cargo-manifest: rust_extension/Cargo.toml + {% endif %} + + - name: Upload coverage data to CodeScene + if: env.CS_ACCESS_TOKEN != '' + uses: leynos/shared-actions/.github/actions/upload-codescene-coverage@927edd45ae77be4251a8a18ca9eb5613a2e32cbd + with: + format: cobertura + path: coverage.xml + access-token: ${{ "{{" }} env.CS_ACCESS_TOKEN {{ "}}" }} + installer-checksum: ${{ "{{" }} env.CODESCENE_CLI_SHA256 {{ "}}" }} diff --git a/tests/helpers/ci_contracts.py b/tests/helpers/ci_contracts.py index 47981f0..7821bec 100644 --- a/tests/helpers/ci_contracts.py +++ b/tests/helpers/ci_contracts.py @@ -71,8 +71,12 @@ def assert_ci_coverage_action_contract( assert ( coverage_step.get("uses") == "leynos/shared-actions/.github/actions/generate-coverage" - "@296dc4aa07acf3a7f60a54fb6bccb5672a597479" + "@927edd45ae77be4251a8a18ca9eb5613a2e32cbd" ), "expected CI to use the pinned shared coverage action" + assert coverage_step.get("if") == "${{ github.event_name == 'pull_request' }}", ( + "expected CI coverage generation to be guarded to pull requests so " + "coverage-main.yml owns the push-to-main upload" + ) coverage_inputs = require_mapping(coverage_step, "with", "coverage step") assert coverage_inputs.get("output-path") == "coverage.xml", ( "expected CI coverage output path to match the act assertion" @@ -80,6 +84,9 @@ def assert_ci_coverage_action_contract( assert coverage_inputs.get("format") == "cobertura", ( "expected CI coverage format to match the CodeScene upload" ) + assert coverage_inputs.get("with-ratchet") == "true", ( + "expected CI coverage to advance the ratchet baseline" + ) assert coverage_inputs.get("artefact-name-suffix") == package_name.replace( "_", "-" ), "expected package-specific coverage artefact name suffix" @@ -93,6 +100,127 @@ def assert_ci_coverage_action_contract( ) +def assert_coverage_main_workflow_contract( + *, coverage_main_workflow: str, package_name: str, use_rust: bool +) -> None: + """Assert the generated push-to-main coverage upload workflow contract. + + Parameters + ---------- + coverage_main_workflow : str + UTF-8 text of the generated ``coverage-main.yml`` workflow. + package_name : str + Generated Python import package name used to derive the coverage + artefact suffix. + use_rust : bool + Whether the rendered variant includes the optional Rust extension. + + Returns + ------- + None + The helper returns after the coverage-main workflow contract matches + expectations. + + Raises + ------ + AssertionError + Raised when the triggers, guarded upload, coverage generation, or Rust + manifest wiring are wrong. + + Examples + -------- + Validate a rendered coverage-main workflow:: + + assert_coverage_main_workflow_contract( + coverage_main_workflow=coverage_main_workflow, + package_name="example_pkg", + use_rust=True, + ) + """ + # ``on:`` is parsed by PyYAML as the boolean key ``True``, so assert the + # triggers against the raw text to stay robust. + assert "branches: [main]" in coverage_main_workflow, ( + "expected coverage-main to upload on push to main" + ) + assert "workflow_dispatch:" in coverage_main_workflow, ( + "expected coverage-main to allow manual dispatch (automerge pushes do " + "not fire push-event workflows)" + ) + parsed = parse_yaml_mapping(coverage_main_workflow, "coverage-main workflow") + jobs = require_mapping(parsed, "jobs", "coverage-main workflow") + upload_job = require_mapping(jobs, "coverage-upload", "coverage-main jobs") + permissions = require_mapping(upload_job, "permissions", "coverage-upload job") + assert permissions.get("contents") == "read", ( + "expected coverage-main upload job to restrict GITHUB_TOKEN to reads" + ) + steps = require_sequence(upload_job, "steps", "coverage-upload job") + + generate_steps = [ + step + for step in steps + if isinstance(step, dict) and step.get("name") == "Generate coverage" + ] + assert len(generate_steps) == 1, "expected one shared coverage generation step" + generate_step = generate_steps[0] + assert ( + generate_step.get("uses") + == "leynos/shared-actions/.github/actions/generate-coverage" + "@927edd45ae77be4251a8a18ca9eb5613a2e32cbd" + ), "expected coverage-main to use the pinned shared coverage action" + generate_inputs = require_mapping(generate_step, "with", "coverage generation step") + assert generate_inputs.get("output-path") == "coverage.xml", ( + "expected coverage-main to write cobertura coverage.xml" + ) + assert generate_inputs.get("format") == "cobertura", ( + "expected coverage-main to generate cobertura coverage for CodeScene" + ) + assert generate_inputs.get("with-ratchet") == "true", ( + "expected coverage-main to advance the authoritative ratchet baseline" + ) + assert generate_inputs.get("artefact-name-suffix") == package_name.replace( + "_", "-" + ), "expected package-specific coverage artefact name suffix" + + upload_steps = [ + step + for step in steps + if isinstance(step, dict) + and step.get("name") == "Upload coverage data to CodeScene" + ] + assert len(upload_steps) == 1, "expected one guarded CodeScene upload step" + upload_step = upload_steps[0] + assert ( + upload_step.get("uses") + == "leynos/shared-actions/.github/actions/upload-codescene-coverage" + "@927edd45ae77be4251a8a18ca9eb5613a2e32cbd" + ), "expected coverage-main to use the pinned shared upload action" + assert upload_step.get("if") == "env.CS_ACCESS_TOKEN != ''", ( + "expected the CodeScene upload to skip when the access token is absent" + ) + upload_inputs = require_mapping(upload_step, "with", "CodeScene upload step") + assert upload_inputs.get("format") == "cobertura", ( + "expected the CodeScene upload to send cobertura coverage" + ) + assert upload_inputs.get("path") == "coverage.xml", ( + "expected the CodeScene upload to read coverage.xml" + ) + + if use_rust: + assert generate_inputs.get("cargo-manifest") == "rust_extension/Cargo.toml", ( + "expected Rust variant coverage-main to pass the extension manifest" + ) + assert "leynos/shared-actions/.github/actions/setup-rust" in ( + coverage_main_workflow + ), "expected Rust variant coverage-main to set up Rust" + else: + assert "cargo-manifest" not in coverage_main_workflow, ( + "expected pure-Python coverage-main to omit Rust coverage inputs" + ) + assert "setup-rust" not in coverage_main_workflow, ( + "expected pure-Python coverage-main to omit Rust setup" + ) + + def _parse_ci_workflow(ci_workflow: str) -> dict[str, Any]: """Parse a generated CI workflow as a YAML mapping.""" return parse_yaml_mapping(ci_workflow, "CI workflow") @@ -144,6 +272,10 @@ def _assert_ci_workflow_contracts( assert "leynos/shared-actions/.github/actions/generate-coverage" in ci_workflow, ( "expected generated CI workflow to use the shared coverage action" ) + assert "cs-coverage upload" not in ci_workflow, ( + "expected the pull-request CI job to defer the CodeScene upload to " + "coverage-main.yml" + ) if use_rust: assert "leynos/shared-actions/.github/actions/setup-rust" in ci_workflow, ( "expected Rust variant CI to set up Rust" diff --git a/tests/helpers/tooling_contracts.py b/tests/helpers/tooling_contracts.py index 19569a9..d56f6f3 100644 --- a/tests/helpers/tooling_contracts.py +++ b/tests/helpers/tooling_contracts.py @@ -21,6 +21,7 @@ _assert_ci_workflow_contracts, _parse_ci_workflow, assert_ci_coverage_action_contract, + assert_coverage_main_workflow_contract, ) from tests.helpers.makefile_contracts import ( _assert_makefile_contracts, @@ -33,6 +34,7 @@ __all__ = [ "assert_ci_coverage_action_contract", "assert_common_make_targets", + "assert_coverage_main_workflow_contract", "assert_generated_tooling_contracts", ] @@ -44,6 +46,7 @@ def assert_generated_tooling_contracts( pyproject: dict[str, Any], makefile: str, ci_workflow: str, + coverage_main_workflow: str, act_validation_workflow: str, release_workflow: str, build_wheels_workflow: str, @@ -65,6 +68,8 @@ def assert_generated_tooling_contracts( UTF-8 text of the generated Makefile. ci_workflow : str UTF-8 text of the generated CI workflow. + coverage_main_workflow : str + UTF-8 text of the generated push-to-main coverage upload workflow. act_validation_workflow : str UTF-8 text of the generated act-validation workflow. release_workflow : str @@ -99,6 +104,7 @@ def assert_generated_tooling_contracts( pyproject=pyproject, makefile=makefile, ci_workflow=ci_workflow, + coverage_main_workflow=coverage_main_workflow, act_validation_workflow=act_validation_workflow, release_workflow=release_workflow, build_wheels_workflow=build_wheels_workflow, @@ -126,6 +132,11 @@ def assert_generated_tooling_contracts( ci_workflow=ci_workflow, use_rust=use_rust, ) + assert_coverage_main_workflow_contract( + coverage_main_workflow=coverage_main_workflow, + package_name=package_name, + use_rust=use_rust, + ) _assert_act_validation_workflow_contracts( act_validation_workflow=act_validation_workflow, use_rust=use_rust, diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 90e3f17..7f07d4e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -26,6 +26,7 @@ from tests.helpers.tooling_contracts import ( assert_ci_coverage_action_contract, assert_common_make_targets, + assert_coverage_main_workflow_contract, ) from tests.utilities import docker_environment @@ -494,6 +495,114 @@ def test_parent_makefile_test_target_uses_requisite_pytest_command() -> None: ) +def _coverage_main_workflow(*, guard: str, rust_setup: str = "") -> str: + """Return a minimal generated coverage-main workflow for contract tests.""" + return f"""\ +name: Coverage (main) +on: + push: + branches: [main] + workflow_dispatch: +jobs: + coverage-upload: + permissions: + contents: read + steps: +{rust_setup}\ + - name: Generate coverage + uses: leynos/shared-actions/.github/actions/generate-coverage@927edd45ae77be4251a8a18ca9eb5613a2e32cbd + with: + output-path: coverage.xml + format: cobertura + artefact-name-suffix: helper-pkg + with-ratchet: 'true' + - name: Upload coverage data to CodeScene + if: {guard} + uses: leynos/shared-actions/.github/actions/upload-codescene-coverage@927edd45ae77be4251a8a18ca9eb5613a2e32cbd + with: + format: cobertura + path: coverage.xml +""" + + +def test_coverage_main_workflow_contract_validates_guarded_upload() -> None: + """Validate the push-to-main coverage upload workflow contract. + + Parameters + ---------- + None + This test does not use pytest fixtures. + + Returns + ------- + None + The test passes when a guarded pure-Python coverage-main workflow is + accepted and an unguarded upload is rejected. + """ + assert_coverage_main_workflow_contract( + coverage_main_workflow=_coverage_main_workflow( + guard="env.CS_ACCESS_TOKEN != ''" + ), + package_name="helper_pkg", + use_rust=False, + ) + + with pytest.raises( + AssertionError, + match="expected the CodeScene upload to skip when the access token is absent", + ): + assert_coverage_main_workflow_contract( + coverage_main_workflow=_coverage_main_workflow(guard="always()"), + package_name="helper_pkg", + use_rust=False, + ) + + +def test_coverage_main_workflow_contract_requires_rust_setup() -> None: + """Validate the Rust variant coverage-main workflow contract. + + Parameters + ---------- + None + This test does not use pytest fixtures. + + Returns + ------- + None + The test passes when a Rust coverage-main workflow that omits Rust + setup and the extension manifest is rejected. + """ + with pytest.raises( + AssertionError, + match="expected Rust variant coverage-main to pass the extension manifest", + ): + assert_coverage_main_workflow_contract( + coverage_main_workflow=_coverage_main_workflow( + guard="env.CS_ACCESS_TOKEN != ''" + ), + package_name="helper_pkg", + use_rust=True, + ) + + rust_setup = ( + " - name: Set up Rust\n" + " uses: leynos/shared-actions/.github/actions/setup-rust" + "@927edd45ae77be4251a8a18ca9eb5613a2e32cbd\n" + ) + rust_manifest = " cargo-manifest: rust_extension/Cargo.toml\n" + workflow = _coverage_main_workflow( + guard="env.CS_ACCESS_TOKEN != ''", rust_setup=rust_setup + ).replace( + " with-ratchet: 'true'\n", + " with-ratchet: 'true'\n" + rust_manifest, + ) + assert_coverage_main_workflow_contract( + coverage_main_workflow=workflow, + package_name="helper_pkg", + use_rust=True, + ) + + def _ci_workflow(*, persist_credentials: str, coverage_inputs: str) -> str: """Return a minimal generated CI workflow for coverage-contract tests.""" return f"""\ @@ -505,9 +614,11 @@ def _ci_workflow(*, persist_credentials: str, coverage_inputs: str) -> str: with: persist-credentials: {persist_credentials} - name: Test and Measure Coverage - uses: leynos/shared-actions/.github/actions/generate-coverage@296dc4aa07acf3a7f60a54fb6bccb5672a597479 + if: ${{{{ github.event_name == 'pull_request' }}}} + uses: leynos/shared-actions/.github/actions/generate-coverage@927edd45ae77be4251a8a18ca9eb5613a2e32cbd with: output-path: coverage.xml format: cobertura + with-ratchet: 'true' {coverage_inputs}\ """ diff --git a/tests/test_template.py b/tests/test_template.py index d8a6940..aa77494 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -421,6 +421,9 @@ def test_generated_tooling_contracts( agents = read_generated_text(project / "AGENTS.md") makefile = read_generated_text(project / "Makefile") ci_workflow = read_generated_text(project / ".github" / "workflows" / "ci.yml") + coverage_main_workflow = read_generated_text( + project / ".github" / "workflows" / "coverage-main.yml" + ) act_validation_workflow = read_generated_text( project / ".github" / "workflows" / "act-validation.yml" ) @@ -443,6 +446,7 @@ def test_generated_tooling_contracts( pyproject=pyproject, makefile=makefile, ci_workflow=ci_workflow, + coverage_main_workflow=coverage_main_workflow, act_validation_workflow=act_validation_workflow, release_workflow=release_workflow, build_wheels_workflow=build_wheels_workflow, diff --git a/typos.toml b/typos.toml index 81e512c..8afb4e5 100644 --- a/typos.toml +++ b/typos.toml @@ -6,6 +6,7 @@ extend-exclude = [ ".git", ".hypothesis", ".pytest_cache", + ".terraform", ".tox", ".typos-oxendict-base.json", ".typos-oxendict-base.toml", @@ -33,10 +34,12 @@ locale = "en-gb" extend-ignore-re = [ "(?s)```.*?```", "Center \\| Microsoft Learn", + "\\brust-analyzer\\b", "`[^`\\n]+`", ] [default.extend-words] +"ASO" = "ASO" "absolutisable" = "absolutizable" "absolutisation" = "absolutization" "absolutisations" = "absolutizations" @@ -595,6 +598,8 @@ extend-ignore-re = [ "desynchronizers" = "desynchronizers" "desynchronizes" = "desynchronizes" "desynchronizing" = "desynchronizing" +"dialog" = "dialog" +"dialogs" = "dialogs" "dockerisable" = "dockerizable" "dockerisation" = "dockerization" "dockerisations" = "dockerizations" @@ -829,6 +834,7 @@ extend-ignore-re = [ "globalizers" = "globalizers" "globalizes" = "globalizes" "globalizing" = "globalizing" +"handwritten" = "handwritten" "harmonisable" = "harmonizable" "harmonisation" = "harmonization" "harmonisations" = "harmonizations" @@ -1009,6 +1015,24 @@ extend-ignore-re = [ "internationalizers" = "internationalizers" "internationalizes" = "internationalizes" "internationalizing" = "internationalizing" +"italicisable" = "italicizable" +"italicisation" = "italicization" +"italicisations" = "italicizations" +"italicise" = "italicize" +"italicised" = "italicized" +"italiciser" = "italicizer" +"italicisers" = "italicizers" +"italicises" = "italicizes" +"italicising" = "italicizing" +"italicizable" = "italicizable" +"italicization" = "italicization" +"italicizations" = "italicizations" +"italicize" = "italicize" +"italicized" = "italicized" +"italicizer" = "italicizer" +"italicizers" = "italicizers" +"italicizes" = "italicizes" +"italicizing" = "italicizing" "itemisable" = "itemizable" "itemisation" = "itemization" "itemisations" = "itemizations" @@ -1497,6 +1521,7 @@ extend-ignore-re = [ "ordinalizing" = "ordinalizing" "organisable" = "organizable" "organisation" = "organization" +"organisational" = "organizational" "organisations" = "organizations" "organise" = "organize" "organised" = "organized" @@ -1506,6 +1531,7 @@ extend-ignore-re = [ "organising" = "organizing" "organizable" = "organizable" "organization" = "organization" +"organizational" = "organizational" "organizations" = "organizations" "organize" = "organize" "organized" = "organized" @@ -1514,6 +1540,24 @@ extend-ignore-re = [ "organizes" = "organizes" "organizing" = "organizing" "oxendict" = "oxendict" +"oxidisable" = "oxidizable" +"oxidisation" = "oxidization" +"oxidisations" = "oxidizations" +"oxidise" = "oxidize" +"oxidised" = "oxidized" +"oxidiser" = "oxidizer" +"oxidisers" = "oxidizers" +"oxidises" = "oxidizes" +"oxidising" = "oxidizing" +"oxidizable" = "oxidizable" +"oxidization" = "oxidization" +"oxidizations" = "oxidizations" +"oxidize" = "oxidize" +"oxidized" = "oxidized" +"oxidizer" = "oxidizer" +"oxidizers" = "oxidizers" +"oxidizes" = "oxidizes" +"oxidizing" = "oxidizing" "palettisable" = "palettizable" "palettisation" = "palettization" "palettisations" = "palettizations" @@ -2414,6 +2458,24 @@ extend-ignore-re = [ "uncategorizers" = "uncategorizers" "uncategorizes" = "uncategorizes" "uncategorizing" = "uncategorizing" +"underutilisable" = "underutilizable" +"underutilisation" = "underutilization" +"underutilisations" = "underutilizations" +"underutilise" = "underutilize" +"underutilised" = "underutilized" +"underutiliser" = "underutilizer" +"underutilisers" = "underutilizers" +"underutilises" = "underutilizes" +"underutilising" = "underutilizing" +"underutilizable" = "underutilizable" +"underutilization" = "underutilization" +"underutilizations" = "underutilizations" +"underutilize" = "underutilize" +"underutilized" = "underutilized" +"underutilizer" = "underutilizer" +"underutilizers" = "underutilizers" +"underutilizes" = "underutilizes" +"underutilizing" = "underutilizing" "uninitialisable" = "uninitializable" "uninitialisation" = "uninitialization" "uninitialisations" = "uninitializations"