diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index dab1adc..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,85 +0,0 @@ - - - - - -## Expected Behavior - - - -## Current Behavior - - -## Steps to Reproduce - - - -1. -2. -3. -4. - -## Context (variables) - - -Operating system: - -Hardware: - -### Variables Used - -`all.yml` - -```yml -k3s_version: "" -ansible_user: NA -systemd_dir: "" - -flannel_iface: "" - -#calico_iface: "" -calico_ebpf: "" -calico_cidr: "" -calico_tag: "" - -apiserver_endpoint: "" - -k3s_token: "NA" - -extra_server_args: "" -extra_agent_args: "" - -kube_vip_tag_version: "" - -kube_vip_cloud_provider_tag_version: "" -kube_vip_lb_ip_range: "" - -metal_lb_speaker_tag_version: "" -metal_lb_controller_tag_version: "" - -metal_lb_ip_range: "" -``` - -### Hosts - -`host.ini` - -```ini -[master] -IP.ADDRESS.ONE -IP.ADDRESS.TWO -IP.ADDRESS.THREE - -[node] -IP.ADDRESS.FOUR -IP.ADDRESS.FIVE - -[k3s_cluster:children] -master -node -``` - -## Possible Solution - - -- [ ] I've checked the [General Troubleshooting Guide](https://github.com/timothystewart6/k3s-ansible/discussions/20) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7198d18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,102 @@ +--- +name: Bug report +description: Report a reproducible problem with the playbooks, roles, or generated resources. +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for reporting a problem. Search existing issues and review the troubleshooting link first. + Remove credentials, tokens, public IP addresses, and private hostnames from all fields and logs. + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I searched existing issues and discussions for this problem. + required: true + - label: I reviewed the troubleshooting guidance linked from the issue chooser. + required: true + - label: I removed secrets and identifying infrastructure details from this report. + required: true + + - type: textarea + id: summary + attributes: + label: Problem summary + description: Describe what failed and its impact. + placeholder: A concise description of the problem and affected nodes or components. + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What should have happened? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Provide the smallest reliable sequence that reproduces the issue. + placeholder: | + 1. Configure ... + 2. Run ... + 3. Observe ... + validations: + required: true + + - type: input + id: revision + attributes: + label: Repository revision + description: Release, tag, branch, or commit SHA used. + placeholder: v1.36.2+k3s1+tt1 or a commit SHA + validations: + required: true + + - type: input + id: ansible-version + attributes: + label: Ansible version + description: Output of `ansible --version`, shortened to version and Python details. + placeholder: ansible-core 2.18.0, Python 3.12 + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Include target OS and version, architecture, node counts, platform, and network provider. + placeholder: Debian 13, amd64, 3 control nodes and 2 agents, bare metal, Cilium + validations: + required: true + + - type: textarea + id: configuration + attributes: + label: Relevant sanitized configuration + description: Include only variables and inventory groups needed to reproduce the problem. + render: yaml + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant logs or task output + description: Include the failing task and surrounding output. Redact sensitive or identifying values. + render: shell + + - type: textarea + id: context + attributes: + label: Additional context + description: Add attempted fixes, suspected causes, regressions, or other useful context. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ac9990b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +--- +blank_issues_enabled: false +contact_links: + - name: Troubleshooting and support + url: https://github.com/timothystewart6/k3s-ansible/discussions/20 + about: Review common troubleshooting guidance and ask configuration or usage questions. + - name: General discussions + url: https://github.com/timothystewart6/k3s-ansible/discussions + about: Discuss ideas and questions that are not confirmed bugs or concrete feature requests. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..23d9888 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,55 @@ +--- +name: Feature request +description: Propose a focused improvement to supported repository behavior. +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Describe the use case before proposing an implementation. Search existing issues and discussions first. + + - type: checkboxes + id: prerequisites + attributes: + label: Prerequisites + options: + - label: I searched existing issues and discussions for this request. + required: true + - label: This request is about reusable project behavior, not support for one private environment. + required: true + + - type: textarea + id: problem + attributes: + label: Problem or use case + description: What limitation exists, who encounters it, and why does it matter? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed behavior + description: Describe the desired user-visible result. Include example variables or commands when useful. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Describe workarounds or other designs and their tradeoffs. + + - type: textarea + id: compatibility + attributes: + label: Compatibility and operational impact + description: Note affected operating systems, architectures, CNIs, existing clusters, or reset behavior. + + - type: textarea + id: context + attributes: + label: Additional context + description: Add relevant upstream documentation, examples, or prior discussion. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d7a374b..55b02f0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,46 @@ -# Proposed Changes - +## Summary + + + +## Changes -- -- - -## Checklist +## Related issues -- [ ] Tested locally -- [ ] Ran `site.yml` playbook -- [ ] Ran `reset.yml` playbook -- [ ] Did not add any unnecessary changes -- [ ] Ran pre-commit install at least once before committing -- [ ] πŸš€ + + +## Testing + + + +- [ ] `pre-commit run --all-files` +- [ ] Relevant Ansible syntax checks +- [ ] Relevant focused regression tests +- [ ] Relevant Molecule scenario +- [ ] Provisioning tested against a non-production cluster +- [ ] Reset behavior tested against a non-production cluster + +Not run, with reason: + +## Risk and compatibility + + + +- Existing cluster or upgrade impact: +- Networking or CNI impact: +- Security impact: +- Rollback plan: + +## Documentation + + + +## Final checklist + +- [ ] The change is focused and contains no unrelated edits. +- [ ] Tests cover new behavior or a regression, where applicable. +- [ ] User-facing variables are documented in role defaults, sample inventory, and the README. +- [ ] Logs, examples, and configuration contain no secrets or identifying infrastructure details. +- [ ] Generated files and local environment files are not included. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3ae7a03 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,6 @@ +# GitHub Copilot instructions + +Read and follow the repository's root-level `AGENTS.md` before proposing or making changes. It is the canonical guide +for architecture, safety, implementation, validation, and documentation expectations. + +Do not duplicate repository guidance here. If instructions need to change, update `AGENTS.md`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f56cef..5dfdbd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ on: - '**/FUNDING.yml' - '**/host.ini' - '**/*.md' + - '.github/ISSUE_TEMPLATE/**' - '**/.editorconfig' - '**/ansible.example.cfg' - '**/deploy.sh' diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7ec8f1f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# Agent Guide + +This file is the canonical repository guide for coding agents and automated contributors. Read it before making +changes. Human contributors should also review [CONTRIBUTING.md](CONTRIBUTING.md). + +## Project overview + +This repository is an Ansible collection that provisions and resets highly available k3s clusters. It supports +multiple networking choices, including Flannel, Calico, Cilium, kube-vip, and MetalLB. + +The main entry points are: + +- `site.yml`: provision or update a cluster. +- `reset.yml`: remove k3s from a cluster. +- `reboot.yml`: reboot cluster nodes. +- `inventory/sample/`: example inventory and variables. +- `roles/`: reusable Ansible roles used by the playbooks. +- `molecule/`: integration scenarios run by CI. +- `.github/scripts/`: CI support scripts and focused regression tests. + +## Source of truth + +- Role defaults belong in `roles//defaults/main.yml`. +- Tasks belong in `roles//tasks/` and handlers in `roles//handlers/`. +- Example user configuration belongs in `inventory/sample/`. +- User-facing setup and variable documentation belongs in `README.md`. +- Contributor workflows and review expectations belong in `CONTRIBUTING.md`. +- Agent-specific repository instructions belong in this file. + +Keep `CLAUDE.md` and `.github/copilot-instructions.md` as small pointers to this file. Do not duplicate these +instructions in tool-specific files. + +## Development setup + +Use a Python virtual environment. Do not commit the environment, generated logs, inventories, kubeconfigs, or +credentials. + +```bash +python3 -m venv .env +source .env/bin/activate +python3 -m pip install -r requirements.txt +ansible-galaxy collection install -r collections/requirements.yml +pre-commit install +``` + +`ansible.cfg` is intentionally ignored. Copy `ansible.example.cfg` when local configuration is needed. + +## Working rules + +1. Inspect the current branch and worktree before editing. Preserve unrelated user changes. +2. Keep changes focused. Avoid drive-by formatting or dependency updates. +3. Never add real IP addresses, hostnames, tokens, private keys, kubeconfigs, or inventory secrets. +4. Use placeholders in examples and redact sensitive values from logs and issue reports. +5. Preserve idempotence. An already-converged host should not report changes without a real state transition. +6. Prefer Ansible modules over `ansible.builtin.command` or `ansible.builtin.shell`. When a command is required, + define accurate `changed_when` and `failed_when` behavior. +7. Use fully qualified collection names, such as `ansible.builtin.copy`. +8. Put configurable values in role defaults or inventory variables. Avoid embedding environment-specific values in + tasks and templates. +9. Maintain compatibility with the operating systems and architectures listed in `README.md`. +10. Do not weaken lint rules, tests, or CI checks to make a change pass. + +## Change guidance + +### Ansible tasks and roles + +- Use descriptive task names in sentence case. +- Use YAML booleans (`true` and `false`) rather than aliases. +- Quote file modes, for example `mode: "0644"`. +- Notify handlers only when the managed resource changes. +- Use `become: true` only where privilege escalation is needed. +- Update role defaults, sample inventory, and the README together when adding or renaming user-facing variables. +- Check reset behavior when provisioning introduces persistent services, files, mounts, or network state. + +### Templates and manifests + +- Keep Jinja logic small and readable. Move complicated decisions into task variables where practical. +- Render valid YAML after Jinja evaluation. +- Preserve explicit handling for optional and undefined variables. +- Add or update a focused test under `.github/scripts/` when changing generated Kubernetes manifests or bootstrap + behavior. + +### Molecule scenarios + +- Reuse `molecule/resources/` for shared behavior. +- Put scenario-specific inputs in `molecule//overrides.yml` and `verify-vars.yml`. +- Update `molecule/README.md` when adding, removing, or materially changing a scenario. +- Clean up resources created by tests, including failure paths. + +## Validation + +Run the smallest relevant checks while iterating, then run the complete local validation before considering a change +ready: + +```bash +pre-commit run --all-files +``` + +For playbook or role changes, also run syntax checks with a non-sensitive inventory: + +```bash +ansible-playbook site.yml --syntax-check -i inventory/sample/hosts.ini +ansible-playbook reset.yml --syntax-check -i inventory/sample/hosts.ini +``` + +Run focused regression scripts when their related files change. The mapping is defined in +`.pre-commit-config.yaml`. + +Molecule tests require Vagrant, VirtualBox, host-only networking, and substantial local resources. Run the most +relevant scenario when that environment is available: + +```bash +molecule test --scenario-name +``` + +If a required test can't be run locally, state exactly which check was skipped and why. Never claim a check passed +unless it was executed. + +## Documentation and review + +- Keep commands copyable and examples free of secrets. +- Update documentation in the same change as user-visible behavior. +- Explain behavior changes, compatibility concerns, operational risks, and rollback steps in the pull request. +- Use conventional commit messages with a scope, for example `fix(k3s-server): handle an existing token safely`. +- Do not commit, push, open a pull request, or modify remote resources unless the user explicitly requests it. + +## Definition of done + +A change is ready for review when it is focused, documented, linted, tested in proportion to its risk, and shown in a +clean diff with no secrets or generated artifacts. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..233e7d3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +@AGENTS.md + +`AGENTS.md` is the canonical repository guide. Follow it for all work in this repository. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..265927a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing to k3s-ansible + +Thank you for improving k3s-ansible. Contributions should be focused, safe to apply to existing clusters, and tested +in proportion to their operational impact. + +## Before opening an issue + +- Search existing issues and discussions for the same behavior. +- Review the [troubleshooting discussion](https://github.com/timothystewart6/k3s-ansible/discussions/20). +- Remove tokens, credentials, public IP addresses, private hostnames, and other sensitive values from logs and + configuration. +- For support requests, include the k3s-ansible revision, Ansible version, target operating system, architecture, + network provider, relevant sanitized variables, and a minimal reproduction. + +Use the bug report template for reproducible defects and the feature request template for proposed behavior. + +## Development environment + +Fork and clone the repository, then create a branch from the latest `master`: + +```bash +git switch master +git pull --ff-only +git switch -c / +``` + +Create a Python environment and install the pinned development dependencies: + +```bash +python3 -m venv .env +source .env/bin/activate +python3 -m pip install -r requirements.txt +ansible-galaxy collection install -r collections/requirements.yml +pre-commit install +``` + +Copy `ansible.example.cfg` to the ignored `ansible.cfg` file if local Ansible configuration is needed. Start custom +inventories from `inventory/sample/`, keep them out of Git, and never use production credentials in tests. + +## Making changes + +- Keep each pull request focused on one problem or feature. +- Follow [AGENTS.md](AGENTS.md) for repository structure, implementation conventions, and safety requirements. +- Preserve idempotence and existing-cluster compatibility. +- Add or update tests for behavior changes and regressions. +- Update role defaults, sample inventory, and documentation when user-facing variables change. +- Consider both provisioning and reset behavior for persistent resources. +- Avoid unrelated reformatting and generated files. + +## Validation + +Run all pre-commit checks before submitting a pull request: + +```bash +pre-commit run --all-files +``` + +For playbook or role changes, run syntax checks: + +```bash +ansible-playbook site.yml --syntax-check -i inventory/sample/hosts.ini +ansible-playbook reset.yml --syntax-check -i inventory/sample/hosts.ini +``` + +Run the most relevant Molecule scenario when Vagrant, VirtualBox, and the required host networking are available: + +```bash +molecule test --scenario-name +``` + +See [molecule/README.md](molecule/README.md) for scenario details and local requirements. Pull requests should list +every check that was run and clearly identify checks that could not be run locally. + +## Commits and pull requests + +Use a conventional commit subject with a scope: + +```text +type(scope): short description +``` + +Common types are `feat`, `fix`, `refactor`, `test`, `docs`, and `chore`. Write subjects in the imperative mood and +keep commits logically focused. + +Pull requests should: + +- Describe the problem and the resulting behavior. +- Identify compatibility, security, networking, and upgrade risks. +- Include testing evidence without sensitive data. +- Call out documentation and sample configuration changes. +- Link related issues with `Fixes #` when applicable. +- Avoid checking boxes for tests that were not run. + +Maintainers may ask for a change to be split when unrelated work makes it difficult to review or roll back. diff --git a/README.md b/README.md index 87063ec..e40401c 100644 --- a/README.md +++ b/README.md @@ -2,51 +2,66 @@ ![Fully Automated K3S etcd High Availability Install](https://img.youtube.com/vi/CbkEWcUZ7zM/0.jpg) -This playbook will build an HA Kubernetes cluster with `k3s`, `kube-vip` and MetalLB via `ansible`. +This Ansible collection builds a highly available Kubernetes cluster with k3s. It supports kube-vip for the control +plane virtual IP, multiple CNI options, and either MetalLB or kube-vip for service load balancing. This is based on the work from [this fork](https://github.com/212850a/k3s-ansible) which is based on the work from [k3s-io/k3s-ansible](https://github.com/k3s-io/k3s-ansible). It uses [kube-vip](https://kube-vip.io/) to create a load balancer for control plane, and [metal-lb](https://metallb.universe.tf/installation/) for its service `LoadBalancer`. -If you want more context on how this works, see: +For more context on how it works, see: πŸ“„ [Documentation](https://technotim.com/posts/k3s-etcd-ansible/) (including example commands) πŸ“Ί [Watch the Video](https://www.youtube.com/watch?v=CbkEWcUZ7zM) +## Project guides + +- [Getting started](#-getting-started) +- [Configuration variables](#variables) +- [Upgrading an existing cluster](#-upgrading-an-existing-cluster) +- [Local Molecule testing](molecule/README.md) +- [Contributing guidelines](CONTRIBUTING.md) +- [Repository guide for coding agents](AGENTS.md) + ## πŸ“– k3s Ansible Playbook -Build a Kubernetes cluster using Ansible with k3s. The goal is easily install a HA Kubernetes cluster on machines running: +Build a Kubernetes cluster using Ansible and k3s. The goal is to make a highly available cluster straightforward to +install on machines running: - [x] Debian (tested on version 13) - [x] Ubuntu (tested on version 26.04 LTS) - [x] Rocky (tested on version 10) -on processor architecture: +Supported processor architectures are: -- [X] x64 -- [X] arm64 -- [X] armhf +- [x] x64 +- [x] arm64 +- [x] armhf ## βœ… System requirements -- Control Node (the machine you are running `ansible` commands) must have Ansible 2.11+ If you need a quick primer on Ansible [you can check out my docs and setting up Ansible](https://technotim.com/posts/ansible-automation/). +- The control node, which runs the Ansible commands, must have Ansible 2.11 or newer. For a quick primer, see + [setting up Ansible](https://technotim.com/posts/ansible-automation/). -- You will also need to install collections that this playbook uses by running `ansible-galaxy collection install -r ./collections/requirements.yml` (important❗) +- Install the required collections with + `ansible-galaxy collection install -r ./collections/requirements.yml`. - [`netaddr` package](https://pypi.org/project/netaddr/) must be available to Ansible. If you have installed Ansible via apt, this is already taken care of. If you have installed Ansible via `pip`, make sure to install `netaddr` into the respective virtual environment. -- `server` and `agent` nodes should have passwordless SSH access, if not you can supply arguments to provide credentials `--ask-pass --ask-become-pass` to each command. +- Server and agent nodes should support passwordless SSH access. Otherwise, pass `--ask-pass --ask-become-pass` to + each playbook command. ## πŸš€ Getting Started ### 🍴 Preparation -First create a new directory based on the `sample` directory within the `inventory` directory: +Create a cluster-specific inventory from the sample. The `inventory/` directory ignores custom inventory content so +credentials and environment details aren't committed accidentally. ```bash cp -R inventory/sample inventory/my-cluster ``` -Second, edit `inventory/my-cluster/hosts.ini` to match the system information gathered above +Edit `inventory/my-cluster/hosts.ini` to match the target hosts. For example: @@ -67,9 +82,10 @@ node If multiple hosts are in the master group, the playbook will automatically set up k3s in [HA mode with etcd](https://rancher.com/docs/k3s/latest/en/installation/ha-embedded/). -Finally, copy `ansible.example.cfg` to `ansible.cfg` and adapt the inventory path to match the files that you just created. +Copy `ansible.example.cfg` to `ansible.cfg`, then update its inventory path. The local `ansible.cfg` file is ignored by +Git. -This requires at least k3s version `1.19.1` however the version is configurable by using the `k3s_version` variable. +The minimum k3s version is `1.19.1`. Select the desired version with the `k3s_version` variable. If needed, you can also edit `inventory/my-cluster/group_vars/all.yml` to match your environment. @@ -81,7 +97,8 @@ Start provisioning of the cluster using the following command: ansible-playbook site.yml -i inventory/my-cluster/hosts.ini ``` -After deployment control plane will be accessible via virtual ip-address which is defined in inventory/group_vars/all.yml as `apiserver_endpoint` +After deployment, the control plane is accessible through the virtual IP defined by `apiserver_endpoint` in the +inventory variables. ### πŸ”₯ Remove k3s cluster @@ -89,7 +106,7 @@ After deployment control plane will be accessible via virtual ip-address which i ansible-playbook reset.yml -i inventory/my-cluster/hosts.ini ``` ->You should also reboot these nodes due to the VIP not being destroyed +> Reboot the nodes after reset because the virtual IP may remain configured. ## πŸ” Upgrading an existing cluster @@ -124,13 +141,21 @@ To copy your `kube config` locally so that you can access your **Kubernetes** cl ```bash scp debian@master_ip:/etc/rancher/k3s/k3s.yaml ~/.kube/config ``` -If you get file Permission denied, go into the node and temporarly run: +If the copy fails with a permission error, grant the SSH user temporary read access using the least permissive method +available for the target system. Restore the original ownership and permissions immediately after copying. Avoid +world-writable permissions on the kubeconfig because it contains cluster credentials. + +For example, copy the file to a temporary user-readable path from the control node: + ```bash -sudo chmod 777 /etc/rancher/k3s/k3s.yaml +ssh debian@master_ip 'sudo install -o "$(id -un)" -m 0600 /etc/rancher/k3s/k3s.yaml /tmp/k3s.yaml' ``` -Then copy with the scp command and reset the permissions back to: + +Copy `/tmp/k3s.yaml`, then remove the temporary remote copy: + ```bash -sudo chmod 600 /etc/rancher/k3s/k3s.yaml +scp debian@master_ip:/tmp/k3s.yaml ~/.kube/config +ssh debian@master_ip rm -f /tmp/k3s.yaml ``` You'll then want to modify the config to point to master IP by running: @@ -150,7 +175,7 @@ See the commands [here](https://technotim.com/posts/k3s-etcd-ansible/#testing-yo | `download` | `k3s_version` | string | ❌ | Required | K3s binaries version | | `k3s_agent`, `k3s_server`, `k3s_server_post` | `apiserver_endpoint` | string | ❌ | Required | Virtual ip-address configured on each master | | `k3s_agent` | `extra_agent_args` | string | `null` | Not required | Extra arguments for agents nodes | -| `k3s_agent`, `k3s_server` | `group_name_master` | string | `null` | Not required | Name othe master group | +| `k3s_agent`, `k3s_server` | `group_name_master` | string | `null` | Not required | Name of the master group | | `k3s_agent` | `k3s_token` | string | `null` | Not required | Token used to communicate between masters | | `k3s_agent`, `k3s_server` | `proxy_env` | dict | `null` | Not required | Internet proxy configurations | | `k3s_agent`, `k3s_server` | `proxy_env.HTTP_PROXY` | string | ❌ | Required | HTTP internet proxy | @@ -227,9 +252,11 @@ It is run automatically in CI, but you can also run the tests locally. This might be helpful for quick feedback in a few cases. You can find more information about it [here](molecule/README.md). -### Pre-commit Hooks +### Pre-commit hooks -This repo uses `pre-commit` and `pre-commit-hooks` to lint and fix common style and syntax errors. Be sure to install python packages and then run `pre-commit install`. For more information, see [pre-commit](https://pre-commit.com/) +This repository uses `pre-commit` to check style, syntax, Ansible content, and shell scripts. Install the Python +dependencies, run `pre-commit install` once, and run `pre-commit run --all-files` before submitting a change. See +[CONTRIBUTING.md](CONTRIBUTING.md) for the complete development workflow. ## 🌌 Ansible Galaxy