2 Commits
Author SHA1 Message Date
shmyga b459eef34b feat(preset): add gitea preset
test / test (push) Successful in 2m20s
2026-10-03 16:34:32 +03:00
shmyga 244f995490 ci(action): add gitea actions
test / test (push) Successful in 2m18s
2026-09-30 15:03:56 +03:00
11 changed files with 176 additions and 31 deletions
+20
View File
@@ -0,0 +1,20 @@
name: release
on:
push:
tags: ["*"]
env:
POETRY_HTTP_BASIC_InfernalGames_USERNAME: ${{ vars.PUBLISH_USERNAME }}
POETRY_HTTP_BASIC_InfernalGames_PASSWORD: ${{ secrets.PUBLISH_TOKEN }}
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: https://git.shmyga.ru/InfernalGames/project-manager@master
with:
setup-python: true
- run: pm build -vv
- run: pm publish -vv
+14
View File
@@ -0,0 +1,14 @@
name: test
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: https://git.shmyga.ru/InfernalGames/project-manager@master
with:
setup-python: true
- run: pm lint -vv
- run: pm test -vv
+36
View File
@@ -0,0 +1,36 @@
name: "Project Manager"
inputs:
python-version:
default: "3.14"
setup-python:
default: false
venv-version:
default: "1"
runs:
using: "composite"
steps:
- id: setup-python
uses: actions/setup-python@v7
with:
python-version: ${{ inputs.python-version }}
cache: "pip"
- run: |
pip install project-manager \
--index-url https://git.shmyga.ru/api/packages/InfernalGames/pypi/simple/ \
--extra-index-url https://pypi.org/simple
- run: pm setup:config -vv
- if: inputs.setup-python
run: pip install poetry
- id: cache-venv
if: inputs.setup-python
uses: actions/cache@v6
with:
path: .venv
key: venv-${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-v${{ inputs.venv-version }}
- if: ${{ inputs.setup-python && steps.cache-venv.outputs.cache-hit != 'true' }}
run: pm setup:python -vv
+15 -6
View File
@@ -1,5 +1,4 @@
import json import json
import os
import re import re
import tomllib import tomllib
from pathlib import Path from pathlib import Path
@@ -9,6 +8,7 @@ import dotenv
from jinja2 import Environment from jinja2 import Environment
from ..preset.model import Variables from ..preset.model import Variables
from .util import get_environ
def do_bool(data: str | None) -> bool: def do_bool(data: str | None) -> bool:
@@ -19,10 +19,6 @@ def do_bool(data: str | None) -> bool:
return data.lower() not in ["false", "null", "none", "0", ""] return data.lower() not in ["false", "null", "none", "0", ""]
def do_env(key: str, default: str | None = None) -> str | None:
return os.environ.get(key=key.upper(), default=default)
def do_not(value: Any | None) -> bool: def do_not(value: Any | None) -> bool:
return not do_bool(str(value)) return not do_bool(str(value))
@@ -31,6 +27,8 @@ def _build_env(basepath: Path) -> Environment:
env = Environment() env = Environment()
def do_file(filename: str) -> Path | None: def do_file(filename: str) -> Path | None:
if not filename:
return None
result = Path(filename) result = Path(filename)
if not result.is_absolute(): if not result.is_absolute():
result = basepath / result result = basepath / result
@@ -73,6 +71,11 @@ def _build_env(basepath: Path) -> Environment:
keys = list(data.keys()) keys = list(data.keys())
return keys[keyindex] return keys[keyindex]
env_data = get_environ(basepath)
def do_env(key: str, default: str | None = None) -> str | None:
return env_data.get(key.upper(), default)
env.filters["file"] = do_file env.filters["file"] = do_file
env.filters["read"] = do_read env.filters["read"] = do_read
env.filters["get"] = do_get env.filters["get"] = do_get
@@ -91,7 +94,13 @@ class PathTemplate:
def update_variables(self, templates: Variables, variables: Variables): def update_variables(self, templates: Variables, variables: Variables):
for k, v in templates.items(): for k, v in templates.items():
variables[k] = self._env.from_string(v).render(**variables) if isinstance(v, dict):
variables[k] = {
kk: self._env.from_string(vv).render(**variables)
for kk, vv in v.items()
}
else:
variables[k] = self._env.from_string(v).render(**variables)
def fill_template(self, template: str, variables: Variables) -> str: def fill_template(self, template: str, variables: Variables) -> str:
return self._env.from_string(template).render(**variables) return self._env.from_string(template).render(**variables)
+13
View File
@@ -1,3 +1,9 @@
import os
from pathlib import Path
import dotenv
def merge_dicts(target: dict, enhancer: dict) -> dict: def merge_dicts(target: dict, enhancer: dict) -> dict:
for key, val in enhancer.items(): for key, val in enhancer.items():
if key not in target: if key not in target:
@@ -9,3 +15,10 @@ def merge_dicts(target: dict, enhancer: dict) -> dict:
else: else:
target[key] = val target[key] = val
return target return target
def get_environ(path: Path) -> dict[str, str]:
result = os.environ.copy()
if (env_file := path / ".env").exists():
result.update(dotenv.dotenv_values(env_file))
return result
+13 -1
View File
@@ -1,4 +1,16 @@
[presets.config.tasks.setup.config] [presets.config.tasks.setup.env]
priority = 1 priority = 1
conditions = ["{{ ('.env-base' | file) and ('.env' | file | not ) }}"] conditions = ["{{ ('.env-base' | file) and ('.env' | file | not ) }}"]
commands = ["cp .env-base .env"] commands = ["cp .env-base .env"]
[presets.config.tasks.setup.env2]
priority = 1
conditions = ["{{ ('.env.default' | file) and ('.env' | file | not ) }}"]
commands = ["cp .env.default .env"]
[presets.config.tasks.setup.json]
priority = 1
conditions = [
"{{ ('config.example.json' | file) and ('config.json' | file | not ) }}",
]
commands = ["cp config.example.json config.json"]
+8 -6
View File
@@ -1,15 +1,17 @@
[presets.docker] [presets.docker]
conditions = ["{{ 'Dockerfile' | file }}"] conditions = ["{{ 'Dockerfile' | file }}"]
variables.docker_name = "{{ 'pyproject.toml' | read | get('project.name') or 'package.json' | read | get('name') }}" variables.docker_name = "{{ project_name }}"
variables.docker_version = "{{ 'pyproject.toml' | read | get('project.version') or 'package.json' | read | get('version') }}" variables.docker_version = "{{ project_version }}"
variables.docker_repo = "{{ '.env' | read | get('DOCKER_REPO') }}" variables.docker_repo = "{{ 'DOCKER_REPO' | env }}"
variables.docker_group = "{{ '.env' | read | get('DOCKER_GROUP') }}" variables.docker_group = "{{ 'DOCKER_GROUP' | env }}"
variables.docker_image = "{% if docker_group | bool %}{{ docker_group }}/{% endif %}{{ docker_name }}" variables.docker_image = "{% if docker_group | bool %}{{ docker_group }}/{% endif %}{{ docker_name }}"
variables.docker_tag = "{{ '.env' | read | get('DOCKER_TAG') or 'latest' }}" variables.docker_tag = "{{ 'DOCKER_TAG' | env('latest') }}"
variables.docker_target = "{{ 'DOCKER_TARGET' | env }}"
variables.docker_target_arg = "{{ ('--target ' + docker_target) if docker_target else '' }}"
# build # build
[presets.docker.tasks.build.default] [presets.docker.tasks.build.default]
commands = ["docker build -t {{ docker_image }} . -f Dockerfile"] commands = ["docker build {{ docker_target_arg }} -t {{ docker_image }} . -f Dockerfile"]
# publish version # publish version
[presets.docker.tasks.publish.version] [presets.docker.tasks.publish.version]
+24
View File
@@ -0,0 +1,24 @@
[presets.gitea]
variables.publish_repo = "{{ 'PUBLISH_REPO' | env }}"
variables.publish_repo_path = "{{ publish_repo }}/generic/{{ project_name }}/{{ project_version }}"
variables.publish_user = "{{ 'PUBLISH_USER' | env }}"
variables.publish_password = "{{ 'PUBLISH_PASSWORD' | env }}"
variables.publish_auth = "{{ publish_user }}:{{ publish_password }}"
variables.publish_artefacts = {}
[presets.gitea.tasks.publish."{{ publish_artefacts | tojson }}"]
conditions = ["{{ item | file }}"]
variables.artefact_file = "{{ item }}"
variables.artefact_name = "{{ (artefact_file | file).name }}"
commands = [
"curl --user \"{{ publish_auth }}\" -X DELETE \"{{ publish_repo_path }}/{{ artefact_name }}\"",
"curl --user \"{{ publish_auth }}\" --fail --upload-file \"{{ artefact_file }}\" \"{{ publish_repo_path }}/{{ artefact_name }}\"",
]
[presets.gitea.tasks.publish.debian_repo]
conditions = ["{{ publish_artefacts.debian | file }}"]
variables.artefact_file = "{{ publish_artefacts.debian }}"
commands = [
"curl --user \"{{ publish_auth }}\" -X DELETE \"{{ publish_repo }}/debian/pool/noble/main/{{ project_name }}/{{ project_version }}/all\"",
"curl --user \"{{ publish_auth }}\" --fail --upload-file \"{{ artefact_file }}\" \"{{ publish_repo }}/debian/pool/noble/main/upload\"",
]
@@ -1,5 +1,7 @@
[presets.'*'] [presets.'*']
priority = 1000 priority = 1000
conditions = ["{{ true }}"] conditions = ["{{ true }}"]
variables.project_name = "{{ 'pyproject.toml' | read | get('project.name') or 'package.json' | read | get('name') }}"
variables.project_version = "{{ 'pyproject.toml' | read | get('project.version') or 'package.json' | read | get('version') }}"
variables.version = "{{ 'version' | env }}" variables.version = "{{ 'version' | env }}"
tasks = {} tasks = {}
+28 -17
View File
@@ -1,3 +1,4 @@
import json
import operator import operator
from os import environ from os import environ
from pathlib import Path from pathlib import Path
@@ -14,7 +15,7 @@ class PresetEntryDetector(EntryDetector):
def __init__(self, preset_loader_config: PresetLoaderConfig | None = None): def __init__(self, preset_loader_config: PresetLoaderConfig | None = None):
self._preset_loader_config = preset_loader_config self._preset_loader_config = preset_loader_config
# pylint:disable=too-many-locals # pylint:disable=too-many-locals,too-many-nested-blocks,too-many-branches
def detect_entry(self, basepath: Path, rootpath: Path) -> Entry: def detect_entry(self, basepath: Path, rootpath: Path) -> Entry:
template = PathTemplate(basepath) template = PathTemplate(basepath)
preset = PresetLoader(basepath, self._preset_loader_config).load() preset = PresetLoader(basepath, self._preset_loader_config).load()
@@ -44,22 +45,32 @@ class PresetEntryDetector(EntryDetector):
continue continue
for task_name, task_pack in preset.tasks.items(): for task_name, task_pack in preset.tasks.items():
for sub_tag, task_preset in task_pack.items(): for sub_tag, task_preset in task_pack.items():
if not task_preset.conditions or template.check_conditions( sub_tags = template.fill_template(sub_tag, preset_variables)
task_preset.conditions, preset_variables try:
): sub_tags = json.loads(sub_tags)
variables = preset_variables.copy() except ValueError:
if task_preset.variables: sub_tags = {sub_tags: None}
template.update_variables(task_preset.variables, variables) for i_sub_tag, i_sub_tag_value in sub_tags.items():
task = Task( if i_sub_tag_value:
key=TaskKey(task_name, tag, sub_tag), preset_variables["item"] = i_sub_tag_value
commands=[ if not task_preset.conditions or template.check_conditions(
template.fill_template(command, variables) task_preset.conditions, preset_variables
for command in task_preset.commands ):
], variables = preset_variables.copy()
priority=task_preset.priority, if task_preset.variables:
cwd=task_preset.cwd, template.update_variables(
) task_preset.variables, variables
tasks.append(task) )
task = Task(
key=TaskKey(task_name, tag, i_sub_tag),
commands=[
template.fill_template(command, variables)
for command in task_preset.commands
],
priority=task_preset.priority,
cwd=task_preset.cwd,
)
tasks.append(task)
if tasks: if tasks:
tasks = sorted(tasks, key=operator.attrgetter("priority"), reverse=True) tasks = sorted(tasks, key=operator.attrgetter("priority"), reverse=True)
return Entry(tasks, basepath, rootpath) return Entry(tasks, basepath, rootpath)
+3 -1
View File
@@ -1,6 +1,8 @@
from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
Variables = dict[str, str] Variables = dict[str, Any]
class TaskPreset(BaseModel): class TaskPreset(BaseModel):