├── .github ├── CODEOWNERS ├── FUNDING.yml ├── labels.yml ├── release-drafter.yml ├── renovate.json └── workflows │ ├── additional-tags.yaml │ ├── labels.yaml │ ├── lint.yaml │ ├── lock.yaml │ ├── release-drafter.yaml │ └── stale.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── action.yaml └── src ├── Dockerfile ├── build.schema.json ├── config.schema.json ├── lint.py └── requirements.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @frenck 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: frenck 3 | patreon: frenck 4 | custom: https://frenck.dev/donate/ 5 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "breaking-change" 3 | color: ee0701 4 | description: "A breaking change for existing users." 5 | - name: "bugfix" 6 | color: ee0701 7 | description: 8 | "Inconsistencies or issues which will cause a problem for users or 9 | implementors." 10 | - name: "documentation" 11 | color: 0052cc 12 | description: "Solely about the documentation of the project." 13 | - name: "enhancement" 14 | color: 1d76db 15 | description: "Enhancement of the code, not introducing new features." 16 | - name: "refactor" 17 | color: 1d76db 18 | description: "Improvement of existing code, not introducing new features." 19 | - name: "performance" 20 | color: 1d76db 21 | description: "Improving performance, not introducing new features." 22 | - name: "new-feature" 23 | color: 0e8a16 24 | description: "New features or options." 25 | - name: "maintenance" 26 | color: 2af79e 27 | description: "Generic maintenance tasks." 28 | - name: "ci" 29 | color: 1d76db 30 | description: "Work that improves the continue integration." 31 | - name: "dependencies" 32 | color: 1d76db 33 | description: "Upgrade or downgrade of project dependencies." 34 | 35 | - name: "in-progress" 36 | color: fbca04 37 | description: "Issue is currently being resolved by a developer." 38 | - name: "stale" 39 | color: fef2c0 40 | description: 41 | "There has not been activity on this issue or PR for quite some time." 42 | - name: "no-stale" 43 | color: fef2c0 44 | description: "This issue or PR is exempted from the stable bot." 45 | 46 | - name: "security" 47 | color: ee0701 48 | description: "Marks a security issue that needs to be resolved asap." 49 | - name: "incomplete" 50 | color: fef2c0 51 | description: "Marks a PR or issue that is missing information." 52 | - name: "invalid" 53 | color: fef2c0 54 | description: "Marks a PR or issue that is missing information." 55 | 56 | - name: "beginner-friendly" 57 | color: 0e8a16 58 | description: 59 | "Good first issue for people wanting to contribute to the project." 60 | - name: "help-wanted" 61 | color: 0e8a16 62 | description: 63 | "We need some extra helping hands or expertise in order to resolve this." 64 | 65 | - name: "hacktoberfest" 66 | description: "Issues/PRs are participating in the Hacktoberfest." 67 | color: fbca04 68 | - name: "hacktoberfest-accepted" 69 | description: "Issues/PRs are participating in the Hacktoberfest." 70 | color: fbca04 71 | 72 | - name: "priority-critical" 73 | color: ee0701 74 | description: 75 | "This should be dealt with ASAP. Not fixing this issue would be a serious 76 | error." 77 | - name: "priority-high" 78 | color: b60205 79 | description: 80 | "After critical issues are fixed, these should be dealt with before any 81 | further issues." 82 | - name: "priority-medium" 83 | color: 0e8a16 84 | description: "This issue may be useful, and needs some attention." 85 | - name: "priority-low" 86 | color: e4ea8a 87 | description: "Nice addition, maybe... someday..." 88 | 89 | - name: "major" 90 | color: b60205 91 | description: "This PR causes a major version bump in the version number." 92 | - name: "minor" 93 | color: 0e8a16 94 | description: "This PR causes a minor version bump in the version number." 95 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: "v$RESOLVED_VERSION" 3 | tag-template: "v$RESOLVED_VERSION" 4 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 5 | sort-direction: ascending 6 | 7 | categories: 8 | - title: "🚨 Breaking changes" 9 | labels: 10 | - "breaking-change" 11 | - title: "✨ New features" 12 | labels: 13 | - "new-feature" 14 | - title: "🐛 Bug fixes" 15 | labels: 16 | - "bugfix" 17 | - title: "🚀 Enhancements" 18 | labels: 19 | - "enhancement" 20 | - "refactor" 21 | - "performance" 22 | - title: "🧰 Maintenance" 23 | labels: 24 | - "maintenance" 25 | - "ci" 26 | - title: "📚 Documentation" 27 | labels: 28 | - "documentation" 29 | - title: "⬆️ Dependency updates" 30 | labels: 31 | - "dependencies" 32 | 33 | version-resolver: 34 | major: 35 | labels: 36 | - "major" 37 | - "breaking-change" 38 | minor: 39 | labels: 40 | - "minor" 41 | - "new-feature" 42 | patch: 43 | labels: 44 | - "bugfix" 45 | - "chore" 46 | - "ci" 47 | - "dependencies" 48 | - "documentation" 49 | - "enhancement" 50 | - "performance" 51 | - "refactor" 52 | default: patch 53 | 54 | template: | 55 | ## What’s changed 56 | 57 | $CHANGES 58 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "rebaseWhen": "behind-base-branch", 4 | "dependencyDashboard": true, 5 | "labels": ["dependencies", "no-stale"], 6 | "commitMessagePrefix": "⬆️", 7 | "commitMessageTopic": "{{depName}}", 8 | "packageRules": [ 9 | { 10 | "matchManagers": ["github-actions"], 11 | "addLabels": ["github_actions"], 12 | "rangeStrategy": "pin" 13 | }, 14 | { 15 | "matchManagers": ["github-actions"], 16 | "matchUpdateTypes": ["minor", "patch"], 17 | "automerge": true 18 | }, 19 | { 20 | "matchManagers": ["pip_requirements"], 21 | "addLabels": ["python"], 22 | "rangeStrategy": "pin" 23 | }, 24 | { 25 | "matchManagers": ["pip_requirements"], 26 | "matchUpdateTypes": ["minor", "patch"], 27 | "automerge": true 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/additional-tags.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Additional Tags 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | tags: 8 | - v[0-9]+.[0-9]+.[0-9]+ 9 | release: 10 | types: 11 | - published 12 | workflow_dispatch: 13 | 14 | jobs: 15 | additional-tags: 16 | name: 🏷 Additional Tags 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: ⤵️ Check out code from GitHub 20 | uses: actions/checkout@v4.2.2 21 | - name: 🚀 Run Release Tracker 22 | uses: vweevers/additional-tags-action@v2.0.0 23 | -------------------------------------------------------------------------------- /.github/workflows/labels.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sync labels 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - .github/labels.yml 11 | workflow_dispatch: 12 | 13 | jobs: 14 | labels: 15 | name: ♻️ Sync labels 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: ⤵️ Check out code from GitHub 19 | uses: actions/checkout@v4.2.2 20 | - name: 🚀 Run Label Syncer 21 | uses: micnncim/action-label-syncer@v1.3.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint 3 | 4 | # yamllint disable-line rule:truthy 5 | on: [push, pull_request, workflow_dispatch] 6 | 7 | jobs: 8 | build: 9 | name: yamllint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: ⤵️ Check out code from GitHub 13 | uses: actions/checkout@v4.2.2 14 | - name: 🚀 Run yamllint 15 | uses: frenck/action-yamllint@v1.5 16 | 17 | json: 18 | name: JSON Lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: ⤵️ Check out code from GitHub 22 | uses: actions/checkout@v4.2.2 23 | - name: 🚀 Run JQ 24 | run: | 25 | shopt -s globstar 26 | cat **/*.json | jq '.' 27 | 28 | prettier: 29 | name: Prettier 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: ⤵️ Check out code from GitHub 33 | uses: actions/checkout@v4.2.2 34 | - name: 🚀 Run Prettier 35 | uses: creyD/prettier_action@v4.6 36 | with: 37 | prettier_options: --write **/*.{json,js,md,yaml} 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lock 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | schedule: 7 | - cron: "0 9 * * *" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lock: 12 | name: 🔒 Lock closed issues and PRs 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: dessant/lock-threads@v5.0.1 16 | with: 17 | github-token: ${{ github.token }} 18 | issue-inactive-days: "30" 19 | issue-lock-reason: "" 20 | pr-inactive-days: "1" 21 | pr-lock-reason: "" 22 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Drafter 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | update_release_draft: 12 | name: ✏️ Draft release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 🚀 Run Release Drafter 16 | uses: release-drafter/release-drafter@v6.1.0 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Stale 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | schedule: 7 | - cron: "0 8 * * *" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | stale: 12 | name: 🧹 Clean up stale issues and PRs 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 🚀 Run stale 16 | uses: actions/stale@v9.1.0 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | days-before-stale: 30 20 | days-before-close: 7 21 | remove-stale-when-updated: true 22 | stale-issue-label: "stale" 23 | exempt-issue-labels: "no-stale,help-wanted" 24 | stale-issue-message: > 25 | There hasn't been any activity on this issue recently, so we clean 26 | up some of the older and inactive issues. 27 | 28 | Please make sure to update to the latest version and check if that 29 | solves the issue. Let us know if that works for you by leaving a 30 | comment 👍 31 | 32 | This issue has now been marked as stale and will be closed if no 33 | further activity occurs. Thanks! 34 | stale-pr-label: "stale" 35 | exempt-pr-labels: "no-stale" 36 | stale-pr-message: > 37 | There hasn't been any activity on this pull request recently. This 38 | pull request has been automatically marked as stale because of that 39 | and will be closed if no further activity occurs within 7 days. 40 | Thank you for your contributions. 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .python-version 2 | venv/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | frenck@frenck.dev. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][faq]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [mozilla coc]: https://github.com/mozilla/diversity 132 | [faq]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish 4 | to make via issue, email, or any other method with the owners of this repository 5 | before making a change. 6 | 7 | Please note we have a code of conduct, please follow it in all your interactions 8 | with the project. 9 | 10 | ## Issues and feature requests 11 | 12 | You've found a bug in the source code, a mistake in the documentation or maybe 13 | you'd like a new feature? You can help us by submitting an issue to our 14 | [GitHub Repository][github]. Before you create an issue, make sure you search 15 | the archive, maybe your question was already answered. 16 | 17 | Even better: You could submit a pull request with a fix / new feature! 18 | 19 | ## Pull request process 20 | 21 | 1. Search our repository for open or closed [pull requests][prs] that relates 22 | to your submission. You don't want to duplicate effort. 23 | 24 | 1. You may merge the pull request in once you have the sign-off of two other 25 | developers, or if you do not have permission to do that, you may request 26 | the second reviewer to merge it for you. 27 | 28 | [github]: https://github.com/frenck/action-addon-linter/issues 29 | [prs]: https://github.com/frenck/action-addon-linter/pulls 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021-2023 Franck Nijhof 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Frenck's Github Action: Home Assistant Add-on Linter 2 | 3 | [![GitHub Release][releases-shield]][releases] 4 | ![Project Stage][project-stage-shield] 5 | ![Project Maintenance][maintenance-shield] 6 | [![License][license-shield]](LICENSE.md) 7 | 8 | [![Sponsor Frenck via GitHub Sponsors][github-sponsors-shield]][github-sponsors] 9 | 10 | 🚀 Frenck's GitHub Action for linting Home Assistant Add-ons. 11 | 12 | ## About 13 | 14 | This GitHub Action is able to validate/lint Home Assistant Add-on configuration 15 | files. 16 | 17 | Besides checking for validity of add-on configuration files, it will 18 | also warn for default configurations that can be removed and cleaned up. 19 | 20 | ## Usage 21 | 22 | ```yaml 23 | name: Lint 24 | on: [push, pull_request] 25 | jobs: 26 | build: 27 | name: Add-on configuration 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: ⤵️ Check out code from GitHub 31 | uses: actions/checkout@v3 32 | - name: 🚀 Run Home Assistant Add-on Lint 33 | uses: frenck/action-addon-linter@v2 34 | with: 35 | path: "./addon" 36 | ``` 37 | 38 | ## Arguments 39 | 40 | | Input | Description | Usage | 41 | | :---------: | :-----------------------------------------------------------------: | :----------: | 42 | | `path` | Path to the folder containing the add-on config.json file. | **Required** | 43 | | `community` | Enable Home Assistant Community Add-ons mode, with specific checks. | _Optional_ | 44 | 45 | ## Updating the JSON Schema 46 | 47 | For the larger part, JSON Schemas are used to validate the configuration files. 48 | The schema files for both the add-on `config.json` and `build.json` can be found 49 | in the `src/` folder. 50 | 51 | - `src/config.schema.json` is used to validate `config.json` 52 | - `src/build.schema.json` is used to validate `build.json` 53 | 54 | If you adjust the schema, please make sure they are pretty: 55 | 56 | ```bash 57 | npx prettier --write ./src/config.schema.json 58 | ``` 59 | 60 | ## Changelog & Releases 61 | 62 | This repository keeps a change log using [GitHub's releases][releases] 63 | functionality. 64 | 65 | Releases are based on [Semantic Versioning][semver], and use the format 66 | of `MAJOR.MINOR.PATCH`. In a nutshell, the version will be incremented 67 | based on the following: 68 | 69 | - `MAJOR`: Incompatible or major changes. 70 | - `MINOR`: Backwards-compatible new features and enhancements. 71 | - `PATCH`: Backwards-compatible bugfixes and package updates. 72 | 73 | ## Versions & Updating 74 | 75 | You can specify which version of this GitHub Action your workflow should use. 76 | And even allowing for using the latest major or minor version. 77 | 78 | For example; this will use release `v1.1.1` of a GitHub Action: 79 | 80 | ```yaml 81 | - name: 🚀 Run Home Assistant Add-on Lint 82 | uses: frenck/action-addon-linter@v1.1.1 83 | ``` 84 | 85 | While the following example, will use the `v1.1.x` minor release, for example 86 | if `v1.1.2` is the latest releases (starting with `v1.1`), this will run 87 | `v1.1.2`: 88 | 89 | ```yaml 90 | - name: 🚀 Run Home Assistant Add-on Lint 91 | uses: frenck/action-addon-linter@v1.1 92 | ``` 93 | 94 | As in the examples throughout the documentation, the following example is 95 | locked on major version, meaning any `v1.x.x` latest version will be used, 96 | as long as it is version 1. 97 | 98 | ```yaml 99 | - name: 🚀 Run Home Assistant Add-on Lint 100 | uses: frenck/action-addon-linter@v1 101 | ``` 102 | 103 | ### Automatically update using Dependabot 104 | 105 | The advantage of locking against a more specific version, is that it prevents 106 | surprises if an issue or breaking changes were introduced in a newer release. 107 | 108 | The disadvantage of being more specific, is that it requires you to keep things 109 | up to date. Fortunately, GitHub has a tool for that, called: Dependabot. 110 | 111 | Dependabot can automatically open a pull request on your repository to update 112 | this Action for you. You can instantly see if the new version works (as the 113 | pull request shows the success or failure status) and you can decide to 114 | merge it in by hitting the merge button. Quick, easy and always up2date. 115 | 116 | To enable Dependabot, create a file called `.github/dependabot.yaml`: 117 | 118 | ```yaml 119 | version: 2 120 | updates: 121 | - package-ecosystem: "github-actions" 122 | directory: "/" 123 | schedule: 124 | interval: daily 125 | ``` 126 | 127 | Your all set! Dependabot will now check (and update) your GitHub actions 128 | every day. 🤩 129 | 130 | ## Contributing 131 | 132 | This is an active open-source project. We are always open to people who want to 133 | use the code or contribute to it. 134 | 135 | We've set up a separate document for our 136 | [contribution guidelines](CONTRIBUTING.md). 137 | 138 | Thank you for being involved! :heart_eyes: 139 | 140 | ## Authors & contributors 141 | 142 | The original setup of this repository is by [Franck Nijhof][frenck]. 143 | 144 | For a full list of all authors and contributors, 145 | check [the contributor's page][contributors]. 146 | 147 | ## License 148 | 149 | MIT License 150 | 151 | Copyright (c) 2021-2023 Franck Nijhof 152 | 153 | Permission is hereby granted, free of charge, to any person obtaining a copy 154 | of this software and associated documentation files (the "Software"), to deal 155 | in the Software without restriction, including without limitation the rights 156 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 157 | copies of the Software, and to permit persons to whom the Software is 158 | furnished to do so, subject to the following conditions: 159 | 160 | The above copyright notice and this permission notice shall be included in all 161 | copies or substantial portions of the Software. 162 | 163 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 164 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 165 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 166 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 167 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 168 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 169 | SOFTWARE. 170 | 171 | [contributors]: https://github.com/frenck/action-addon-linter/graphs/contributors 172 | [frenck]: https://github.com/frenck 173 | [github-sponsors-shield]: https://frenck.dev/wp-content/uploads/2019/12/github_sponsor.png 174 | [github-sponsors]: https://github.com/sponsors/frenck 175 | [license-shield]: https://img.shields.io/github/license/frenck/action-addon-linter.svg 176 | [maintenance-shield]: https://img.shields.io/maintenance/yes/2023.svg 177 | [project-stage-shield]: https://img.shields.io/badge/project%20stage-production%20ready-brightgreen.svg 178 | [releases-shield]: https://img.shields.io/github/release/frenck/action-addon-linter.svg 179 | [releases]: https://github.com/frenck/action-addon-linter/releases 180 | [semver]: http://semver.org/spec/v2.0.0.html 181 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Frenck's Home Assistant Add-on Linter" 3 | description: 🚀 Frenck's GitHub Action for linting Home Assistant Add-ons. 4 | author: frenck 5 | 6 | branding: 7 | color: red 8 | icon: thumbs-up 9 | 10 | inputs: 11 | path: 12 | description: Path to the add-on configuration (where config.json is) 13 | required: true 14 | community: 15 | description: Enable Home Assistant Community Add-ons mode 16 | default: "false" 17 | required: false 18 | 19 | runs: 20 | using: "docker" 21 | image: "src/Dockerfile" 22 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-alpine 2 | 3 | COPY requirements.txt /tmp/ 4 | 5 | RUN \ 6 | pip install \ 7 | --no-cache-dir \ 8 | --prefer-binary \ 9 | -r /tmp/requirements.txt 10 | 11 | COPY build.schema.json /build.schema.json 12 | COPY config.schema.json /config.schema.json 13 | COPY lint.py /lint.py 14 | 15 | ENTRYPOINT ["python3", "/lint.py"] 16 | -------------------------------------------------------------------------------- /src/build.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "additionalProperties": false, 4 | "properties": { 5 | "args": { 6 | "additionalProperties": {}, 7 | "default": {}, 8 | "type": "object" 9 | }, 10 | "build_from": { 11 | "default": {}, 12 | "anyOf": [ 13 | { 14 | "additionalProperties": false, 15 | "properties": { 16 | "aarch64": { 17 | "default": "homeassistant/aarch64-base:latest", 18 | "pattern": "^([a-zA-Z\\-\\.:\\d{}]+/)*?(([\\-\\w{}]+)/)?([\\-\\w{}]+)(:[\\.\\-\\w{}]+)?$", 19 | "type": "string" 20 | }, 21 | "amd64": { 22 | "default": "homeassistant/amd64-base:latest", 23 | "pattern": "^([a-zA-Z\\-\\.:\\d{}]+/)*?(([\\-\\w{}]+)/)?([\\-\\w{}]+)(:[\\.\\-\\w{}]+)?$", 24 | "type": "string" 25 | }, 26 | "armhf": { 27 | "default": "homeassistant/armhf-base:latest", 28 | "pattern": "^([a-zA-Z\\-\\.:\\d{}]+/)*?(([\\-\\w{}]+)/)?([\\-\\w{}]+)(:[\\.\\-\\w{}]+)?$", 29 | "type": "string" 30 | }, 31 | "armv7": { 32 | "default": "homeassistant/armv7-base:latest", 33 | "pattern": "^([a-zA-Z\\-\\.:\\d{}]+/)*?(([\\-\\w{}]+)/)?([\\-\\w{}]+)(:[\\.\\-\\w{}]+)?$", 34 | "type": "string" 35 | }, 36 | "i386": { 37 | "default": "homeassistant/i386-base:latest", 38 | "pattern": "^([a-zA-Z\\-\\.:\\d{}]+/)*?(([\\-\\w{}]+)/)?([\\-\\w{}]+)(:[\\.\\-\\w{}]+)?$", 39 | "type": "string" 40 | } 41 | }, 42 | "type": "object" 43 | }, 44 | { 45 | "type": "string", 46 | "pattern": "^([a-zA-Z\\-\\.:\\d{}]+/)*?([\\-\\w{}]+)/([\\-\\w{}]+)(:[\\.\\-\\w{}]+)?$" 47 | } 48 | ] 49 | }, 50 | "codenotary": { 51 | "additionalProperties": false, 52 | "default": {}, 53 | "properties": { 54 | "signer": { 55 | "type": "string", 56 | "format": "email" 57 | }, 58 | "base_image": { 59 | "type": "string", 60 | "format": "email" 61 | } 62 | }, 63 | "type": "object" 64 | }, 65 | "labels": { 66 | "additionalProperties": { 67 | "type": "string" 68 | }, 69 | "default": {}, 70 | "type": "object" 71 | }, 72 | "squash": { 73 | "default": false, 74 | "type": "boolean" 75 | } 76 | }, 77 | "type": "object" 78 | } 79 | -------------------------------------------------------------------------------- /src/config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "additionalProperties": false, 4 | "properties": { 5 | "advanced": { 6 | "default": false, 7 | "type": "boolean" 8 | }, 9 | "apparmor": { 10 | "default": true, 11 | "type": ["string", "boolean"] 12 | }, 13 | "arch": { 14 | "items": { 15 | "enum": ["aarch64", "amd64", "armhf", "armv7", "i386"], 16 | "type": "string" 17 | }, 18 | "type": "array" 19 | }, 20 | "audio": { 21 | "default": false, 22 | "type": "boolean" 23 | }, 24 | "auth_api": { 25 | "default": false, 26 | "type": "boolean" 27 | }, 28 | "auto_uart": { 29 | "default": false, 30 | "type": "boolean" 31 | }, 32 | "backup": { 33 | "default": "hot", 34 | "enum": ["hot", "cold"], 35 | "type": "string" 36 | }, 37 | "backup_exclude": { 38 | "items": { 39 | "type": "string" 40 | }, 41 | "type": "array" 42 | }, 43 | "backup_pre": { 44 | "type": "string" 45 | }, 46 | "backup_post": { 47 | "type": "string" 48 | }, 49 | "boot": { 50 | "default": "auto", 51 | "enum": ["auto", "manual", "manual_only"], 52 | "type": "string" 53 | }, 54 | "breaking_versions": { 55 | "items": { 56 | "type": "string" 57 | }, 58 | "type": "array" 59 | }, 60 | "codenotary": { 61 | "type": "string", 62 | "format": "email" 63 | }, 64 | "description": { 65 | "type": "string" 66 | }, 67 | "devices": { 68 | "items": { 69 | "type": "string" 70 | }, 71 | "type": "array" 72 | }, 73 | "devicetree": { 74 | "default": false, 75 | "type": "boolean" 76 | }, 77 | "discovery": { 78 | "items": { 79 | "type": "string" 80 | }, 81 | "type": "array" 82 | }, 83 | "docker_api": { 84 | "default": false, 85 | "type": "boolean" 86 | }, 87 | "environment": { 88 | "additionalProperties": {}, 89 | "type": "object" 90 | }, 91 | "full_access": { 92 | "default": false, 93 | "type": "boolean" 94 | }, 95 | "gpio": { 96 | "default": false, 97 | "type": "boolean" 98 | }, 99 | "hassio_api": { 100 | "default": false, 101 | "type": "boolean" 102 | }, 103 | "hassio_role": { 104 | "default": "default", 105 | "enum": ["admin", "backup", "default", "homeassistant", "manager"], 106 | "type": "string" 107 | }, 108 | "homeassistant": { 109 | "default": false, 110 | "type": "string" 111 | }, 112 | "homeassistant_api": { 113 | "default": false, 114 | "type": "boolean" 115 | }, 116 | "host_dbus": { 117 | "default": false, 118 | "type": "boolean" 119 | }, 120 | "host_ipc": { 121 | "default": false, 122 | "type": "boolean" 123 | }, 124 | "host_network": { 125 | "default": false, 126 | "type": "boolean" 127 | }, 128 | "host_pid": { 129 | "default": false, 130 | "type": "boolean" 131 | }, 132 | "host_uts": { 133 | "default": false, 134 | "type": "boolean" 135 | }, 136 | "image": { 137 | "type": "string" 138 | }, 139 | "ingress": { 140 | "default": false, 141 | "type": "boolean" 142 | }, 143 | "ingress_entry": { 144 | "type": "string" 145 | }, 146 | "ingress_port": { 147 | "default": 8099, 148 | "type": "integer", 149 | "minimum": 0, 150 | "maximum": 65535 151 | }, 152 | "ingress_stream": { 153 | "default": false, 154 | "type": "boolean" 155 | }, 156 | "init": { 157 | "default": true, 158 | "type": "boolean" 159 | }, 160 | "journald": { 161 | "default": false, 162 | "type": "boolean" 163 | }, 164 | "kernel_modules": { 165 | "default": false, 166 | "type": "boolean" 167 | }, 168 | "legacy": { 169 | "default": false, 170 | "type": "boolean" 171 | }, 172 | "machine": { 173 | "items": { 174 | "pattern": "^!?(?:generic-aarch64|generic-x86-64|intel-nuc|khadas-vim3|odroid-c2|odroid-c4|odroid-n2|odroid-xu|qemuarm|qemuarm-64|qemux86|qemux86-64|raspberrypi|raspberrypi2|raspberrypi3|raspberrypi3-64|raspberrypi4|raspberrypi4-64|tinker|yellow)$" 175 | }, 176 | "type": "array" 177 | }, 178 | "map": { 179 | "type": "array", 180 | "oneOf": [ 181 | { 182 | "items": { 183 | "type": "string", 184 | "pattern": "^(addon_config|all_addon_configs|config|homeassistant_config|ssl|addons|backup|share|media)(:(rw|ro))?$" 185 | } 186 | }, 187 | { 188 | "items": { 189 | "type": "object", 190 | "properties": { 191 | "type": { 192 | "type": "string", 193 | "enum": [ 194 | "addon_config", 195 | "addons", 196 | "all_addon_configs", 197 | "backup", 198 | "config", 199 | "data", 200 | "homeassistant_config", 201 | "media", 202 | "share", 203 | "ssl" 204 | ] 205 | }, 206 | "read_only": { 207 | "type": "boolean" 208 | }, 209 | "path": { 210 | "type": "string" 211 | } 212 | }, 213 | "required": ["type"], 214 | "additionalProperties": false 215 | } 216 | } 217 | ] 218 | }, 219 | "name": { 220 | "type": "string" 221 | }, 222 | "options": { 223 | "additionalProperties": {}, 224 | "default": {}, 225 | "type": "object" 226 | }, 227 | "panel_admin": { 228 | "default": true, 229 | "type": "boolean" 230 | }, 231 | "panel_icon": { 232 | "default": "mdi:puzzle", 233 | "type": "string" 234 | }, 235 | "panel_title": { 236 | "type": "string" 237 | }, 238 | "ports": { 239 | "additionalProperties": { 240 | "anyOf": [ 241 | { "type": "integer", "minimum": 1, "maximum": 65535 }, 242 | { "type": "null" } 243 | ] 244 | }, 245 | "type": "object" 246 | }, 247 | "ports_description": { 248 | "additionalProperties": { 249 | "type": "string" 250 | }, 251 | "type": "object" 252 | }, 253 | "privileged": { 254 | "items": { 255 | "enum": [ 256 | "BPF", 257 | "DAC_READ_SEARCH", 258 | "IPC_LOCK", 259 | "NET_ADMIN", 260 | "NET_RAW", 261 | "PERFMON", 262 | "SYS_ADMIN", 263 | "SYS_MODULE", 264 | "SYS_NICE", 265 | "SYS_PTRACE", 266 | "SYS_RAWIO", 267 | "SYS_RESOURCE", 268 | "SYS_TIME" 269 | ], 270 | "type": "string" 271 | }, 272 | "type": "array" 273 | }, 274 | "realtime": { 275 | "default": false, 276 | "type": "boolean" 277 | }, 278 | "schema": { 279 | "additionalProperties": {}, 280 | "default": {}, 281 | "type": "object" 282 | }, 283 | "services": { 284 | "items": { 285 | "pattern": "^(?Pmqtt|mysql):(?Pprovide|want|need)$" 286 | }, 287 | "type": "array" 288 | }, 289 | "slug": { 290 | "type": "string" 291 | }, 292 | "stage": { 293 | "default": "stable", 294 | "enum": ["deprecated", "experimental", "stable"], 295 | "type": "string" 296 | }, 297 | "startup": { 298 | "default": "application", 299 | "enum": ["application", "initialize", "once", "services", "system"], 300 | "type": "string" 301 | }, 302 | "stdin": { 303 | "default": false, 304 | "type": "boolean" 305 | }, 306 | "timeout": { 307 | "default": 10, 308 | "type": "integer" 309 | }, 310 | "tmpfs": { 311 | "default": false, 312 | "type": ["string", "boolean"] 313 | }, 314 | "uart": { 315 | "default": false, 316 | "type": "boolean" 317 | }, 318 | "udev": { 319 | "default": false, 320 | "type": "boolean" 321 | }, 322 | "url": { 323 | "format": "uri-reference", 324 | "type": "string" 325 | }, 326 | "usb": { 327 | "default": false, 328 | "type": "boolean" 329 | }, 330 | "version": { 331 | "type": "string" 332 | }, 333 | "video": { 334 | "default": false, 335 | "type": "boolean" 336 | }, 337 | "watchdog": { 338 | "type": "string" 339 | }, 340 | "webui": { 341 | "type": "string" 342 | } 343 | }, 344 | "required": ["arch", "description", "name", "slug", "version"], 345 | "type": "object" 346 | } 347 | -------------------------------------------------------------------------------- /src/lint.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from jsonschema import Draft7Validator, ValidationError, validators 7 | import yaml 8 | 9 | 10 | def check_is_default(validator_class): 11 | """Check if a JSON property is using its default value.""" 12 | validate_properties = validator_class.VALIDATORS["properties"] 13 | 14 | def is_default(validator, properties, instance, schema): 15 | for property, subschema in properties.items(): 16 | if "default" in subschema: 17 | if instance.get(property) == subschema["default"]: 18 | yield ValidationError( 19 | f"'{property}' should be removed, it uses a default value" 20 | ) 21 | 22 | for error in validate_properties( 23 | validator, 24 | properties, 25 | instance, 26 | schema, 27 | ): 28 | yield error 29 | 30 | return validators.extend( 31 | validator_class, 32 | {"properties": is_default}, 33 | ) 34 | 35 | 36 | path = Path(os.environ["INPUT_PATH"]) 37 | if not path.exists(): 38 | print(f"::error ::Add-on configuration path not found: {path}") 39 | sys.exit(1) 40 | 41 | for file_type in ("json", "yaml", "yml"): 42 | config = path / f"config.{file_type}" 43 | if config.exists(): 44 | break 45 | 46 | if not config.exists(): 47 | print(f"::error ::Add-on configuration file not found in '{path}'") 48 | sys.exit(1) 49 | 50 | 51 | with open(config) as fp: 52 | if config.suffix == "json": 53 | configuration = json.load(fp) 54 | else: 55 | configuration = yaml.load(fp, Loader=yaml.SafeLoader) 56 | 57 | with open("/config.schema.json") as fp: 58 | schema = json.load(fp) 59 | 60 | DefaultValidatingDraft7Validator = check_is_default(Draft7Validator) 61 | v = DefaultValidatingDraft7Validator(schema) 62 | 63 | exit_code = 0 64 | 65 | for error in sorted(v.iter_errors(configuration), key=str): 66 | print(f"::error file={config}::{error.message}") 67 | exit_code = 1 68 | 69 | if configuration.get("ingress", False): 70 | if configuration.get("webui"): 71 | print(f"::error file={config}::'webui' should be removed, Ingress is enabled.") 72 | exit_code = 1 73 | 74 | if ( 75 | not configuration.get("host_network", False) 76 | and configuration.get("ingress_port", 8099) == 0 77 | ): 78 | print( 79 | f"::error file={config}::'ingress_port' this does not run on the host network. " 80 | "In Ingress port doesn't have to be randomized (not 0)." 81 | ) 82 | exit_code = 1 83 | 84 | if configuration.get("full_access") and any( 85 | item in ["devices", "gpio", "uart", "usb"] for item in configuration 86 | ): 87 | print( 88 | f"::error file={config}::'full_access', don't add 'devices', 'uart', 'usb' or 'gpio' this is not needed" 89 | ) 90 | exit_code = 1 91 | 92 | if configuration.get("full_access"): 93 | print( 94 | f"::warning file={config}::'full_access' consider using other options instead, like 'devices'" 95 | ) 96 | 97 | if "auto_uart" in configuration: 98 | print(f"::error file={config}::'auto_uart' is deprecated, use 'uart' instead.") 99 | exit_code = 1 100 | 101 | if any(":" in line for line in configuration.get("devices", [])): 102 | print( 103 | f"::error file={config}::'devices' uses a deprecated format, the new format uses a list of paths only." 104 | ) 105 | exit_code = 1 106 | 107 | if not isinstance(configuration.get("tmpfs", False), bool): 108 | print( 109 | f"::error file={config}::'tmpfs' use a deprecated format, it is a boolean now." 110 | ) 111 | exit_code = 1 112 | 113 | if configuration.get("backup", "hot") == "cold": 114 | for option in ["backup_pre", "backup_post"]: 115 | if option in configuration: 116 | print( 117 | f"::error file={config}::'{option}' is not valid when using cold backups." 118 | ) 119 | exit_code = 1 120 | 121 | if configuration.get("watchdog"): 122 | print( 123 | f"::error file={config}::'watchdog', is obsolete. Use the native Docker HEALTHCHECK directive instead." 124 | ) 125 | exit_code = 1 126 | 127 | if configuration.get("map") and ( 128 | "config" in configuration["map"] 129 | or "config:rw" in configuration["map"] 130 | or "config:ro" in configuration["map"] 131 | ): 132 | print( 133 | f"::warning file={config}::'map' contains the 'config' folder, which has been replaced by 'homeassistant_config'. See: https://developers.home-assistant.io/blog/2023/11/06/public-addon-config" 134 | ) 135 | 136 | if ( 137 | configuration.get("map") 138 | and ( 139 | "config" in configuration["map"] 140 | or "config:rw" in configuration["map"] 141 | or "config:ro" in configuration["map"] 142 | ) 143 | and ( 144 | "homeassistant_config" in configuration["map"] 145 | or "homeassistant_config:rw" in configuration["map"] 146 | or "homeassistant_config:ro" in configuration["map"] 147 | ) 148 | ): 149 | print( 150 | f"::error file={config}::'map' contains both the 'config' and 'homeassistant_config' folder, which are conflicting. See: https://developers.home-assistant.io/blog/2023/11/06/public-addon-config" 151 | ) 152 | exit_code = 1 153 | 154 | if ( 155 | configuration.get("map") 156 | and ( 157 | "config" in configuration["map"] 158 | or "config:rw" in configuration["map"] 159 | or "config:ro" in configuration["map"] 160 | ) 161 | and ( 162 | "addon_config" in configuration["map"] 163 | or "addon_config:rw" in configuration["map"] 164 | or "addon_config:ro" in configuration["map"] 165 | ) 166 | ): 167 | print( 168 | f"::error file={config}::'map' contains both the 'config' and 'addon_config' folder, which are conflicting. See: https://developers.home-assistant.io/blog/2023/11/06/public-addon-config" 169 | ) 170 | exit_code = 1 171 | 172 | # Checks regarding build file(if found) 173 | for file_type in ("json", "yaml", "yml"): 174 | build = path / f"build.{file_type}" 175 | if build.exists(): 176 | break 177 | 178 | if build.exists(): 179 | with open(build) as fp: 180 | if build.suffix == "json": 181 | build_configuration = json.load(fp) 182 | else: 183 | build_configuration = yaml.load(fp, Loader=yaml.SafeLoader) 184 | 185 | with open("/build.schema.json") as fp: 186 | build_schema = json.load(fp) 187 | 188 | v = DefaultValidatingDraft7Validator(build_schema) 189 | 190 | for error in sorted(v.iter_errors(build_configuration), key=str): 191 | print(f"::error file={build}::{error.message}") 192 | exit_code = 1 193 | 194 | # Start of additional community checks 195 | if os.environ["INPUT_COMMUNITY"] != "true": 196 | sys.exit(exit_code) 197 | 198 | if configuration["version"] != "dev": 199 | print(f"::error file={config}::Add-on version identifier must be 'dev'") 200 | exit_code = 1 201 | 202 | if not build.exists(): 203 | print(f"::error file={build}::The build.json file is missing") 204 | sys.exit(1) 205 | 206 | if ( 207 | "build_from" in build_configuration 208 | and not isinstance(build_configuration["build_from"], str) 209 | and set(configuration["arch"]) != set(build_configuration["build_from"]) 210 | ): 211 | print(f"::error file={build}::Architectures in config and build do not match") 212 | exit_code = 1 213 | 214 | # All good things, come to an end \o/! 215 | sys.exit(exit_code) 216 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema==4.24.0 2 | pyyaml==6.0.2 3 | --------------------------------------------------------------------------------