Loading .gitlab-ci.yml 0 → 100644 +20 −0 Original line number Diff line number Diff line stages: - validate pre-commit: stage: validate image: python:3.14-slim variables: PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.pre-commit-cache cache: key: pre-commit paths: - .pre-commit-cache/ before_script: - apt-get update -qq && apt-get install -qqy git - pip install pre-commit --quiet script: - pre-commit run --all-files --hook-stage pre-push rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "master" .pre-commit-config.yaml 0 → 100644 +16 −0 Original line number Diff line number Diff line repos: - repo: https://github.com/adrienverge/yamllint rev: v1.35.1 hooks: - id: yamllint stages: [pre-push] - repo: local hooks: - id: gitlab-ci-lint name: Validate GitLab CI templates via API language: script entry: scripts/validate-gitlab-ci.py stages: [pre-push] pass_filenames: false always_run: true .yamllint.yml 0 → 100644 +9 −0 Original line number Diff line number Diff line extends: default rules: document-start: disable line-length: max: 160 level: warning truthy: allowed-values: ['true', 'false'] check-keys: false scripts/validate-gitlab-ci.py 0 → 100755 +101 −0 Original line number Diff line number Diff line #!/usr/bin/env python3 import json import os import re import subprocess import sys import tempfile import urllib.request GITLAB_URL = "https://gitlab.cetera.ru" PROJECT_PATH = "boilerplate/ci" INCLUDE_PATTERN = re.compile( r"https://gitlab\.cetera\.ru/boilerplate/ci/raw/[^/]+/([^\s'\"]+)" ) def build_content(): with open("gitlab-ci-template.yml") as f: template = f.read() parts = [] for filename in INCLUDE_PATTERN.findall(template): with open(filename) as f: parts.append(f.read().rstrip()) job_definitions = strip_include_block(template).strip() if job_definitions: parts.append(job_definitions) return "\n\n".join(parts) def strip_include_block(content): lines = content.splitlines(keepends=True) result = [] in_include = False for line in lines: if line.startswith("include:"): in_include = True continue if in_include: if line and not line[0].isspace() and not line.startswith("#"): in_include = False result.append(line) else: result.append(line) return "".join(result) def validate_via_token(content, token): encoded_path = urllib.parse.quote(PROJECT_PATH, safe="") url = f"{GITLAB_URL}/api/v4/projects/{encoded_path}/ci/lint" data = json.dumps({"content": content}).encode() req = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json", "PRIVATE-TOKEN": token, }, ) with urllib.request.urlopen(req) as response: return json.loads(response.read()) def validate_via_glab(content): with tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False) as f: f.write(content) tmpfile = f.name try: result = subprocess.run(["glab", "ci", "lint", tmpfile]) return result.returncode == 0 finally: os.unlink(tmpfile) def main(): content = build_content() print("Validating GitLab CI configuration from local files") gitlab_token = os.environ.get("GITLAB_TOKEN") if gitlab_token: result = validate_via_token(content, gitlab_token) for warning in result.get("warnings", []): print(f"Warning: {warning}") if result.get("valid"): print("GitLab CI configuration is valid") sys.exit(0) else: print("GitLab CI configuration is INVALID:") for error in result.get("errors", []): print(f" - {error}") sys.exit(1) elif os.environ.get("CI_JOB_TOKEN"): print("Error: GITLAB_TOKEN is required in CI (CI_JOB_TOKEN lacks lint API permissions)", file=sys.stderr) sys.exit(1) else: sys.exit(0 if validate_via_glab(content) else 1) if __name__ == "__main__": main() Loading
.gitlab-ci.yml 0 → 100644 +20 −0 Original line number Diff line number Diff line stages: - validate pre-commit: stage: validate image: python:3.14-slim variables: PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.pre-commit-cache cache: key: pre-commit paths: - .pre-commit-cache/ before_script: - apt-get update -qq && apt-get install -qqy git - pip install pre-commit --quiet script: - pre-commit run --all-files --hook-stage pre-push rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == "master"
.pre-commit-config.yaml 0 → 100644 +16 −0 Original line number Diff line number Diff line repos: - repo: https://github.com/adrienverge/yamllint rev: v1.35.1 hooks: - id: yamllint stages: [pre-push] - repo: local hooks: - id: gitlab-ci-lint name: Validate GitLab CI templates via API language: script entry: scripts/validate-gitlab-ci.py stages: [pre-push] pass_filenames: false always_run: true
.yamllint.yml 0 → 100644 +9 −0 Original line number Diff line number Diff line extends: default rules: document-start: disable line-length: max: 160 level: warning truthy: allowed-values: ['true', 'false'] check-keys: false
scripts/validate-gitlab-ci.py 0 → 100755 +101 −0 Original line number Diff line number Diff line #!/usr/bin/env python3 import json import os import re import subprocess import sys import tempfile import urllib.request GITLAB_URL = "https://gitlab.cetera.ru" PROJECT_PATH = "boilerplate/ci" INCLUDE_PATTERN = re.compile( r"https://gitlab\.cetera\.ru/boilerplate/ci/raw/[^/]+/([^\s'\"]+)" ) def build_content(): with open("gitlab-ci-template.yml") as f: template = f.read() parts = [] for filename in INCLUDE_PATTERN.findall(template): with open(filename) as f: parts.append(f.read().rstrip()) job_definitions = strip_include_block(template).strip() if job_definitions: parts.append(job_definitions) return "\n\n".join(parts) def strip_include_block(content): lines = content.splitlines(keepends=True) result = [] in_include = False for line in lines: if line.startswith("include:"): in_include = True continue if in_include: if line and not line[0].isspace() and not line.startswith("#"): in_include = False result.append(line) else: result.append(line) return "".join(result) def validate_via_token(content, token): encoded_path = urllib.parse.quote(PROJECT_PATH, safe="") url = f"{GITLAB_URL}/api/v4/projects/{encoded_path}/ci/lint" data = json.dumps({"content": content}).encode() req = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json", "PRIVATE-TOKEN": token, }, ) with urllib.request.urlopen(req) as response: return json.loads(response.read()) def validate_via_glab(content): with tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False) as f: f.write(content) tmpfile = f.name try: result = subprocess.run(["glab", "ci", "lint", tmpfile]) return result.returncode == 0 finally: os.unlink(tmpfile) def main(): content = build_content() print("Validating GitLab CI configuration from local files") gitlab_token = os.environ.get("GITLAB_TOKEN") if gitlab_token: result = validate_via_token(content, gitlab_token) for warning in result.get("warnings", []): print(f"Warning: {warning}") if result.get("valid"): print("GitLab CI configuration is valid") sys.exit(0) else: print("GitLab CI configuration is INVALID:") for error in result.get("errors", []): print(f" - {error}") sys.exit(1) elif os.environ.get("CI_JOB_TOKEN"): print("Error: GITLAB_TOKEN is required in CI (CI_JOB_TOKEN lacks lint API permissions)", file=sys.stderr) sys.exit(1) else: sys.exit(0 if validate_via_glab(content) else 1) if __name__ == "__main__": main()