Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.swift]
indent_size = 4

[*.{rs,go}]
indent_size = 4

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
46 changes: 46 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Normalize all text files to LF
* text=auto eol=lf

# Scripts -- must be LF (executed in Docker/Linux)
*.sh text eol=lf
*.bash text eol=lf

# Data/config
*.sql text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.json text eol=lf
*.toml text eol=lf
*.properties text eol=lf

# Source code
*.scala text eol=lf
*.kt text eol=lf
*.kts text eol=lf
*.java text eol=lf
*.swift text eol=lf
*.ts text eol=lf
*.js text eol=lf
*.rs text eol=lf
*.go text eol=lf

# Docs
*.md text eol=lf
*.txt text eol=lf

# Docker
Dockerfile text eol=lf
docker-compose*.yml text eol=lf

# Binary -- never touch
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.jar binary
*.zip binary
*.tar.gz binary
*.woff binary
*.woff2 binary
*.ttf binary
41 changes: 41 additions & 0 deletions .github/workflows/lint-files.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Lint Files

on:
workflow_call:

jobs:
file-hygiene:
name: File Hygiene (editorconfig)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: EditorConfig Checker
uses: editorconfig-checker/action-editorconfig-checker@d2ed4fd072ae6f887e9407c909af0f585d2ad9f4 # v2

shell-lint:
name: Shell Scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ShellCheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
with:
severity: warning

markdown-lint:
name: Markdown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: markdownlint
uses: DavidAnson/markdownlint-cli2-action@ce4853d43830c74c1753b39f3cf40f71c2031eb9 # v23
with:
globs: "**/*.md"

yaml-lint:
name: YAML
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: yamllint
uses: frenck/action-yamllint@e3e8cdef144eeb914883bd3c77f0333227140ffc # v1
10 changes: 10 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: Lint

on:
pull_request:
push:
branches: [main]

jobs:
lint:
uses: hyperledger-identus/.github/.github/workflows/lint-files.yml@main
17 changes: 17 additions & 0 deletions .markdownlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# markdownlint configuration
# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md

default: true

# Allow long lines (common in tables, URLs, and generated content)
MD013: false

# Allow multiple top-level headings (common in multi-section docs)
MD025: false

# Allow inline HTML (needed for badges, details/summary, etc.)
MD033: false

# Allow duplicate headings in different sections
MD024:
siblings_only: true
13 changes: 13 additions & 0 deletions .yamllint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
extends: default

rules:
# Allow long lines (common in CI workflows and docker-compose)
line-length: disable

# Allow truthy values like 'on' (used in GitHub Actions triggers)
truthy:
allowed-values: ["true", "false", "yes", "no", "on"]

# Relaxed comment indentation
comments-indentation: disable
127 changes: 127 additions & 0 deletions scripts/check-file-hygiene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
from __future__ import annotations

"""Check that all Identus repos contain the canonical hygiene config files.

Compares .gitattributes, .editorconfig, .markdownlint.yml, and .yamllint.yml
in every repo against the templates in the .github repo.

Uses prefix matching: a repo's file is considered OK if it starts with the
canonical template content. This allows repos to append local overrides
(e.g., ktlint rules in .editorconfig) while still enforcing the shared
baseline.

Requires: gh CLI authenticated with access to hyperledger-identus org.

Usage:
python3 scripts/check-file-hygiene.py
"""

import argparse
import base64
import json
import subprocess
import sys
from pathlib import Path

ORG = "hyperledger-identus"
TEMPLATE_REPO = ".github"
FILES = [".gitattributes", ".editorconfig", ".markdownlint.yml", ".yamllint.yml"]

# Repos to skip (the template repo itself)
SKIP_REPOS = {TEMPLATE_REPO}


def gh_api(endpoint: str) -> dict | list | None:
"""Call the GitHub API via gh CLI."""
result = subprocess.run(
["gh", "api", endpoint, "--paginate"],
capture_output=True,
text=True,
)
if result.returncode != 0:
return None
return json.loads(result.stdout)


def get_repo_names() -> list[str]:
"""Get all repo names in the org."""
repos = gh_api(f"orgs/{ORG}/repos?per_page=100")
if not repos:
print("Error: could not fetch repos. Is `gh` authenticated?", file=sys.stderr)
sys.exit(1)
return sorted(r["name"] for r in repos if not r["archived"])


def get_file_content(repo: str, path: str, ref: str = "main") -> str | None:
"""Fetch a file's content from a repo via the GitHub API."""
data = gh_api(f"repos/{ORG}/{repo}/contents/{path}?ref={ref}")
if not data or "content" not in data:
return None
return base64.b64decode(data["content"]).decode("utf-8")


def load_template(path: str) -> str:
"""Load a template file from the local .github repo."""
template_path = Path(__file__).resolve().parent.parent / path
if not template_path.exists():
print(f"Error: template {template_path} not found", file=sys.stderr)
sys.exit(1)
return template_path.read_text()


def main():
parser = argparse.ArgumentParser(description="Check file hygiene across Identus repos")
parser.parse_args()

repos = get_repo_names()
templates = {f: load_template(f) for f in FILES}

# Status tracking
results: dict[str, dict[str, str]] = {}

for repo in repos:
if repo in SKIP_REPOS:
continue
results[repo] = {}
for filename in FILES:
content = get_file_content(repo, filename)
if content is None:
results[repo][filename] = "MISSING"
elif content.startswith(templates[filename].rstrip()):
results[repo][filename] = "OK"
else:
results[repo][filename] = "OUTDATED"

# Print table
col_width = max(len(r) for r in results) + 2
file_widths = {f: max(len(f), 8) + 2 for f in FILES}

header = "Repo".ljust(col_width) + "".join(f.ljust(file_widths[f]) for f in FILES)
print(header)
print("-" * len(header))

all_ok = True
for repo, statuses in results.items():
row = repo.ljust(col_width)
for filename in FILES:
status = statuses[filename]
if status != "OK":
all_ok = False
row += status.ljust(file_widths[filename])
print(row)

# Summary
print()
total = len(results) * len(FILES)
ok_count = sum(1 for r in results.values() for s in r.values() if s == "OK")
missing = sum(1 for r in results.values() for s in r.values() if s == "MISSING")
outdated = sum(1 for r in results.values() for s in r.values() if s == "OUTDATED")
print(f"Total: {ok_count}/{total} OK, {missing} missing, {outdated} outdated")

if not all_ok:
sys.exit(1)


if __name__ == "__main__":
main()