Skip to content

2.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 16 Aug 22:02
· 32 commits to main since this release
cf4703b

[2.0.0] - 2026-08-16

Removed

  • Five unused helm variables: helm_target_groups, helm_target_host,
    helm_kubectl_context, helm_validation_timeout and helm_log_level are
    gone from roles/helm/defaults/main.yml and
    roles/helm/meta/argument_specs.yml. No task file in the role ever read
    them, so setting them changed nothing and produced no warning.
    helm_target_groups and helm_target_host were especially misleading: they
    suggested the role filters its own target hosts, which it does not — host
    selection belongs in the play (hosts:). Validation behaviour is unaffected:
    helm_validation_retries and helm_validation_delay are read by
    roles/helm/tasks/charts.yml, helm_validation_enabled by
    roles/helm/tasks/prerequisites.yml, and together they cover the validation
    flow.

Added

  • apt_version_pin filter plugin: resolves a version prefix to the full
    version string apt reports, replacing four hand-rolled Jinja regex copies in
    the docker and docker_compose_v2 roles. Documented in
    plugins/filter/DOCUMENTATION.yml and covered by
    tests/unit/plugins/filter/test_apt_filters.py.

  • Configurable fleet bundle readiness wait: fleet_bundle_wait (true)
    and fleet_bundle_wait_timeout (300) in roles/fleet/defaults/main.yml,
    both overridable per bundle via wait / wait_timeout on the item and
    declared in meta/argument_specs.yml. The "Manage Fleet Bundles" task
    previously applied each bundle with a hardcoded wait for Ready, so a serial
    loop over many bundles stacked up the 300s timeout per item. Stable bundles
    can now be applied fire-and-forget and let Fleet reconcile them
    asynchronously; the registered result shape is unchanged.

  • k3s container log rotation defaults: k3s_container_log_max_size
    (10Mi) and k3s_container_log_max_files (5) now have defaults in
    defaults/main.yml and entries in meta/argument_specs.yml. Both were
    previously only read by an is defined guard in the agent config template,
    so out of the box kubelet never received container-log-max-size /
    container-log-max-files and container logs grew unbounded. The guards stay
    in place, so setting k3s_container_log_max_size to "" or
    k3s_container_log_max_files to 0 still omits the respective flag.

  • k3s hardening variables: eight new variables defined in both
    defaults/main.yml and meta/argument_specs.yml
    k3s_secrets_encryption, k3s_protect_kernel_defaults,
    k3s_audit_log_enabled, k3s_audit_log_maxage, k3s_anonymous_auth,
    k3s_node_restriction, k3s_kubelet_read_only_port_disabled,
    k3s_audit_log_level and k3s_pod_security_admission_profile. All are gated
    by k3s_security_hardening, which previously only covered SELinux/AppArmor.

  • k3s audit policy: renders audit-policy.yaml and passes
    audit-policy-file to the kube-apiserver when audit logging is enabled.
    Without a policy file the apiserver matches no events and the audit log stays
    empty. Drops high-volume node/proxy and health-endpoint traffic, caps secrets
    and configmaps at Metadata, and logs everything else at
    k3s_audit_log_level.

  • k3s Pod Security Admission: optional cluster-wide default profile via
    k3s_pod_security_admission_profile (empty by default, so no behaviour
    change). Renders psa-config.yaml with kube-system exempt and wires it as
    admission-control-config-file.

  • k3s molecule hardening assertions: verify.yml checks the rendered
    config for the hardening flags, k3s secrets-encrypt status for Enabled,
    the credential directory for 0700, and that a user-supplied
    kube-apiserver-arg survives the hardening merge.

  • Molecule coverage for the five previously-untested roles, each with a
    default scenario under roles/<role>/molecule/default/ and a matching
    molecule-<role> job wired into pull-request.yml (qemu/KVM driver,
    Ubuntu 22.04 cloud image, pinned to ci-ansible-molecule.yml@2026-06-18).

  • docker_login molecule — full converge: a prepare.yml installs Docker via
    the docker role and starts a throwaway local registry:2 container,
    converge logs into localhost:5000, and verify asserts the registry appears
    under auths in the Docker client config.

  • docker_compose_v2 molecule — full converge: prepare.yml installs Docker,
    converge deploys a minimal nginx compose project, and verify asserts the
    compose plugin, the rendered project file, the systemd unit and a running
    container.

  • helm, fleet, tailscale molecule — full converge against an
    ephemeral k3s cluster. Each scenario gained a prepare.yml that installs
    arillso.container.k3s in the same QEMU VM and adds whatever upstream
    controller the role needs, so converge, idempotence and verify are now
    part of the test_sequence. No external cluster and no repository secret is
    involved, which keeps the jobs working on fork pull requests. These scenarios
    assert that the roles emit schema-valid resources the API accepts; they do not
    assert that the upstream controllers act on them.

  • helm molecule exercises the whole deployment chain: the role applies a
    HelmChart CR, the k3s Helm controller turns it into an install Job, and verify
    asserts the CR, the completed Job and the running Pod.

  • fleet molecule installs standalone Fleet (fleet-crd, then fleet) and
    asserts that the GitRepo and Bundle CRs the role emits are accepted by the
    fleet.cattle.io API with the expected spec and labels. Workspaces
    (Rancher-only management.cattle.io/v3), registration tokens (the controller
    rotates the secret) and clusters (need a second cluster's kubeconfig) stay out
    of the fixture.

  • tailscale molecule installs the operator chart with dummy OAuth
    credentials and without waiting for the Pod, which registers the
    tailscale.com CRDs without a real tailnet, then asserts the ProxyGroup the
    role creates.

Fixed

  • docker version pin never matched on the Debian family: apt matches an =
    pin against the full version string and does not expand globs in it, so the
    shipped docker_version default (docker-ce=28.5.2) was unusable on Debian
    and Ubuntu — apt reports the version with an epoch and a distro release
    suffix. install_docker_debian.yml and install_docker_ubuntu.yml now
    resolve the full version string on the host before pinning. Every existing
    docker scenario blanked docker_version, so the shipped default had never
    been exercised; a new pinned molecule scenario runs the role without that
    override and asserts the installed server version equals the pin.

  • docker_compose_v2 pin was built in one form for three package managers:
    apt needed the full version string it never got, and dnf does not know the
    pkg=version syntax at all, so every pinned run failed on a role that
    advertises Debian, Ubuntu and EL 9. The pin form now lives in
    per-distribution vars/ files, and the apt string is resolved on the host
    through the new apt_version_pin filter.

  • helm applied charts to only one host per play: charts.yml,
    prerequisites.yml and repositories.yml carried run_once, but each host
    the role targets is its own k3s cluster, so the HelmChart CRs reached a single
    host and left the other clusters empty. Removing run_once also unmasks the
    kubeconfig autodetection, which promised the first existing candidate but let
    the loop overwrite it down to the last; it now selects the first match in one
    pass.

  • helm_kubeconfig_path was undocumented and unvalidated: the role read it
    in 13 task lines, including as kubeconfig for every kubernetes.core call,
    but declared it in neither defaults/main.yml nor meta/argument_specs.yml.
    Both now declare it. The default stays empty, since prerequisites.yml only
    autodetects the path while the value is unset or empty.

  • lefthook could not lint without Galaxy access: the ansible-lint hook
    resolved the collections declared in the molecule requirements against
    galaxy.ansible.com, so a machine without Galaxy egress could not commit any
    file under the hook's glob and had to reach for --no-verify, which drops the
    gitleaks hook too. The hook now lints offline.

  • Renovate could not refresh the pinned nginx digest: the image: lines in
    roles/docker_compose_v2/molecule/default/converge.yml and verify.yml
    carried a # renovate: datasource=docker depName=nginx comment, which the
    comment manager from the shared renovate-base preset read as
    currentValue: nginx:alpine. The resulting request for the
    library/nginx:nginx:alpine manifest returned 400 DIGEST_INVALID and every
    Renovate run reported "Could not determine new digest for update" as a
    repository problem, leaving the digest frozen. The comments are gone and
    .github/renovate.json gained a customManager that parses the image:
    line itself, so depName is nginx, currentValue is alpine and the
    digest updates again. The match requires an @sha256: suffix, which keeps
    tag-only test images such as registry:2 in docker_login untracked, while
    the tag itself is optional so a digest-only pin (image: nginx@sha256:…) is
    still tracked rather than silently frozen. Excluding / from the tag keeps a
    port-qualified registry (registry.example.com:5000/nginx) inside depName
    instead of splitting it at the port.

  • k3s AppArmor profile survived being disabled: setting
    k3s_apparmor_profile to false only stopped
    roles/k3s/tasks/security.yml from writing /etc/apparmor.d/k3s; a copy
    left from an earlier run stayed on disk and was reloaded by
    apparmor.service on every boot, so turning the variable off never took
    effect on hosts that once had it on. The role now mirrors the cri-containerd
    disable path — it unloads the profile with apparmor_parser -R, removes the
    file and clears a hand-placed /etc/apparmor.d/disable/k3s symlink. With the
    profile still loaded, container processes ran under stacked child profiles,
    so kubectl exec failed with an AppArmor operation not permitted error and
    pods hung in Terminating with FailedKillPod. The load task also read
    k3s_apparmor_profile | default(true) where the deploy task read
    default(false); both now use default(false).

  • k3s server nodes ignored container log rotation: the
    container-log-max-size and container-log-max-files kubelet args were only
    built by agent-config.yaml.j2, so a server node never rotated container
    logs even with the variables set. server-config.yaml.j2 now builds the same
    two args, and molecule/default/verify.yml asserts both reach the rendered
    config.

  • k3s server config template used the pre-migration fact spelling: the
    ansible_facts migration left
    templates/etc/rancher/k3s/server-config.yaml.j2 on ansible_local.k3s,
    while the sister template agent-config.yaml.j2 in the same role had already
    moved to ansible_facts['ansible_local']['k3s']. Both templates read the same
    fact in two different spellings. No behaviour change: ansible_local is
    exempt from the INJECT_FACTS_AS_VARS deprecation and is always promoted to
    the top level, so the old spelling still resolved. A sweep for
    ansible_local. across roles/ confirms this was the last remaining
    occurrence.

  • k3s environment variables were never rendered: k3s_environment_vars was
    fully wired — documented in defaults/main.yml, declared as a dict in
    meta/argument_specs.yml, consumed by
    templates/etc/systemd/system/k3s.service.env.j2 and loaded by the unit via
    EnvironmentFile= — but the task that renders the file was commented out in
    roles/k3s/tasks/utilities.yml. Setting HTTP_PROXY/HTTPS_PROXY, the
    documented use case, produced no error and no file: the leading - in
    EnvironmentFile=- makes systemd treat the missing file as fine, so k3s ran
    without proxy settings and failed image pulls silently. The task is active
    again and notifies Restart k3s instead of the stale lowercase handler name
    restart k3s, which no longer exists. Because utilities.yml is included
    after the role has already flushed its handlers and waited for the API server,
    the task flushes handlers itself and waits for the server to come back, so a
    following play never meets a restarting cluster. verify.yml asserts the file
    is rendered 0600 and carries the value set through k3s_environment_vars,
    matching it with grep instead of reading the file into a variable.

  • k3s environment file broke idempotence: the environment template rendered
    K3S_TOKEN whenever k3s_token was set. On a fresh node the variable is
    empty during the first run and only read back once the datastore exists, so
    the line appeared on the second run and rewrote the file every time. The
    template no longer renders the token; server-config.yaml.j2 already writes
    it into config.yaml, which the unit loads through K3S_CONFIG_FILE, so the
    join credential now lives in one file instead of two. verify.yml asserts the
    environment file carries no K3S_TOKEN= line.

  • fleet GitRepo and Bundle sent empty values for unset options: both tasks
    sent every optional field with an empty default ('', [], false) instead
    of omitting it, and patched instead of applying. The API drops empty values
    and writes its own defaults (such as spec.targetCustomizationMode), so the
    applied object never matched the stored one. Optional fields are now omitted
    when unset and both tasks use apply: true, which compares against the
    fields the role owns. This narrows the diff but does not make the applies
    idempotent yet — a second run still bumps the resource's generation, so the
    fleet molecule scenario runs without the idempotence step for now.

  • fleet Bundle sent fields the API rejects: spec.helm.timeout,
    spec.helm.timeoutForceDelete and spec.yodaMode are not part of the Fleet
    Bundle schema and were discarded with an unknown field warning on every
    apply. All three are removed from the task and from argument_specs.yml.

  • k3s secrets encryption was silently off: server-config.yaml.j2
    referenced k3s_secrets_encryption, k3s_protect_kernel_defaults and
    k3s_audit_log_enabled, but none of them were defined in defaults/main.yml
    or argument_specs.yml. Every default deployment therefore ran without
    secrets encryption while k3s_security_hardening: true suggested otherwise.

  • k3s dead permission tasks: the kubeconfig, token-file and credential
    directory permission tasks were commented out, making k3s_kubeconfig_mode,
    k3s_kubeconfig_owner and k3s_kubeconfig_group dead configuration. All
    three are restored with a stat pre-check, since security.yml runs before
    the installation. The orphaned stat on the credential directory has its
    consumer back.

  • k3s bash completion never ran: both completion tasks in
    roles/k3s/tasks/utilities.yml were gated on k3s_completion_test, a
    variable that was never registered anywhere in the collection, so the
    condition was always false and the tasks were silently skipped on every run
    despite k3s_create_bash_completion defaulting to true. The completion
    output is now generated by a dedicated probe task per binary and written with
    ansible.builtin.copy, which replaces the changed_when: true shell
    redirects and reports changed only when the file content actually differs.
    verify.yml asserts that both completion files exist and are non-empty.

  • k3s dead security blocks removed: the commented-out audit-policy task
    referenced templates/etc/kubernetes/audit-policy.yaml.j2, which does not
    exist in the repository, and the commented-out debug summary duplicated the
    active debug task in the same file.

  • k3s cluster-init drift on re-runs: the first server rewrote
    config.yaml and restarted k3s on every run after the first. cluster-init
    was derived from k3s_server_init, which follows should_init and requires
    the datastore to be absent — so the flag was written on the initial run and
    dropped on every later one. cluster-init is a bootstrap flag that k3s only
    evaluates while creating the datastore, so it is now kept for as long as this
    node runs the cluster. Secondary servers and agents keep writing server: as
    before.

  • fleet async applies could never run: bundles.yml and clusters.yml
    fired the Bundle and Cluster applies with async/poll: 0, but
    kubernetes.core.k8s ships an action plugin wrapper that never opts into
    async, so ActionBase.run() rejects the task before the module executes
    ("This action (kubernetes.core.k8s) does not support async"). Only
    check_mode ever reached the synchronous fallback, so a real run failed
    outright. Both resources now apply synchronously; the fleet_async_enabled,
    fleet_async_timeout, fleet_async_retries and fleet_async_delay
    variables are gone with the path they configured.

  • fleet sent keepFailHistory as an integer: the Bundle spec rendered
    spec.correctDrift.keepFailHistory through | int and the argument spec
    declared it int ("Number of failed attempts to keep"), but the Fleet CRD
    types it as a boolean, so the API rejected every Bundle carrying a
    correct_drift block with a 422. Both now use bool.

  • fleet applied Bundles and one namespace without a kubeconfig: the Bundle
    apply and the ClusterRegistrationToken namespace task omitted the
    kubeconfig parameter every other task in the role passes, so they fell back
    to the module default and failed with "Could not create API client: Invalid
    kube-config file" wherever the k3s kubeconfig is not the ambient default.

  • fleet Bundle targets ignored the documented spelling: bundles.yml
    passed targets to the API verbatim while the argument spec documents (and
    validates) snake_case keys, so a spec-conformant cluster_selector reached
    the API unconverted and a working clusterSelector failed validation.
    Bundles now run targets through fleet_transform_targets, the same filter
    gitrepos.yml already used.

  • helm chart deploys are excluded from the idempotence check: the k3s Helm
    controller owns the HelmChart it reconciles and writes back to the spec, so
    the applied resource never matches and the task reports changed on every
    run. The molecule scenario skips it via molecule-idempotence-notest rather
    than masking the result with changed_when: false.

  • helm role failed on a host with an empty apt cache: the packages entry
    point of arillso.system.packages never refreshes the package cache and its
    install task pins update_cache: false, so python3-kubernetes was reported
    as unavailable on a freshly provisioned host. The role now refreshes the apt
    cache before installing its Python dependency.

  • k3s config drift on re-runs: a server rewrote config.yaml and restarted
    k3s on every run after the first. On the initial run the cluster database does
    not exist yet, so the role initialises the cluster and k3s_token stays empty
    — the rendered config carries no token. From the second run on, the token
    file exists, the role reads it back and writes it into the config, which
    changes the file and notifies Restart k3s. A token read from the node's own
    server/node-token is redundant in config.yaml and is now left out of the
    server config. A token fetched from another host stays in place, so secondary
    servers and agents can still join.

  • docker_compose_v2 molecule idempotence: the default scenario failed on
    the launch task with Idempotence test failed. nginx:alpine is an OCI image
    index with 16 manifests, and compose stores the resolved image ID in the
    container's com.docker.compose.image label. Once the tag resolved to a
    different ID than the stored one, compose recreated the container on every
    run — the config hash still matched on both sides, so only the image label
    differed. The converge image is now pinned by digest.

  • Secret masking: tasks that read, render or transport credentials ran
    without no_log: true, so the values were printed with -v or in the task
    result. The k3s cluster join token (slurp + set_fact in
    roles/k3s/tasks/main.yml), the k3s config and registries templates, the
    Fleet GitRepo auth secret and the Fleet ClusterRegistrationToken lookup,
    and the docker_login registry login are now all masked. The k3s_token
    and docker_login_password options in argument_specs.yml carry no_log
    as well, matching the already-masked k3s_etcd_s3_* keys.

  • fleet argument_specs: the main entry point used six separate <<: merge
    keys in one mapping, which ruamel.yaml (used by ansible-lint) rejects as
    duplicate keys. Consolidated into a single YAML 1.1 list-form merge
    (<<: [*a, *b, ...]); the merged result is byte-for-key identical.

  • omit defaults: docker_login and fleet declared optional parameters
    with a string default: "{{ omit }}", which fails argument-spec validation on
    Ansible 2.18+ for non-string types. Removed the string-omit defaults; the
    docker_login tasks now resolve those values with | default(omit).

  • docker role: the Docker prune service/timer setup called
    arillso.system.systemd_unit, a role that no longer exists in
    arillso.system (replaced by the systemd role with a systemd_units
    list interface), so the role failed with "role
    'arillso.system.systemd_unit' was not found". Rewrite the prune units in
    the systemd_units format and call arillso.system.systemd.

  • docker role: add the missing log-opts option to the docker_daemon
    argument spec. log-opts is a valid daemon.json key but was absent from
    the spec, so passing it (e.g. log rotation max-size/max-file) failed
    argument validation.

  • docker role — hardening and merge documentation: new ## Hardening
    section in the role README covering the no-new-privileges opt-out and the
    userns-remap opt-in with its costs, plus a Quick Start that explains the
    merge semantics. The log-rotation troubleshooting entry now states that
    max-size/max-file are json-file options and are ignored under the
    default journald driver, which limits size via SystemMaxUse instead.

  • docker role — merge coverage in tests: the molecule verify.yml asserts
    that role defaults survive a user-provided docker_daemon and that nested
    log-opts merge per sub-key; the integration target asserts the pure
    default path, which no test previously covered.

Changed

  • Role-internal registers now carry a role prefix: every register in
    roles/helm and roles/k3s was renamed to _<role>_<name>, so
    role-internal state no longer leaks into the play namespace. This covers
    both the 12 registers that carried no role prefix at all (cluster_token,
    kubeconfig_check, facts_check_k3s_cluster_token,
    _helm_kubeconfig_check, _k3s_facts_check) and the 16 that already had
    one but no leading underscore (helm_repo_check, k3s_release_checksum,
    k3s_selinux_restorecon_helm_repo_check, _k3s_release_checksum,
    _k3s_selinux_restorecon). The second group was already lint-clean;
    renaming it keeps a single style inside the two roles. The molecule verify
    playbooks for docker and k3s use the plain <role>_<name> form, since
    play-level variables are not role-internal. k3s_token, k3s_token_file
    and k3s_token_source are the role interface and are unchanged.
  • ansible-lint runs the production profile: .ansible-lint gains
    profile: production and strict: true, and drops
    var-naming[no-role-prefix] from skip_list, which turns the prefix
    convention into an enforced rule. package-latest stays skipped.
  • Unified the declared Ansible floor at 2.18: the collection declared its
    minimum Ansible version in nine places with three different values. Galaxy
    already enforces requires_ansible: ">=2.18.0" from meta/runtime.yml at
    install time, while README.md promised >= 2.15 and the helm/fleet
    roles claimed 2.9. README.md now states >= 2.18 and all seven role
    min_ansible_version values are "2.18". For README readers this is a
    documented tightening, even though Galaxy has been enforcing it all along.
    2.9 was unsupportable regardless, since the community.docker dependency
    declared in galaxy.yml requires >=3.4.11, which does not run on it.
  • BREAKING — the fleet async variables are gone: fleet_async_enabled,
    fleet_async_timeout, fleet_async_retries and fleet_async_delay are
    removed. They configured an async apply path that Ansible refuses to run for
    kubernetes.core.k8s, so setting them never had the documented effect.
    Playbooks passing them need to drop the variables; Bundle and Cluster applies
    now always run synchronously.
  • BREAKING — k3s hardening now applies by default: k3s_secrets_encryption,
    k3s_anonymous_auth, k3s_node_restriction and
    k3s_kubelet_read_only_port_disabled take effect on existing clusters running
    default variables. Secrets encryption restarts K3s (no data loss);
    anonymous-auth=false breaks anonymous health probes; NodeRestriction
    breaks node credentials modifying foreign node objects; read-only-port=0
    breaks collectors on kubelet port 10255. Set k3s_security_hardening: false
    to keep the previous behaviour.
  • k3s apiserver and kubelet args merge instead of overwrite:
    k3s_kube_apiserver_args and k3s_kubelet_args previously replaced the
    argument list wholesale, leaving no room for role-managed hardening flags.
    They are now appended after the hardening args, so a user-supplied duplicate
    still wins (K3s keeps the last occurrence of a repeated flag).
  • docker role — daemon config is now merged, not replaced (BREAKING).
    /etc/docker/daemon.json is rendered from the new docker_daemon_base
    (role defaults) recursively merged with docker_daemon (user config,
    now defaulting to {}). Previously the template rendered docker_daemon
    alone, so any playbook setting it silently dropped the role defaults
    log-driver: journald and live-restore: true. Existing playbooks keep
    working and additionally inherit the defaults; to drop a base key instead
    of overriding it, replace docker_daemon_base. The per-key default:
    entries for log-driver and live-restore were removed from the
    docker_daemon argument spec, since argument validation would otherwise
    inject them into the user dict and defeat the merge.
  • docker role — no-new-privileges: true is now a default (BREAKING).
    Set in docker_daemon_base, so new containers can no longer gain
    privileges via setuid/setgid binaries. Images relying on them (e.g. sudo,
    some ping builds) will break. Opt out with
    docker_daemon: {no-new-privileges: false}. userns-remap deliberately
    stays opt-in; see the role README for its costs.
  • Deprecated syntax: replace top-level ansible_* fact references with
    ansible_facts['...'] across the docker, k3s and helm roles (and their
    defaults/argument_specs), and migrate the docker Debian/Ubuntu
    repository tasks from the deprecated ansible.builtin.apt_repository to
    ansible.builtin.deb822_repository. Removes the INJECT_FACTS_AS_VARS
    (core 2.24) and apt_repository (core 2.25) deprecation warnings. The
    docker role now installs python3-debian (required by
    deb822_repository) and removes any legacy docker.list left by earlier
    role versions to avoid a duplicate apt source. A follow-up pass covers the
    references the first migration missed: the hostvars[...]['ansible_*']
    lookups in install_docker_redhat.yml and the k3s server-URL fallbacks,
    ansible_local.k3sansible_facts['ansible_local']['k3s'] on the k3s
    cluster-init path and in the agent config template, plus
    ansible_swaptotal_mb and the molecule/integration references. Custom facts
    keep their ansible_local key inside ansible_facts, unlike the built-in
    facts that drop the ansible_ prefix there.
  • Renovate: track the python_version CI input via Renovate. Each
    python_version: in pull-request.yml/merge.yml carries a
    # renovate: datasource=github-releases depName=python/cpython marker, and
    the local regex manager now scans .github/workflows/*.yml, so the Python
    version is bumped automatically instead of drifting hardcoded.
  • Dependency: raise the arillso.system lower bound from >=0.0.17 to
    >=1.0.0 — the systemd role with the systemd_units interface the docker
    role now uses is only available from 1.0.0 onwards.
  • Molecule (CI): run the docker and k3s scenarios on both Ubuntu 22.04 and
    Debian 12 (was Ubuntu-only), restoring the two-distro coverage from the
    pre-KVM docker-driver setup.
  • Align the release workflow with the org convention: set
    name: Release - Ansible Collection, simplify run-name to
    Release <ref>, use a release-<ref> concurrency group, rename the job to
    release, and pin the reusable workflow to @2026-06-18.
  • The python_version CI input is pinned to 3.13 in pull-request.yml and
    merge.yml, because ansible-test sanity rejects 3.14. The local
    .python-version tracks the newest CPython release (3.14.7) and is not
    used by the test matrix.
  • Security scan runs as nightly-security.yml (name: Nightly Security Scan)
    on a daily cron (0 2 * * *). Per the repo-standard visibility rule, public
    repos run the scan daily (free Actions minutes); private repos run it weekly.
  • LICENSE copyright 20252023-2026 (org range FIRST-CURRENT,
    consistent with the README).
  • Repo standard alignment: dropped scanner configs no consumer reads
    (.checkov.yml, .grype.yaml, .jscpd.json, .markdown-link-check.json,
    .secretlintrc.json, .trivy.yaml) — no reusable workflow or lefthook hook
    passed them to their tools, so they documented settings that never reached a
    scanner. The repo-local community health files (issue templates, PR template,
    CODEOWNERS) are gone as well, so the arillso/.github org defaults apply
    directly; the copies had drifted, and CODEOWNERS replaced the org file
    rather than extending it.
  • helm chart install in the fleet and tailscale molecule prepare steps now
    uses helm/helm v4 (Renovate).
  • Role metadata: drop EOL Ubuntu focal (and Debian buster where present)
    from galaxy_info.platforms across all roles; ensure jammy/noble and
    bullseye/bookworm are listed.
  • Role READMEs: add a Troubleshooting section to the docker, k3s, and
    fleet READMEs and correct the helm README, which documented a non-existent
    helm_version binary install — the role drives the K3s-embedded Helm
    controller via HelmChart CRDs, so the Quick Start now uses helm_charts.