From 345e97a8342e0b96de0f8bab8b8a63018618f7f0 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sat, 14 Feb 2026 09:48:27 +1000 Subject: [PATCH 1/6] renaming to vue template, tweaking styling --- biome.json | 30 +++++++++++++ github_linter/web/__init__.py | 2 +- github_linter/web/css/github-linter.css | 19 ++++++++ github_linter/web/templates/basetemplate.html | 44 ++++++++----------- .../web/templates/{index.html => index.vue} | 3 ++ 5 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 biome.json create mode 100644 github_linter/web/css/github-linter.css rename github_linter/web/templates/{index.html => index.vue} (98%) diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..91de1c88 --- /dev/null +++ b/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "javascript": { + "formatter": { + "lineWidth": 80 + } + }, + "overrides": [ + { + "includes": ["*.vue"], + "linter": { + "enabled": true + }, + "formatter": { + "enabled": true + } + } + ] +} diff --git a/github_linter/web/__init__.py b/github_linter/web/__init__.py index cfd60a10..a5c60c42 100644 --- a/github_linter/web/__init__.py +++ b/github_linter/web/__init__.py @@ -400,6 +400,6 @@ async def root( ), autoescape=select_autoescape(), ) - template = env.get_template("index.html") + template = env.get_template("index.vue") return HTMLResponse(template.render()) diff --git a/github_linter/web/css/github-linter.css b/github_linter/web/css/github-linter.css new file mode 100644 index 00000000..7ed60b95 --- /dev/null +++ b/github_linter/web/css/github-linter.css @@ -0,0 +1,19 @@ +[data-theme="light"], +:root:not([data-theme="dark"]) { + --primary: #8e24aa; + --primary-hover: #5f1972; +} + +main { + margin-top: 0.8rem; +} +h1 { + margin-bottom: 1rem; +} + +.buttonbar { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + justify-content: space-between; +} diff --git a/github_linter/web/templates/basetemplate.html b/github_linter/web/templates/basetemplate.html index 7613a215..ab0e0255 100644 --- a/github_linter/web/templates/basetemplate.html +++ b/github_linter/web/templates/basetemplate.html @@ -1,32 +1,24 @@ - - Github Linter + + Github Linter + - - - - -
-
- {% block content %} - {% endblock content %} -
- - - - - - - - + +
+
+ {% block content %} + {% endblock content %} +
+ + + + +
+ + \ No newline at end of file diff --git a/github_linter/web/templates/index.html b/github_linter/web/templates/index.vue similarity index 98% rename from github_linter/web/templates/index.html rename to github_linter/web/templates/index.vue index 93f0ac6a..a0dc281d 100644 --- a/github_linter/web/templates/index.html +++ b/github_linter/web/templates/index.vue @@ -7,10 +7,13 @@

Repositories (|totalRepos|)

+ + From f1192eba1021e9cb7235412e0dc5efb610dc556a Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sat, 14 Feb 2026 09:48:56 +1000 Subject: [PATCH 2/6] fmt javascript, remove commented code, change defaults to hide archived and show only PRs --- github_linter/web/github_linter.js | 222 ++++++++++++++--------------- 1 file changed, 108 insertions(+), 114 deletions(-) diff --git a/github_linter/web/github_linter.js b/github_linter/web/github_linter.js index 14c7fc1d..907fe0f3 100644 --- a/github_linter/web/github_linter.js +++ b/github_linter/web/github_linter.js @@ -1,120 +1,114 @@ // axios get from here: https://reactgo.com/vue-fetch-data/ -// Vue.component('repo-item', { -// props: ['repo'], -// template: '' -// }) - const _repo_app = new Vue({ - delimiters: ["|", "|"], // because we're using it alongside jinja2 - el: "#repos", - data: { - repos: [{ full_name: "Loading" }], - repo_filter: "", - hide_archived: false, - show_has_issues: true, - show_has_prs: false, - last_updated: null, - waiting_for_update: false, // used when waiting for repos to update - // total_issues: -1, - }, - created() { - this.updateRepos(); - this.getLastUpdated(); - this.timer = setInterval(this.getLastUpdated, 5000); - this.timer = setInterval(this.updateRepos, 5000); - this.timer = setInterval(this.checkIsWaiting, 1000); - }, - computed: { - filteredRows() { - return this.repos.filter((repo) => { - const full_name = repo.full_name.toString().toLowerCase(); - let owner = ""; - if (typeof repo.owner !== "undefined" && repo.owner != null) { - owner = repo.owner.toString().toLowerCase(); - } - // const department = repo.department.toLowerCase(); - const searchTerm = this.repo_filter.toLowerCase(); - let result = - full_name.includes(searchTerm) || owner.includes(searchTerm); + delimiters: ["|", "|"], // because we're using it alongside jinja2 + el: "#repos", + data: { + repos: [{ full_name: "Loading" }], + repo_filter: "", + hide_archived: true, + show_has_issues: true, + show_has_prs: true, + last_updated: null, + waiting_for_update: false, // used when waiting for repos to update + }, + created() { + this.updateRepos(); + this.getLastUpdated(); + this.timer = setInterval(this.getLastUpdated, 5000); + this.timer = setInterval(this.updateRepos, 5000); + this.timer = setInterval(this.checkIsWaiting, 1000); + }, + computed: { + filteredRows() { + return this.repos.filter((repo) => { + const full_name = repo.full_name.toString().toLowerCase(); + let owner = ""; + if (typeof repo.owner !== "undefined" && repo.owner != null) { + owner = repo.owner.toString().toLowerCase(); + } + // const department = repo.department.toLowerCase(); + const searchTerm = this.repo_filter.toLowerCase(); + let result = + full_name.includes(searchTerm) || owner.includes(searchTerm); - // filter for show_has_issues - if (this.show_has_issues && result) { - if (repo.open_issues === 0) { - result = false; - // console.log("hiding " + repo.full_name + " because it doesn't have issues"); - } - } - // filter for show_has_prs - if (this.show_has_prs && result) { - if (repo.open_prs === 0) { - result = false; - } else { - result = true; - } - } - // filter for hide_archived - if (this.hide_archived && result) { - if (repo.archived === true) { - // console.log("skipping archived" + repo.full_name); - result = false; - } - } + // filter for show_has_issues + if (this.show_has_issues && result) { + if (repo.open_issues === 0) { + result = false; + // console.log("hiding " + repo.full_name + " because it doesn't have issues"); + } + } + // filter for show_has_prs + if (this.show_has_prs && result) { + if (repo.open_prs === 0) { + result = false; + } else { + result = true; + } + } + // filter for hide_archived + if (this.hide_archived && result) { + if (repo.archived === true) { + // console.log("skipping archived" + repo.full_name); + result = false; + } + } - return result; - }); - }, - totalRepos() { - // this calculates the current view's number of total open issues - return this.filteredRows.length; - }, - totalFilteredOpenIssues() { - // this calculates the current view's number of total open issues - tmp_count = 0; - for (const issue of this.filteredRows) { - tmp_count += issue.open_issues; - // console.log("issues: "+issue.open_issues) - } - return tmp_count; - }, - totalFilteredPRs() { - // this calculates the current view's number of total open issues - tmp_count = 0; - for (const issue of this.filteredRows) { - tmp_count += issue.open_prs; - } - return tmp_count; - }, - }, - methods: { - updateRepos: function () { - const headers = { crossDomain: true }; - axios.get("/repos", headers).then((res) => { - this.repos = res.data; - }); - // TODO: maybe block the update repos button for 30 seconds? - }, - getLastUpdated: function () { - // get the "last updated" data - axios.get("/db/updated").then((res) => { - if (res.data > this.last_updated) { - this.waiting_for_update = false; - this.last_updated = res.data; - } else { - console.log( - `result: ${res.data} less than or equal to ${this.last_updated}`, - ); - } - }); - }, - updateReposBackend: function () { - this.waiting_for_update = true; - axios.get("/repos/update"); - }, - checkIsWaiting: function () { - axios.get("/db/updating").then((res) => { - this.waiting_for_update = res.data; - }); - }, - }, + return result; + }); + }, + totalRepos() { + // this calculates the current view's number of total open issues + return this.filteredRows.length; + }, + totalFilteredOpenIssues() { + // this calculates the current view's number of total open issues + tmp_count = 0; + for (const issue of this.filteredRows) { + tmp_count += issue.open_issues; + // console.log("issues: "+issue.open_issues) + } + return tmp_count; + }, + totalFilteredPRs() { + // this calculates the current view's number of total open issues + tmp_count = 0; + for (const issue of this.filteredRows) { + tmp_count += issue.open_prs; + } + return tmp_count; + }, + }, + methods: { + updateRepos: function () { + const headers = { crossDomain: true }; + axios.get("/repos", headers).then((res) => { + this.repos = res.data; + }); + // TODO: maybe block the update repos button for 30 seconds? + }, + getLastUpdated: function () { + // get the "last updated" data + axios.get("/db/updated").then((res) => { + if (res.data > this.last_updated) { + this.waiting_for_update = false; + this.last_updated = res.data; + } else { + console.log( + `result: ${res.data} less than or equal to ${this.last_updated}`, + ); + } + }); + }, + updateReposBackend: function () { + this.waiting_for_update = true; + axios.get("/repos/update"); + }, + checkIsWaiting: function () { + axios.get("/db/updating").then((res) => { + this.waiting_for_update = res.data; + }); + }, + }, }); From 78dc82ae66860a0087e4c509111fed617686fcd7 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sat, 14 Feb 2026 10:08:49 +1000 Subject: [PATCH 3/6] nicer sh --- run_web.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/run_web.sh b/run_web.sh index 2cd2908d..2418c319 100755 --- a/run_web.sh +++ b/run_web.sh @@ -1,3 +1,6 @@ #!/bin/bash -LOG_LEVEL=DEBUG uvicorn github_linter.web:app --reload-dir ./github_linter --debug +LOG_LEVEL=DEBUG \ + uvicorn \ + github_linter.web:app \ + --reload-dir ./github_linter From 6d0d352686421153e935c6465d4f98986e5fa9d2 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sat, 14 Feb 2026 10:48:12 +1000 Subject: [PATCH 4/6] cleaning up display, cutting repo data response by 50%, removing pylint noise --- .github/workflows/pylint.yml | 2 +- CLAUDE.md => AGENTS.md | 0 README.md | 6 ++ github_linter/__init__.py | 2 +- github_linter/__main__.py | 1 - .../fixes/github_actions/__init__.py | 6 +- github_linter/loaders.py | 6 +- github_linter/repolinter.py | 1 - github_linter/tests/__init__.py | 1 - github_linter/tests/dependabot/__init__.py | 1 - github_linter/tests/dependabot/types.py | 27 ++----- github_linter/tests/dependabot/utils.py | 2 +- github_linter/tests/generic.py | 1 - github_linter/tests/issues.py | 2 - github_linter/tests/pyproject.py | 5 +- github_linter/utils/pages.py | 14 ++-- github_linter/web/__init__.py | 35 ++++++--- github_linter/web/css/github-linter.css | 11 +++ github_linter/web/github_linter.js | 8 +- github_linter/web/images/open-black.svg | 11 +++ github_linter/web/images/open-white.svg | 11 +++ github_linter/web/templates/index.vue | 22 +++--- pyproject.toml | 11 +-- tests/test_pyproject.py | 4 +- uv.lock | 74 ------------------- 25 files changed, 105 insertions(+), 159 deletions(-) rename CLAUDE.md => AGENTS.md (100%) create mode 100644 github_linter/web/images/open-black.svg create mode 100644 github_linter/web/images/open-white.svg diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 7e1cc3a2..5ef288d3 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -8,7 +8,7 @@ name: Python Linting pull_request: jobs: - pylint: + linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/CLAUDE.md b/AGENTS.md similarity index 100% rename from CLAUDE.md rename to AGENTS.md diff --git a/README.md b/README.md index 236e39d2..aa22ab9a 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,9 @@ docker run --rm -it \ ghcr.io/yaleman/github_linter:latest \ python -m github_linter.web ``` + +## Thanks + +* [Vue.js](http://vuejs.org) used in the UI +* [Pico](https://picocss.com) CSS framework. + \ No newline at end of file diff --git a/github_linter/__init__.py b/github_linter/__init__.py index 8f89a575..f5e0585e 100644 --- a/github_linter/__init__.py +++ b/github_linter/__init__.py @@ -282,7 +282,7 @@ def filter_by_repo(repo_list: List[Repository], repo_filters: List[str]) -> List return retval -class RepoSearchString(pydantic.BaseModel): # pylint: disable=no-member +class RepoSearchString(pydantic.BaseModel): """Result of running generate_repo_search_string""" needs_post_filtering: bool diff --git a/github_linter/__main__.py b/github_linter/__main__.py index 468b4614..05e50bdb 100644 --- a/github_linter/__main__.py +++ b/github_linter/__main__.py @@ -33,7 +33,6 @@ @click.option("--check", "-k", multiple=True, help="Filter by check name, eg check_example") @click.option("--list-repos", is_flag=True, default=False, help="List repos and exit") @click.option("--debug", "-d", is_flag=True, default=False, help="Enable debug logging") -# pylint: disable=too-many-arguments,too-many-locals def cli( repo: Optional[Tuple[str]] = None, owner: Optional[Tuple[str]] = None, diff --git a/github_linter/fixes/github_actions/__init__.py b/github_linter/fixes/github_actions/__init__.py index a37c6cb3..9a59e322 100644 --- a/github_linter/fixes/github_actions/__init__.py +++ b/github_linter/fixes/github_actions/__init__.py @@ -30,7 +30,6 @@ def get_repo_default_workflow_permissions( # https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-default-workflow-permissions-for-a-repository - # pylint: disable=protected-access resp = repo.repository3._get( f"https://api.github.com/repos/{repo.repository3.owner}/{repo.repository3.name}/actions/permissions/workflow", ) @@ -51,16 +50,13 @@ def set_repo_default_workflow_permissions( # https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#set-default-workflow-permissions-for-a-repository if default_workflow_permissions not in VALID_DEFAULT_WORKFLOW_PERMISSIONS: - raise ValueError( - f"Invalid default_workflow_permissions: {default_workflow_permissions}. Valid values are: {VALID_DEFAULT_WORKFLOW_PERMISSIONS}" - ) + raise ValueError(f"Invalid default_workflow_permissions: {default_workflow_permissions}. Valid values are: {VALID_DEFAULT_WORKFLOW_PERMISSIONS}") payload = { "default_workflow_permissions": default_workflow_permissions, "can_approve_pull_request_reviews": can_approve_pull_request_reviews, } - # pylint: disable=protected-access res: Response = repo.repository3._put( f"https://api.github.com/repos/{repo.repository3.owner}/{repo.repository3.name}/actions/permissions/workflow", data=json.dumps(payload), diff --git a/github_linter/loaders.py b/github_linter/loaders.py index 8eecd890..0b219034 100644 --- a/github_linter/loaders.py +++ b/github_linter/loaders.py @@ -18,11 +18,9 @@ def load_yaml_file( if not fileresult: return {} try: - filecontents: Dict[Any, Any] = YAML(pure=True).load( - fileresult.decoded_content.decode("utf-8") - ) + filecontents: Dict[Any, Any] = YAML(pure=True).load(fileresult.decoded_content.decode("utf-8")) return filecontents - except Exception as error_message: # pylint: disable=broad-except + except Exception as error_message: logger.error("Failed to parse yaml file {}: {}", filename, error_message) # TODO: Catch a better exception in loaders.load_yaml_file return None diff --git a/github_linter/repolinter.py b/github_linter/repolinter.py index 9d203a5f..e061dfed 100644 --- a/github_linter/repolinter.py +++ b/github_linter/repolinter.py @@ -136,7 +136,6 @@ def cached_get_file(self, filepath: str, clear_cache: bool = False) -> Optional[ return None return self.filecache[filepath] - # pylint: disable=too-many-branches def create_or_update_file( self, filepath: str, diff --git a/github_linter/tests/__init__.py b/github_linter/tests/__init__.py index 28608b6a..c848c92e 100644 --- a/github_linter/tests/__init__.py +++ b/github_linter/tests/__init__.py @@ -17,7 +17,6 @@ homebrew, # noqa: F401 issues, # noqa: F401 mkdocs, # noqa: F401 - # pylintrc, pyproject, # noqa: F401 security_md, # noqa: F401 terraform, # noqa: F401 diff --git a/github_linter/tests/dependabot/__init__.py b/github_linter/tests/dependabot/__init__.py index c9921bd9..a406ace5 100644 --- a/github_linter/tests/dependabot/__init__.py +++ b/github_linter/tests/dependabot/__init__.py @@ -104,7 +104,6 @@ def generate_expected_update_config( return config_file -# pylint: disable=too-many-branches def check_updates_for_languages(repo: RepoLinter) -> None: """ensures that for every known language/package ecosystem, there's a configured update task""" diff --git a/github_linter/tests/dependabot/types.py b/github_linter/tests/dependabot/types.py index 9f3eed8d..705a4060 100644 --- a/github_linter/tests/dependabot/types.py +++ b/github_linter/tests/dependabot/types.py @@ -1,4 +1,4 @@ -""" types for gihtub_linter dependabot tests """ +"""types for gihtub_linter dependabot tests""" from typing import Any, Dict, List, Optional, TypedDict, Union @@ -32,9 +32,7 @@ def validate_interval(cls, value: str) -> str: # TODO: write tests for this @pydantic.field_validator("timezone") - def validate_timezone( - cls, value: Optional[DoubleQuotedScalarString] - ) -> Optional[DoubleQuotedScalarString]: + def validate_timezone(cls, value: Optional[DoubleQuotedScalarString]) -> Optional[DoubleQuotedScalarString]: """validator""" if value not in pytz.all_timezones: raise ValueError(f"Invalid timezone: {value}") @@ -42,9 +40,7 @@ def validate_timezone( # TODO: write tests for this @pydantic.field_validator("time") - def validate_time( - cls, value: Optional[DoubleQuotedScalarString] - ) -> Optional[DoubleQuotedScalarString]: + def validate_time(cls, value: Optional[DoubleQuotedScalarString]) -> Optional[DoubleQuotedScalarString]: """validator""" if value is not None: return DoubleQuotedScalarString(value) @@ -122,9 +118,7 @@ class DependabotUpdateConfig(pydantic.BaseModel): None # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#allow ) assignees: Optional[List[str]] = None - commit_message: Optional[DependabotCommitMessage] = pydantic.Field( - None, alias="commit-message" - ) + commit_message: Optional[DependabotCommitMessage] = pydantic.Field(None, alias="commit-message") ignore: Optional[List[str]] = None insecure_external_code_execution: Optional[str] = pydantic.Field( alias="insecure-external-code-execution", @@ -136,20 +130,15 @@ class DependabotUpdateConfig(pydantic.BaseModel): alias="open-pull-requests-limit", default=None, ) - # noqa: E501 pylint: disable=line-too-long # TODO: this needs to be a thing - https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#pull-request-branch-nameseparator # pull-request-branch-name.separator - rebase_strategy: Optional[str] = pydantic.Field( - alias="rebase-strategy", default=None - ) + rebase_strategy: Optional[str] = pydantic.Field(alias="rebase-strategy", default=None) # TODO: registries typing for DependabotUpdateConfig registries: Optional[Any] = None reviewers: Optional[List[str]] = None target_branch: Optional[str] = pydantic.Field(alias="target-branch", default=None) vendor: Optional[bool] = None - versioning_strategy: Optional[str] = pydantic.Field( - alias="versioning-strategy", default=None - ) + versioning_strategy: Optional[str] = pydantic.Field(alias="versioning-strategy", default=None) # TODO: write tests for this @pydantic.field_validator("package_ecosystem") @@ -172,9 +161,7 @@ def validate_rebase_strategy(cls, value: str) -> str: def validate_execution_permissions(cls, value: str) -> str: """validates you're getting the right value""" if value not in ["deny", "allow"]: - raise ValueError( - "insecure-external-code-execution needs to be either 'allow' or 'deny'." - ) + raise ValueError("insecure-external-code-execution needs to be either 'allow' or 'deny'.") return value diff --git a/github_linter/tests/dependabot/utils.py b/github_linter/tests/dependabot/utils.py index 401a24fd..c6921bf0 100644 --- a/github_linter/tests/dependabot/utils.py +++ b/github_linter/tests/dependabot/utils.py @@ -53,7 +53,7 @@ def load_dependabot_config_file( for update in retval.updates: logger.debug("Package: {}", update.package_ecosystem) return retval - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: logger.error("Failed to parse dependabot config: {}", exc) repo.error(category, f"Failed to parse dependabot config: {exc}") return None diff --git a/github_linter/tests/generic.py b/github_linter/tests/generic.py index b2a7ee69..fb756bcc 100644 --- a/github_linter/tests/generic.py +++ b/github_linter/tests/generic.py @@ -92,7 +92,6 @@ def generate_funding_file(input_data: FundingDict) -> str: yaml.dump(output_data, outputio) outputio.seek(0) - # pylint: disable=line-too-long # doc_line = "# Documentation for this file format is here: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository" result = outputio.read() return result diff --git a/github_linter/tests/issues.py b/github_linter/tests/issues.py index 28e45568..c77899a8 100644 --- a/github_linter/tests/issues.py +++ b/github_linter/tests/issues.py @@ -25,7 +25,6 @@ class DefaultConfig(TypedDict): } -# pylint: disable=unused-argument def check_open_issues( repo: RepoLinter, ) -> None: @@ -37,7 +36,6 @@ def check_open_issues( ) -# pylint: disable=unused-argument def check_open_prs( repo: RepoLinter, ) -> None: diff --git a/github_linter/tests/pyproject.py b/github_linter/tests/pyproject.py index 8136185d..98e68738 100644 --- a/github_linter/tests/pyproject.py +++ b/github_linter/tests/pyproject.py @@ -29,8 +29,8 @@ DEFAULT_CONFIG: DefaultConfig = { # TODO: FIX THIS TO USE UV/HATCHLING "build-system": [ - # "flit_core.buildapi", # flit - # "poetry.core.masonry.api", # poetry + # "flit_core.buildapi", # flit + # "poetry.core.masonry.api", # poetry ], "readme": "README.md", } @@ -55,7 +55,6 @@ def validate_pyproject_authors( repo.warning(CATEGORY, f"Check author is expected: {author}") -# pylint: disable=unused-argument def validate_project_name( repo: RepoLinter, project_object: Dict[str, Any], diff --git a/github_linter/utils/pages.py b/github_linter/utils/pages.py index 337d5f12..ae93cf3b 100644 --- a/github_linter/utils/pages.py +++ b/github_linter/utils/pages.py @@ -40,19 +40,17 @@ def get_repo_pages_data(repo: RepoLinter) -> PagesData: github = GithubLinter() github.do_login() url = f"/repos/{repo.repository.full_name}/pages" - # pylint: disable=protected-access - pagesdata = github.github._Github__requester.requestJson(verb="GET", url=url) # type: ignore + if hasattr(github.github, "_Github__requester") and github.github._Github__requester is not None: + pagesdata = github.github._Github__requester.requestJson(verb="GET", url=url) # type: ignore[possibly-missing-attribute] + else: + raise ValueError("Github object doesn't have a requester, can't get pages data.") if len(pagesdata) != 3: - raise ValueError( - f"Got {len(pagesdata)} from requesting the repo pages endpoint ({url})." - ) + raise ValueError(f"Got {len(pagesdata)} from requesting the repo pages endpoint ({url}).") pages: PagesData = json.loads(pagesdata[2]) if pages is None: - raise ValueError( - f"Invalid data returned from requesting the repo pages endpoint ({url})." - ) + raise ValueError(f"Invalid data returned from requesting the repo pages endpoint ({url}).") logger.debug( json.dumps( diff --git a/github_linter/web/__init__.py b/github_linter/web/__init__.py index a5c60c42..a37904db 100644 --- a/github_linter/web/__init__.py +++ b/github_linter/web/__init__.py @@ -36,7 +36,6 @@ app = FastAPI() -# pylint: disable=too-few-public-methods class SQLRepos(Base): """sqlrepos""" @@ -56,7 +55,6 @@ class SQLRepos(Base): parent = sqlalchemy.Column(sqlalchemy.String(255), nullable=True) -# pylint: disable=too-few-public-methods class SQLMetadata(Base): """metadata""" @@ -77,7 +75,6 @@ async def create_db() -> None: """do the initial DB creation""" async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) - # logger.info("Result of creating DB: {}", result) async def get_async_session() -> AsyncGenerator[AsyncSession, None]: @@ -105,6 +102,19 @@ class RepoData(BaseModel): model_config = ConfigDict(from_attributes=True) +class RepoDataSimple(BaseModel): + """because the full data is chonky, cuts it by 50%""" + + full_name: str + archived: bool + fork: bool + open_issues: int + open_prs: int + last_updated: float + + model_config = ConfigDict(from_attributes=True, extra="ignore") + + def githublinter_factory() -> Generator[GithubLinter, None, None]: """githublinter factory""" githublinter = GithubLinter() @@ -216,6 +226,15 @@ async def favicon() -> Union[Response, FileResponse]: return Response(status_code=404) +@app.get("/images/{filename}", response_model=None) +async def images(filename: str) -> Union[Response, FileResponse]: + """return an image""" + icon_file = Path(Path(__file__).resolve().parent.as_posix() + f"/images/{filename}") + if icon_file.exists(): + return FileResponse(icon_file) + return Response(status_code=404) + + @app.get("/css/{filename:str}", response_model=None) async def css_file(filename: str) -> Union[Response, FileResponse]: """css returner""" @@ -263,13 +282,11 @@ async def db_update_running() -> bool: ) await set_db_update_running(False) return False - # pylint: disable=broad-except except Exception as error_message: logger.warning(f"Failed to pull update_running: {error_message}") try: await set_db_update_running(False) logger.success("Set it to False instead") - # pylint: disable=broad-except except Exception as error: logger.error( "Tried to set update_running to False but THAT went wrong too! {}", @@ -320,14 +337,12 @@ async def db_updated() -> int: data = MetaData.model_validate(row) if "." in data.value: return int(data.value.split(".")[0]) - # pylint: disable=broad-except except Exception as error_message: logger.warning(f"Failed to pull last_updated: {error_message}") try: await set_update_time(-1, conn) await conn.commit() logger.success("Set it to -1 instead") - # pylint: disable=broad-except except Exception as error: logger.error("Tried to set it to -1 but THAT went wrong too! {}", error) return -1 @@ -371,13 +386,13 @@ async def get_health( @app.get("/repos") async def get_repos( session: AsyncSession = Depends(get_async_session), -) -> List[RepoData]: +) -> List[RepoDataSimple]: """endpoint to provide the cached repo list""" try: stmt = sqlalchemy.select(SQLRepos) result = await session.execute(stmt) - retval = [RepoData.model_validate(element.SQLRepos) for element in result.fetchall()] + retval = [RepoDataSimple.model_validate(element.SQLRepos) for element in result.fetchall()] except OperationalError as operational_error: logger.warning("Failed to pull repos from DB: {}", operational_error) return [] @@ -389,7 +404,7 @@ async def root( background_tasks: BackgroundTasks, ) -> Union[Response, HTMLResponse]: """homepage""" - logger.info("Creating background task to create DB.") + logger.debug("Creating background task to create DB.") background_tasks.add_task( create_db, ) diff --git a/github_linter/web/css/github-linter.css b/github_linter/web/css/github-linter.css index 7ed60b95..0fe8b6c1 100644 --- a/github_linter/web/css/github-linter.css +++ b/github_linter/web/css/github-linter.css @@ -17,3 +17,14 @@ h1 { margin-bottom: 1rem; justify-content: space-between; } + +[role="button"] { + width: 20vw; +} + +.open { + background-image: url("/images/open-black.svg"); + width: 1rem; + height: 1rem; + background-repeat: no-repeat; +} diff --git a/github_linter/web/github_linter.js b/github_linter/web/github_linter.js index 907fe0f3..9f25bec4 100644 --- a/github_linter/web/github_linter.js +++ b/github_linter/web/github_linter.js @@ -15,9 +15,11 @@ const _repo_app = new Vue({ created() { this.updateRepos(); this.getLastUpdated(); - this.timer = setInterval(this.getLastUpdated, 5000); - this.timer = setInterval(this.updateRepos, 5000); - this.timer = setInterval(this.checkIsWaiting, 1000); + this.timers = { + lastUpdated: setInterval(this.getLastUpdated, 5000), + updateRepos: setInterval(this.updateRepos, 5000), + checkWaiting: setInterval(this.checkIsWaiting, 1500), + }; }, computed: { filteredRows() { diff --git a/github_linter/web/images/open-black.svg b/github_linter/web/images/open-black.svg new file mode 100644 index 00000000..43ab5c91 --- /dev/null +++ b/github_linter/web/images/open-black.svg @@ -0,0 +1,11 @@ + + + ic_fluent_open_24_filled + + + + + + + + \ No newline at end of file diff --git a/github_linter/web/images/open-white.svg b/github_linter/web/images/open-white.svg new file mode 100644 index 00000000..8fd86071 --- /dev/null +++ b/github_linter/web/images/open-white.svg @@ -0,0 +1,11 @@ + + + ic_fluent_open_24_filled + + + + + + + + \ No newline at end of file diff --git a/github_linter/web/templates/index.vue b/github_linter/web/templates/index.vue index a0dc281d..6319b811 100644 --- a/github_linter/web/templates/index.vue +++ b/github_linter/web/templates/index.vue @@ -9,17 +9,18 @@
{{ repo.full_name }}{{repo.archived }}
- + @@ -27,15 +28,18 @@ - - - + + diff --git a/pyproject.toml b/pyproject.toml index 5645a0e9..cceddd06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,25 +37,16 @@ dependencies = [ Home = "https://github.com/yaleman/github_linter" [project.optional-dependencies] -dev = ["flit", "pytest", "pylint", "mypy", "types-PyYAML", "types-pytz"] +dev = ["flit", "pytest", "mypy", "types-PyYAML", "types-pytz"] [project.scripts] "github-linter" = 'github_linter.__main__:cli' "github-linter-web" = 'github_linter.web.__main__:cli' -[tool.pylint.MASTER] -max-line-length = 200 -disable = "W0511,consider-using-dict-items,duplicate-code" -# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 -extension-pkg-whitelist = "pydantic" -load-plugins = "pylint_pydantic" [tool.ruff] line-length = 200 exclude = ["W0511", "consider-using-dict-items", "duplicate-code"] -# https://github.com/samuelcolvin/pydantic/issues/1961#issuecomment-759522422 -# extension-pkg-whitelist="pydantic" -# load-plugins="pylint_pydantic" [tool.pytest.ini_options] markers = ["network: things which need authentication, '-m \"not network\"'"] diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py index 3698cde7..681cec22 100644 --- a/tests/test_pyproject.py +++ b/tests/test_pyproject.py @@ -1,4 +1,4 @@ -""" testing pyproject """ +"""testing pyproject""" # from io import BytesIO @@ -6,7 +6,6 @@ # from github_linter.tests.pyproject import validate_project_name, validate_readme_configured -# pylint: disable=too-few-public-methods # class TestRepoFoo: # """ just for testing """ # name = "foobar" @@ -19,7 +18,6 @@ # return readme # return BytesIO() -# pylint: disable=too-few-public-methods # class TestGithub: # """ test instance """ # config = { diff --git a/uv.lock b/uv.lock index 85ed5863..7b6d9c6f 100644 --- a/uv.lock +++ b/uv.lock @@ -42,15 +42,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "astroid" -version = "4.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" }, -] - [[package]] name = "bandit" version = "1.9.2" @@ -337,15 +328,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, ] -[[package]] -name = "dill" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -436,7 +418,6 @@ dependencies = [ dev = [ { name = "flit" }, { name = "mypy" }, - { name = "pylint" }, { name = "pytest" }, { name = "types-pytz" }, { name = "types-pyyaml" }, @@ -470,7 +451,6 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'" }, { name = "pydantic", specifier = ">=2.10.5" }, { name = "pygithub", specifier = ">=2.5.0" }, - { name = "pylint", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "python-hcl2", specifier = ">=5.1.1" }, { name = "pytz", specifier = ">=2024.2" }, @@ -611,15 +591,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "isort" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -790,15 +761,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -877,15 +839,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/00/5ac7aa77688ec4d34148b423d34dc0c9bc4febe0d872a9a1ad9860b2f6f1/pip-26.0-py3-none-any.whl", hash = "sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754", size = 1787564, upload-time = "2026-01-31T01:40:52.252Z" }, ] -[[package]] -name = "platformdirs" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1029,24 +982,6 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pylint" -version = "4.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "dill" }, - { name = "isort" }, - { name = "mccabe" }, - { name = "platformdirs" }, - { name = "tomlkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" }, -] - [[package]] name = "pynacl" version = "1.6.2" @@ -1471,15 +1406,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] -[[package]] -name = "tomlkit" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, -] - [[package]] name = "ty" version = "0.0.12" From c1ef5eb52c0eda986002516213d3acd9c8e0a24d Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sat, 14 Feb 2026 10:51:23 +1000 Subject: [PATCH 5/6] fixing up db creation on startup --- github_linter/web/__init__.py | 36 ++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/github_linter/web/__init__.py b/github_linter/web/__init__.py index a37904db..1389739d 100644 --- a/github_linter/web/__init__.py +++ b/github_linter/web/__init__.py @@ -1,5 +1,6 @@ """web interface for the project that outgrew its intention""" +from contextlib import asynccontextmanager from pathlib import Path from time import time from typing import Any, AsyncGenerator, Generator, List, Optional, Union, Tuple @@ -33,7 +34,30 @@ async_session_maker = async_sessionmaker(engine, expire_on_commit=False) Base = declarative_base() -app = FastAPI() + +async def create_db() -> None: + """do the initial DB creation""" + logger.info("Synchronising database on startup...") + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("Done!") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Handle app startup and shutdown""" + # Startup + try: + await create_db() + except Exception as error_message: + logger.critical(f"Failed to create DB, shutting down: {error_message}") + raise + yield + # Shutdown + await engine.dispose() + + +app = FastAPI(lifespan=lifespan) class SQLRepos(Base): @@ -71,12 +95,6 @@ class MetaData(BaseModel): model_config = ConfigDict(from_attributes=True) -async def create_db() -> None: - """do the initial DB creation""" - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - - async def get_async_session() -> AsyncGenerator[AsyncSession, None]: """session factory""" async with async_session_maker() as session: @@ -404,10 +422,6 @@ async def root( background_tasks: BackgroundTasks, ) -> Union[Response, HTMLResponse]: """homepage""" - logger.debug("Creating background task to create DB.") - background_tasks.add_task( - create_db, - ) env = Environment( loader=PackageLoader( package_name="github_linter.web.templates", From aec542732fd985cabe979c9a60fea6537c129c0d Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sat, 14 Feb 2026 11:27:57 +1000 Subject: [PATCH 6/6] formatting, tweaking justfile --- .../workflows/codeql-analysis.yml.disabled | 70 ------------------- .github/workflows/mypy.yml | 4 +- .github/workflows/pylint.yml | 7 +- .github/workflows/pytest.yml | 3 +- AGENTS.md | 17 ++--- Makefile | 43 ------------ biome.json | 8 +++ github_linter/custom_types.py | 2 +- github_linter/defaults.py | 2 +- github_linter/tests/dependabot/constants.py | 2 +- github_linter/tests/docs.py | 5 +- github_linter/tests/mkdocs/__init__.py | 28 ++------ github_linter/tests/terraform.py | 2 +- github_linter/web/__init__.py | 2 + justfile | 41 +++++++++++ tests/test_branch_protection.py | 13 ++-- tests/test_dependabot.py | 2 +- tests/test_generic.py | 10 +-- tests/test_github_data.py | 24 +++++++ tests/test_hcl2.py | 3 +- tests/test_repo_search.py | 11 +-- tests/test_web.py | 23 +----- 22 files changed, 118 insertions(+), 204 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml.disabled delete mode 100644 Makefile create mode 100644 justfile create mode 100644 tests/test_github_data.py diff --git a/.github/workflows/codeql-analysis.yml.disabled b/.github/workflows/codeql-analysis.yml.disabled deleted file mode 100644 index d6616f9d..00000000 --- a/.github/workflows/codeql-analysis.yml.disabled +++ /dev/null @@ -1,70 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '22 4 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 009501bf..2af5e57e 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,11 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 - name: Install uv uses: astral-sh/setup-uv@v7 with: activate-environment: "true" github-token: ${{ github.token }} + - uses: extractions/setup-just@v3 - name: Running type checking run: | - make types + just lint diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 5ef288d3..b135c33d 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -8,15 +8,16 @@ name: Python Linting pull_request: jobs: - linting: + pylint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 - name: Install uv uses: astral-sh/setup-uv@v7 with: activate-environment: "true" github-token: ${{ github.token }} - - name: Running ruff + - name: Running ruff checks run: | - uv run ruff check $(basename $(pwd)) tests + just format diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2eebadf1..cdbb03bf 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: extractions/setup-just@v3 - name: Install uv uses: astral-sh/setup-uv@v7 with: @@ -19,4 +20,4 @@ jobs: github-token: ${{ github.token }} - name: Running pytest run: | - uv run python -m pytest -m 'not network' + just test -m 'not\ network' diff --git a/AGENTS.md b/AGENTS.md index 5e32d88e..0eebe440 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,14 +6,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co github_linter is a Python tool for auditing GitHub repositories at scale. It scans repositories for common configuration issues, missing files, and standardization opportunities across multiple repos. +**MANDATORY** You are not finished with a task until running `just check` passes without warnings or errors. + ## Development Commands ### Testing and Linting -- Run all precommit checks: `make precommit` -- Run linting: `make ruff` -- Run type checking: `make types` -- Run tests: `make test` +- Run all precommit checks: `just check` +- Run linting: `just ruff` +- Run type checking: `just lint` +- Run tests: `just test` - Run single test: `uv run pytest tests/test_.py::` ### Running the CLI @@ -26,13 +28,12 @@ github_linter is a Python tool for auditing GitHub repositories at scale. It sca ### Web Interface -- Start web server: `uv run github-linter-web` -- Or use the script: `./run_web.sh` +- Start web server: `./run_web.sh` ### Docker -- Build container: `make docker_build` -- Run web server in container: `make docker_run` +- Build container: `just docker_build` +- Run web server in container: `just docker_run` ## Architecture diff --git a/Makefile b/Makefile deleted file mode 100644 index 09099f6d..00000000 --- a/Makefile +++ /dev/null @@ -1,43 +0,0 @@ -FULLNAME ?= kanidm/kanidm - - -.DEFAULT: precommit - -.PHONY: precommit -precommit: ruff types test - -.PHONY: jslint -jslint: - find github_linter -name '*.js' -or -name '*.css' -not -name 'pico.min.css' | xargs biome check --json-formatter-enabled=false - -.PHONY: ruff -ruff: - uv run ruff check github_linter tests - -.PHONY: types -types: - uv run ty check - -.PHONY: test -test: - uv run pytest github_linter tests - -.PHONY: docker_build -docker_build: - docker build -t 'ghcr.io/yaleman/github_linter:latest' \ - . - -.PHONY: docker_run -docker_run: docker_build - docker run --rm -it \ - -p 8000:8000 \ - --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ - 'ghcr.io/yaleman/github_linter:latest' \ - python -m github_linter.web - -.PHONY: container/workflow_stats -container/workflow_stats: - docker run --rm -it \ - --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ - 'ghcr.io/yaleman/github_linter:latest' \ - python -m github_linter.workflow_stats -f $(FULLNAME) \ No newline at end of file diff --git a/biome.json b/biome.json index 91de1c88..0f53fa4f 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,13 @@ { "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "files": { + "includes": [ + "github_linter/**/*.js", + "github_linter/**/*.css", + "!!.venv/", + "!!**/pico.min.css" + ] + }, "linter": { "enabled": true, "rules": { diff --git a/github_linter/custom_types.py b/github_linter/custom_types.py index e7809fbd..aab1f954 100644 --- a/github_linter/custom_types.py +++ b/github_linter/custom_types.py @@ -1,4 +1,4 @@ -""" Custom types """ +"""Custom types""" from typing import Dict, List diff --git a/github_linter/defaults.py b/github_linter/defaults.py index 23f4d64c..77fd5329 100644 --- a/github_linter/defaults.py +++ b/github_linter/defaults.py @@ -1,4 +1,4 @@ -""" default linter configuration goes here """ +"""default linter configuration goes here""" from typing import Dict, List, Optional, TypedDict diff --git a/github_linter/tests/dependabot/constants.py b/github_linter/tests/dependabot/constants.py index 0c488eb8..0baeac1e 100644 --- a/github_linter/tests/dependabot/constants.py +++ b/github_linter/tests/dependabot/constants.py @@ -1,4 +1,4 @@ -""" github_linter dependabot tests constants """ +"""github_linter dependabot tests constants""" from typing import Dict, List diff --git a/github_linter/tests/docs.py b/github_linter/tests/docs.py index 3b9bf3b4..4f825e1e 100644 --- a/github_linter/tests/docs.py +++ b/github_linter/tests/docs.py @@ -67,10 +67,7 @@ def fix_contributing_exists(repo: RepoLinter) -> None: oldfile = repo.cached_get_file(filepath) - if ( - oldfile is not None - and oldfile.decoded_content.decode("utf-8") == new_filecontents - ): + if oldfile is not None and oldfile.decoded_content.decode("utf-8") == new_filecontents: logger.debug("Don't need to update {}", filepath) return diff --git a/github_linter/tests/mkdocs/__init__.py b/github_linter/tests/mkdocs/__init__.py index 67f61f8c..cc495eff 100644 --- a/github_linter/tests/mkdocs/__init__.py +++ b/github_linter/tests/mkdocs/__init__.py @@ -40,9 +40,7 @@ def needs_mkdocs_workflow(repo: RepoLinter) -> bool: def check_mkdocs_workflow_exists(repo: RepoLinter) -> None: """checks that the mkdocs github actions workflow exists""" if needs_mkdocs_workflow(repo): - if not repo.cached_get_file( - repo.config[CATEGORY]["workflow_filepath"], clear_cache=True - ): + if not repo.cached_get_file(repo.config[CATEGORY]["workflow_filepath"], clear_cache=True): repo.error(CATEGORY, "MKDocs github actions configuration missing.") # TODO: check if the file differs from expected. @@ -58,9 +56,7 @@ def fix_missing_mkdocs_workflow(repo: RepoLinter) -> None: newfile=get_fix_file_path(CATEGORY, "mkdocs.yml"), message="github-linter.mkdocs created MKDocs github actions configuration", ) - repo.fix( - CATEGORY, f"Created MKDocs github actions configuration: {commit_url}" - ) + repo.fix(CATEGORY, f"Created MKDocs github actions configuration: {commit_url}") else: fix_file = get_fix_file_path(CATEGORY, "mkdocs.yml") @@ -74,9 +70,7 @@ def fix_missing_mkdocs_workflow(repo: RepoLinter) -> None: oldfile=workflow_file, message="github-linter.mkdocs updated MKDocs github actions configuration", ) - repo.fix( - CATEGORY, f"Updated MKDocs github actions configuration: {commit_url}" - ) + repo.fix(CATEGORY, f"Updated MKDocs github actions configuration: {commit_url}") def generate_expected_config(repo: RepoLinter) -> Tuple[str, bytes]: @@ -127,14 +121,10 @@ def check_github_metadata(repo: RepoLinter) -> bool: current_file = repo.cached_get_file(current_filename) if current_file is None: - raise ValueError( - f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?" - ) + raise ValueError(f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?") if current_file.decoded_content == expected_config: - logger.debug( - "Config is up to date, no action required from check_github_metadata" - ) + logger.debug("Config is up to date, no action required from check_github_metadata") return True repo.error(CATEGORY, "mkdocs needs updating for github metadata") @@ -152,13 +142,9 @@ def fix_github_metadata(repo: RepoLinter) -> None: current_file = repo.cached_get_file(current_filename) if current_file is None or current_file.content is None: - raise ValueError( - f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?" - ) + raise ValueError(f"Somehow you got an empty file from {current_filename}, the file may have gone missing while the script was running?") if current_file.decoded_content == expected_config: - logger.debug( - "Config is up to date, no action required from fix_github_metadata" - ) + logger.debug("Config is up to date, no action required from fix_github_metadata") raise NoChangeNeeded # generate a diff for debugging purposes diff --git a/github_linter/tests/terraform.py b/github_linter/tests/terraform.py index 196f9ac9..0832c719 100644 --- a/github_linter/tests/terraform.py +++ b/github_linter/tests/terraform.py @@ -104,7 +104,7 @@ def check_providers_for_modules( found_files.append(filename) - # # make a list of the providers + # make a list of the providers for provider in required_providers: for provider_name in provider: provider_list.append(provider_name) diff --git a/github_linter/web/__init__.py b/github_linter/web/__init__.py index 1389739d..f3e1f947 100644 --- a/github_linter/web/__init__.py +++ b/github_linter/web/__init__.py @@ -7,6 +7,7 @@ from fastapi import BackgroundTasks, FastAPI, Depends, HTTPException from fastapi.responses import HTMLResponse, FileResponse, Response +from fastapi.middleware.gzip import GZipMiddleware from github.Repository import Repository from jinja2 import Environment, PackageLoader, select_autoescape from loguru import logger @@ -58,6 +59,7 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=9) # type: ignore[argument-type] class SQLRepos(Base): diff --git a/justfile b/justfile new file mode 100644 index 00000000..9fa3997d --- /dev/null +++ b/justfile @@ -0,0 +1,41 @@ + + + +[private] +default: + just --list + +check: format lint jslint + just test -m 'not\ network' + +set positional-arguments + +test *args='': + uv run pytest {{ args }} + +lint: + uv run ty check + +format: + uv run ruff format tests github_linter + uv run ruff check tests github_linter + +jslint: + biome check --verbose + +docker_build: + docker build -t 'ghcr.io/yaleman/github_linter:latest' \ + . + +docker_run: + docker run --rm -it \ + -p 8000:8000 \ + --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + 'ghcr.io/yaleman/github_linter:latest' \ + python -m github_linter.web + +workflow_stats: + docker run --rm -it \ + --env-file .envrc -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + 'ghcr.io/yaleman/github_linter:latest' \ + python -m github_linter.workflow_stats -f yaleman/pygoodwe \ No newline at end of file diff --git a/tests/test_branch_protection.py b/tests/test_branch_protection.py index 434bf3d3..3b2da206 100644 --- a/tests/test_branch_protection.py +++ b/tests/test_branch_protection.py @@ -14,7 +14,7 @@ def create_mock_workflow_file(name: str, content: str) -> Mock: """Create a mock workflow file ContentFile object""" mock_file = Mock() mock_file.name = name - mock_file.decoded_content = content.encode('utf-8') + mock_file.decoded_content = content.encode("utf-8") return mock_file @@ -47,9 +47,7 @@ def test_get_available_checks_with_simple_workflow() -> None: mock_repo.repository = Mock() # Mock get_contents to return our test workflow - workflow_files = [ - create_mock_workflow_file("test.yml", workflow_content) - ] + workflow_files = [create_mock_workflow_file("test.yml", workflow_content)] mock_repo.repository.get_contents.return_value = workflow_files # Call the function @@ -119,11 +117,8 @@ def test_get_available_checks_with_no_workflows() -> None: # Mock get_contents to raise UnknownObjectException from github.GithubException import UnknownObjectException - mock_repo.repository.get_contents.side_effect = UnknownObjectException( - status=404, - data={"message": "Not Found"}, - headers={} - ) + + mock_repo.repository.get_contents.side_effect = UnknownObjectException(status=404, data={"message": "Not Found"}, headers={}) # Call the function available_checks = _get_available_checks_for_repo(mock_repo) diff --git a/tests/test_dependabot.py b/tests/test_dependabot.py index 8abe93e6..2fbb7c43 100644 --- a/tests/test_dependabot.py +++ b/tests/test_dependabot.py @@ -1,4 +1,4 @@ -""" testing dependabot """ +"""testing dependabot""" # from github_linter.tests.dependabot import load_file diff --git a/tests/test_generic.py b/tests/test_generic.py index 082a188b..77fef92a 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1,4 +1,4 @@ -""" testing pyproject """ +"""testing pyproject""" from utils import generate_test_repo @@ -49,9 +49,7 @@ def test_generate_funding_file() -> None: def test_generate_funding_file_simple_with_quote() -> None: """tests generator""" - result = parse_funding_file( - "# test funding file\ngithub: yaleman" - ) # need to put the leading newline to make it not be a list... because YAML? + result = parse_funding_file("# test funding file\ngithub: yaleman") # need to put the leading newline to make it not be a list... because YAML? test_parse_funding_file() output = generate_funding_file(result) assert output == "github: yaleman\n" @@ -59,9 +57,7 @@ def test_generate_funding_file_simple_with_quote() -> None: def test_generate_funding_file_simple() -> None: """tests generator""" - result: FundingDict = parse_funding_file( - " github: yaleman" - ) # need to put the leading newline to make it not be a list... because YAML? + result: FundingDict = parse_funding_file(" github: yaleman") # need to put the leading newline to make it not be a list... because YAML? test_parse_funding_file() output = generate_funding_file(result) assert output == "github: yaleman\n" diff --git a/tests/test_github_data.py b/tests/test_github_data.py new file mode 100644 index 00000000..935deedb --- /dev/null +++ b/tests/test_github_data.py @@ -0,0 +1,24 @@ +import pytest + + +from github_linter import GithubLinter +from github_linter.web import get_all_user_repos + + +@pytest.mark.network +def test_get_all_user_repos() -> None: + """tests what we get back from it, can be slow and burn things""" + linter = GithubLinter() + linter.do_login() + config = { + "linter": { + "owner_list": [ + "TerminalOutcomes", + ] + } + } + result = get_all_user_repos(linter, config) + + for repo in result: + print(repo) + print(f"Found {len(result)} repositories") diff --git a/tests/test_hcl2.py b/tests/test_hcl2.py index d60bdd03..0df895b3 100644 --- a/tests/test_hcl2.py +++ b/tests/test_hcl2.py @@ -1,5 +1,4 @@ -""" tests things interacting with HCL-format files (terraform, hopefully)""" - +"""tests things interacting with HCL-format files (terraform, hopefully)""" import hcl2.api diff --git a/tests/test_repo_search.py b/tests/test_repo_search.py index b4c49748..25b97347 100644 --- a/tests/test_repo_search.py +++ b/tests/test_repo_search.py @@ -1,4 +1,4 @@ -""" testing the search filter generator """ +"""testing the search filter generator""" from utils import generate_test_repo from github_linter import filter_by_repo, generate_repo_search_string @@ -44,10 +44,5 @@ def test_generate_repo_search_string() -> None: owner_filter = ["yaleman", "terminaloutcomes "] repo_filter = ["github_linter", "cheese"] - result = generate_repo_search_string( - repo_filter=repo_filter, owner_filter=owner_filter - ) - assert ( - result.search_string - == "repo:yaleman/github_linter repo:yaleman/cheese repo:terminaloutcomes/github_linter repo:terminaloutcomes/cheese" - ) + result = generate_repo_search_string(repo_filter=repo_filter, owner_filter=owner_filter) + assert result.search_string == "repo:yaleman/github_linter repo:yaleman/cheese repo:terminaloutcomes/github_linter repo:terminaloutcomes/cheese" diff --git a/tests/test_web.py b/tests/test_web.py index 1071afc9..c7be2fb4 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,10 +1,8 @@ """test the web interface a bit""" -import pytest from fastapi.testclient import TestClient -from github_linter import GithubLinter -from github_linter.web import app, get_all_user_repos +from github_linter.web import app client = TestClient(app) @@ -15,22 +13,3 @@ def test_read_main() -> None: response = client.get("/") assert response.status_code == 200 assert b"Github Linter" in response.content - - -@pytest.mark.network -def test_get_all_user_repos() -> None: - """tests what we get back from it""" - linter = GithubLinter() - linter.do_login() - config = { - "linter": { - "owner_list": [ - "yaleman", - ] - } - } - result = get_all_user_repos(linter, config) - - for repo in result: - print(repo) - print(f"Found {len(result)} repositories")
Name  Archived Open Issues (| totalFilteredOpenIssues |) PRs (| totalFilteredPRs |)
| repo.full_name | (P)Open in github🪦 + + + + 🪦 - |repo.open_issues| + |repo.open_issues| - |repo.open_prs| + |repo.open_prs|