From c8dbd399ce3b9cee9ba6a137d6b2ed72d323f61f Mon Sep 17 00:00:00 2001 From: Martin Tahiraj Date: Thu, 2 Apr 2026 14:46:17 +0200 Subject: [PATCH] refactor(ci): extract inline scripts to ci/scripts/ - ci/scripts/read_meta.py legge version|build_from da config.yaml/json - ci/scripts/update_repo.py upserta repository.json - ci/scripts/lint_addon.sh hadolint + yaml/json + shellcheck - ci/scripts/git_push_repo.sh commit e push di repository.json Rimuove writeFile /tmp/ dal Jenkinsfile Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ci/Jenkinsfile | 142 +----------------------------------- ci/scripts/git_push_repo.sh | 36 +++++++++ ci/scripts/lint_addon.sh | 38 ++++++++++ ci/scripts/read_meta.py | 32 ++++++++ ci/scripts/update_repo.py | 87 ++++++++++++++++++++++ 5 files changed, 197 insertions(+), 138 deletions(-) create mode 100644 ci/scripts/git_push_repo.sh create mode 100644 ci/scripts/lint_addon.sh create mode 100644 ci/scripts/read_meta.py create mode 100644 ci/scripts/update_repo.py diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile index 0203d06..c2a497f 100644 --- a/ci/Jenkinsfile +++ b/ci/Jenkinsfile @@ -210,30 +210,7 @@ pipeline { [(addon): { stage("lint ❯ ${addon}") { try { - // ---- Dockerfile ---- - sh "docker run --rm -i hadolint/hadolint < ${addon}/Dockerfile" - - // ---- Config (yaml o json) ---- - def configYaml = "${addon}/config.yaml" - def configJson = "${addon}/config.json" - sh """ - if [ -f ${configYaml} ]; then - python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" ${configYaml} - echo "[OK] ${addon}/config.yaml valido" - elif [ -f ${configJson} ]; then - python3 -c "import json,sys; json.load(open(sys.argv[1]))" ${configJson} - echo "[OK] ${addon}/config.json valido" - fi - """ - - // ---- Shell scripts ---- - sh """ - find ${addon}/ -name '*.sh' | while read f; do - echo "[INFO] shellcheck: \$f" - docker run --rm -v \$PWD:/mnt koalaman/shellcheck:stable /mnt/\$f - done - """ - + sh "bash ci/scripts/lint_addon.sh ${addon}" echo "[OK] ${addon}: tutti i check superati" lintOk.add(addon) @@ -291,25 +268,7 @@ pipeline { [(addon): { stage("build ❯ ${addon}") { try { - // Legge versione e build_from dalla config (yaml o json) - def metaScript = """ -import yaml, json, os, sys - -addon = sys.argv[1] -for name in ('config.yaml', 'config.json'): - p = os.path.join(addon, name) - if os.path.exists(p): - with open(p) as f: - cfg = yaml.safe_load(f) if name.endswith('.yaml') else json.load(f) - version = str(cfg.get('version', 'latest')) - build_from = (cfg.get('build_from') or {}).get('amd64', '') - print(f'{version}|{build_from}') - sys.exit(0) - -print('latest|') -""" - writeFile file: '/tmp/read_meta.py', text: metaScript - def meta = sh(script: "python3 /tmp/read_meta.py ${addon}", returnStdout: true).trim() + def meta = sh(script: "python3 ci/scripts/read_meta.py ${addon}", returnStdout: true).trim() def parts = meta.split('\\|') def version = parts[0] def buildFrom = parts.size() > 1 ? parts[1] : '' @@ -381,108 +340,16 @@ print('latest|') echo "[INFO] Aggiorno repository.json per: ${successAddons}" - def updateScript = '''\ -import json, os, sys - -# Carica yaml solo se disponibile (config.yaml), altrimenti json -try: - import yaml - HAS_YAML = True -except ImportError: - HAS_YAML = False - -REGISTRY = os.environ['REGISTRY'] -GITEA_URL = os.environ['GITEA_BASE_URL'] -GITEA_USER = sys.argv[1] -addons_arg = sys.argv[2:] - -repo_file = 'repository.json' -skeleton = { - 'name': f'HA Add-ons by {GITEA_USER}', - 'url': f'{GITEA_URL}/{GITEA_USER}/HomeAssistantAddOns', - 'maintainer': GITEA_USER, - 'addons': [] -} -repo = skeleton.copy() -if os.path.exists(repo_file): - with open(repo_file) as f: - repo = json.load(f) -repo.setdefault('addons', []) - -def load_cfg(addon_dir): - for name, loader in [('config.yaml', 'yaml'), ('config.json', 'json')]: - p = os.path.join(addon_dir, name) - if not os.path.exists(p): - continue - with open(p) as f: - if loader == 'yaml' and HAS_YAML: - return yaml.safe_load(f) - return json.load(f) - return {} - -changed = False -for addon in addons_arg: - cfg = load_cfg(addon) - if not cfg: - print(f'[WARN] Nessuna config trovata per {addon}, skip', flush=True) - continue - - slug = cfg.get('slug', addon) - version = str(cfg.get('version', 'latest')) - entry = { - 'slug': slug, - 'name': cfg.get('name', addon), - 'description': cfg.get('description', ''), - 'version': version, - 'url': cfg.get('url', ''), - 'arch': cfg.get('arch', ['amd64']), - 'image': f'{REGISTRY}/hassio-addons/{slug}:{version}', - } - - idx = next((i for i, a in enumerate(repo['addons']) if a.get('slug') == slug), None) - if idx is not None: - old_ver = repo['addons'][idx].get('version', '?') - repo['addons'][idx] = entry - print(f'[UPDATE] {slug}: {old_ver} → {version}', flush=True) - else: - repo['addons'].append(entry) - print(f'[ADD] {slug} v{version}', flush=True) - changed = True - -if changed: - with open(repo_file, 'w') as f: - json.dump(repo, f, indent=2, ensure_ascii=False) - f.write('\\n') - print('[OK] repository.json aggiornato', flush=True) -else: - print('[INFO] Nessuna modifica a repository.json', flush=True) -''' - writeFile file: '/tmp/update_repo.py', text: updateScript - - sh "python3 /tmp/update_repo.py \"${params.GITEA_USER}\" ${successAddons.join(' ')}" + sh "python3 ci/scripts/update_repo.py \"${params.GITEA_USER}\" ${successAddons.join(' ')}" sh 'git diff repository.json || true' - // Commit e push solo se ci sono modifiche staged withCredentials([usernamePassword( credentialsId: 'GIT_TOKEN', usernameVariable: 'GITEA_PUSH_USR', passwordVariable: 'GITEA_PUSH_PSW' )]) { - sh """ - git config user.email "jenkins@pipelines.mt-home.uk" - git config user.name "Jenkins CI" - git add repository.json - if git diff --staged --quiet; then - echo "[INFO] Nessuna modifica a repository.json da committare" - else - git commit -m "chore: update repository.json [skip ci]" - git push \\ - "https://oauth2:\${GITEA_PUSH_PSW}@git.mt-home.uk/${params.GITEA_USER}/HomeAssistantAddOns.git" \\ - HEAD:main - echo "[OK] repository.json pushato su main" - fi - """ + sh "bash ci/scripts/git_push_repo.sh \"${params.GITEA_USER}\" \"${env.GITEA_BASE_URL}\"" } } } @@ -571,7 +438,6 @@ else: script { try { sh 'docker logout ${REGISTRY} 2>/dev/null || true' - sh 'rm -f /tmp/read_meta.py /tmp/update_repo.py' } catch (e) { echo "[WARN] cleanup: ${e.message}" } diff --git a/ci/scripts/git_push_repo.sh b/ci/scripts/git_push_repo.sh new file mode 100644 index 0000000..102a686 --- /dev/null +++ b/ci/scripts/git_push_repo.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# git_push_repo.sh +# +# Committa repository.json (se modificato) e lo pusha su main. +# Il messaggio include [skip ci] per evitare loop webhook. +# +# Variabili d'ambiente richieste: +# GITEA_PUSH_PSW — password/token Gitea (iniettata da withCredentials) + +set -euo pipefail + +GITEA_USER=${1:?Uso: git_push_repo.sh } +GITEA_BASE_URL=${2:?Uso: git_push_repo.sh } +GITEA_PSW=${GITEA_PUSH_PSW:?GITEA_PUSH_PSW non impostata} + +git config user.email "jenkins@pipelines.mt-home.uk" +git config user.name "Jenkins CI" + +git add repository.json + +if git diff --staged --quiet; then + echo "[INFO] Nessuna modifica a repository.json — niente da committare" + exit 0 +fi + +git commit -m "chore: update repository.json [skip ci]" + +# Rimuove schema (https://) per costruire l'URL con credenziali +GITEA_HOST=${GITEA_BASE_URL#https://} +GITEA_HOST=${GITEA_HOST#http://} + +git push \ + "https://oauth2:${GITEA_PSW}@${GITEA_HOST}/${GITEA_USER}/HomeAssistantAddons.git" \ + HEAD:main + +echo "[OK] repository.json pushato su main" diff --git a/ci/scripts/lint_addon.sh b/ci/scripts/lint_addon.sh new file mode 100644 index 0000000..b16f064 --- /dev/null +++ b/ci/scripts/lint_addon.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# lint_addon.sh +# +# Esegue tre check su un addon HA: +# 1. hadolint — Dockerfile best practices +# 2. yaml/json — config.yaml o config.json valido +# 3. shellcheck — tutti gli *.sh nella directory +# +# Esce con codice 0 se tutti i check passano, 1 altrimenti. + +set -euo pipefail + +ADDON=${1:?Uso: lint_addon.sh } + +echo "[INFO] === Lint: ${ADDON} ===" + +# ── 1. hadolint ──────────────────────────────────────────────────────────────── +echo "[INFO] hadolint: ${ADDON}/Dockerfile" +docker run --rm -i hadolint/hadolint < "${ADDON}/Dockerfile" + +# ── 2. Config valida ─────────────────────────────────────────────────────────── +if [ -f "${ADDON}/config.yaml" ]; then + python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" "${ADDON}/config.yaml" + echo "[OK] ${ADDON}/config.yaml valido" +elif [ -f "${ADDON}/config.json" ]; then + python3 -c "import json,sys; json.load(open(sys.argv[1]))" "${ADDON}/config.json" + echo "[OK] ${ADDON}/config.json valido" +else + echo "[WARN] Nessuna config trovata in ${ADDON}/" +fi + +# ── 3. shellcheck ────────────────────────────────────────────────────────────── +find "${ADDON}/" -name '*.sh' | while read -r f; do + echo "[INFO] shellcheck: ${f}" + docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable "/mnt/${f}" +done + +echo "[OK] ${ADDON}: lint completato" diff --git a/ci/scripts/read_meta.py b/ci/scripts/read_meta.py new file mode 100644 index 0000000..75969db --- /dev/null +++ b/ci/scripts/read_meta.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +Legge version e build_from.amd64 dalla config di un addon HA. + +Uso: python3 read_meta.py +Output su stdout: version|build_from_amd64 +""" +import json +import os +import sys + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +addon = sys.argv[1] + +for filename in ('config.yaml', 'config.json'): + path = os.path.join(addon, filename) + if not os.path.exists(path): + continue + with open(path) as f: + cfg = yaml.safe_load(f) if (filename.endswith('.yaml') and HAS_YAML) else json.load(f) + version = str(cfg.get('version', 'latest')) + build_from = (cfg.get('build_from') or {}).get('amd64', '') + print(f'{version}|{build_from}') + sys.exit(0) + +# Nessuna config trovata +print('latest|') diff --git a/ci/scripts/update_repo.py b/ci/scripts/update_repo.py new file mode 100644 index 0000000..cc623dd --- /dev/null +++ b/ci/scripts/update_repo.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Aggiorna repository.json con i metadati degli addon buildati con successo. +Upserta ogni addon (match su slug). Il file viene creato se non esiste. + +Uso: python3 update_repo.py [addon2 ...] + +Variabili d'ambiente richieste: + REGISTRY es. registry.mt-home.uk + GITEA_BASE_URL es. https://git.mt-home.uk +""" +import json +import os +import sys + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +REGISTRY = os.environ['REGISTRY'] +GITEA_BASE_URL = os.environ['GITEA_BASE_URL'] +GITEA_USER = sys.argv[1] +addons_arg = sys.argv[2:] + +REPO_FILE = 'repository.json' +skeleton = { + 'name': f'HA Add-ons by {GITEA_USER}', + 'url': f'{GITEA_BASE_URL}/{GITEA_USER}/HomeAssistantAddons', + 'maintainer': GITEA_USER, + 'addons': [], +} + +repo = skeleton.copy() +if os.path.exists(REPO_FILE): + with open(REPO_FILE) as f: + repo = json.load(f) +repo.setdefault('addons', []) + + +def load_cfg(addon_dir): + for name, use_yaml in [('config.yaml', True), ('config.json', False)]: + p = os.path.join(addon_dir, name) + if not os.path.exists(p): + continue + with open(p) as f: + return yaml.safe_load(f) if (use_yaml and HAS_YAML) else json.load(f) + return {} + + +changed = False +for addon in addons_arg: + cfg = load_cfg(addon) + if not cfg: + print(f'[WARN] Nessuna config trovata per {addon}, skip', flush=True) + continue + + slug = cfg.get('slug', addon) + version = str(cfg.get('version', 'latest')) + entry = { + 'slug': slug, + 'name': cfg.get('name', addon), + 'description': cfg.get('description', ''), + 'version': version, + 'url': cfg.get('url', ''), + 'arch': cfg.get('arch', ['amd64']), + 'image': f'{REGISTRY}/hassio-addons/{slug}:{version}', + } + + idx = next((i for i, a in enumerate(repo['addons']) if a.get('slug') == slug), None) + if idx is not None: + old_ver = repo['addons'][idx].get('version', '?') + repo['addons'][idx] = entry + print(f'[UPDATE] {slug}: {old_ver} → {version}', flush=True) + else: + repo['addons'].append(entry) + print(f'[ADD] {slug} v{version}', flush=True) + changed = True + +if changed: + with open(REPO_FILE, 'w') as f: + json.dump(repo, f, indent=2, ensure_ascii=False) + f.write('\n') + print('[OK] repository.json aggiornato', flush=True) +else: + print('[INFO] Nessuna modifica a repository.json', flush=True)