6 Commits
Author SHA1 Message Date
shmyga 244f995490 ci(action): add gitea actions
test / test (push) Successful in 2m18s
2026-09-30 15:03:56 +03:00
shmyga 75adb282e7 ci(version): 0.3.1 2026-09-29 19:24:02 +03:00
shmyga eacfa17e40 feat(preset): add '--all-extras' flag to poetry install command 2026-09-29 19:23:41 +03:00
shmyga f4448835ab docs: update README 2026-09-15 19:35:00 +03:00
shmyga 732cec8771 ci(version): 0.3.0 2026-09-15 17:21:45 +03:00
shmyga 5cf71fcae1 feat: update default presets 2026-09-15 17:21:23 +03:00
18 changed files with 150 additions and 27 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
+20 -7
View File
@@ -1,9 +1,17 @@
# Project Manager # Project Manager
Automatically detects modules and executes commands
Supported types:
- python (poetry)
- nodejs (npm)
- docker
## Install ## Install
```bash ```bash
pipx install project-manager -i https://git.shmyga.ru/InfernalGames/project-manager/packages pipx install project-manager -i https://git.shmyga.ru/api/packages/InfernalGames/pypi/simple/ --pip-args="--extra-index-url https://pypi.org/simple"
``` ```
## Usage ## Usage
@@ -14,17 +22,16 @@ pipx install project-manager -i https://git.shmyga.ru/InfernalGames/project-mana
pm [-v] [-vv] pm [-v] [-vv]
``` ```
![pm show](docs/Screenshot_0.png "pm show")
### Run task ### Run task
```bash ```bash
pm <task>[:<tag>] [-v] [-vv] pm <task>[:<tag>][:<sub_tag>] [-v] [-vv]
``` ```
## Presets locations ![pm lint](docs/Screenshot_1.png "pm lint")
![pm build](docs/Screenshot_2.png "pm build")
- default presets [`project_manager/preset/default/*.toml`](./project_manager/preset/default)
- user preset `$HOME/.pm.toml`
- project preset `./.pm.toml`
## Preset format ## Preset format
@@ -39,3 +46,9 @@ commands = ["<command>"]
### Examples ### Examples
[Default presets](./project_manager/preset/default) [Default presets](./project_manager/preset/default)
## Presets locations
- default presets [`project_manager/preset/default/*.toml`](./project_manager/preset/default)
- user preset `$HOME/.pm.toml`
- project preset `./.pm.toml`
+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
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+11 -1
View File
@@ -3,6 +3,7 @@ import os
import re import re
import tomllib import tomllib
from pathlib import Path from pathlib import Path
from typing import Any
import dotenv import dotenv
from jinja2 import Environment from jinja2 import Environment
@@ -11,13 +12,21 @@ from ..preset.model import Variables
def do_bool(data: str | None) -> bool: def do_bool(data: str | None) -> bool:
return not (data is None or data.lower() in ["false", "null", "none", "0", ""]) if data is None:
return False
if not isinstance(data, str):
data = str(data)
return data.lower() not in ["false", "null", "none", "0", ""]
def do_env(key: str, default: str | None = None) -> str | None: def do_env(key: str, default: str | None = None) -> str | None:
return os.environ.get(key=key.upper(), default=default) return os.environ.get(key=key.upper(), default=default)
def do_not(value: Any | None) -> bool:
return not do_bool(str(value))
def _build_env(basepath: Path) -> Environment: def _build_env(basepath: Path) -> Environment:
env = Environment() env = Environment()
@@ -71,6 +80,7 @@ def _build_env(basepath: Path) -> Environment:
env.filters["key"] = do_key env.filters["key"] = do_key
env.filters["bool"] = do_bool env.filters["bool"] = do_bool
env.filters["env"] = do_env env.filters["env"] = do_env
env.filters["not"] = do_not
return env return env
+4 -4
View File
@@ -85,20 +85,20 @@ class Entry:
for command in task.commands: for command in task.commands:
self._logger.debug(" %s", command) self._logger.debug(" %s", command)
def run_task(self, key: PartialTaskKey) -> bool: def run_task(self, key: PartialTaskKey) -> int:
if key.name == "show": if key.name == "show":
self.show() self.show()
return True return True
runned = False c = 0
for task in self._tasks: for task in self._tasks:
if key == task.key: if key == task.key:
self._logger.info("🔵 %s %s", task.key, self) self._logger.info("🔵 %s %s", task.key, self)
runned = True
for command in task.commands: for command in task.commands:
CommandExecutor(command).run( CommandExecutor(command).run(
(task.cwd and Path(task.cwd)) or self._basepath (task.cwd and Path(task.cwd)) or self._basepath
) )
return runned c += 1
return c
def __repr__(self): def __repr__(self):
path = self._basepath.relative_to(self._roopath.parent) path = self._basepath.relative_to(self._roopath.parent)
+1 -1
View File
@@ -9,7 +9,7 @@ from .entry import Entry
class EntryResolver: class EntryResolver:
def __init__(self, detector: EntryDetector, ignore: PathSpec, max_depth: int = 4): def __init__(self, detector: EntryDetector, ignore: PathSpec, max_depth: int = 2):
self._detector = detector self._detector = detector
self._ignore = ignore self._ignore = ignore
self._max_depth = max_depth self._max_depth = max_depth
+5 -5
View File
@@ -1,6 +1,6 @@
import logging import logging
from collections import Counter from collections import Counter
from enum import StrEnum from enum import StrEnum, auto
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable
@@ -14,8 +14,8 @@ logger = logging.getLogger("project")
class ResultType(StrEnum): class ResultType(StrEnum):
SUCCESS = "success" SUCCESS = auto()
FAILED = "failed" FAILED = auto()
RESULT_ICONS = { RESULT_ICONS = {
@@ -40,8 +40,8 @@ class ProjectManager:
counter = Counter() counter = Counter()
for entry in self._iter_entries(): for entry in self._iter_entries():
try: try:
if entry.run_task(key): if count := entry.run_task(key):
counter[ResultType.SUCCESS] += 1 counter[ResultType.SUCCESS] += count
except CommandError as error: except CommandError as error:
self._logger.error("%s\n%s", entry, error.output) self._logger.error("%s\n%s", entry, error.output)
counter[ResultType.FAILED] += 1 counter[ResultType.FAILED] += 1
@@ -0,0 +1,4 @@
[presets.config.tasks.setup.config]
priority = 1
conditions = ["{{ ('.env-base' | file) and ('.env' | file | not ) }}"]
commands = ["cp .env-base .env"]
@@ -34,3 +34,13 @@ commands = [
"mkdir -p {{ docker_output }}", "mkdir -p {{ docker_output }}",
"docker save --output {{ docker_output }}/{{ docker_name }}-{{ docker_version }}.tar {{ docker_image }}", "docker save --output {{ docker_output }}/{{ docker_name }}-{{ docker_version }}.tar {{ docker_image }}",
] ]
# run
[presets.docker.tasks.run.compose]
conditions = ["{{ 'docker-compose.yaml' | file }}"]
commands = ["docker compose -f docker-compose.yaml up"]
# develop
[presets.docker.tasks.develop.compose]
conditions = ["{{ 'docker-compose-develop.yaml' | file }}"]
commands = ["docker compose -f docker-compose-develop.yaml up --build --watch"]
+8 -6
View File
@@ -1,30 +1,32 @@
[presets.nodejs] [presets.nodejs]
conditions = ["{{ 'package.json' | file }}"] conditions = ["{{ 'package.json' | file }}"]
variables.use_nvm = "{{ (((('home' | env) + '/.nvm/nvm.sh') | file) and ('.nvmrc' | file)) | bool }}"
variables.nvm = "{{ '. ~/.nvm/nvm.sh && nvm use && ' if use_nvm | bool else '' }}"
# install # install
[presets.nodejs.tasks.setup.npm] [presets.nodejs.tasks.setup.npm]
conditions = ["{{ 'package-lock.json' | file }}"] conditions = ["{{ 'package-lock.json' | file }}"]
commands = ["npm install-clean"] commands = ["{{ nvm }}npm install-clean"]
# install legacy # install legacy
[presets.nodejs.tasks.setup.npm-legacy] [presets.nodejs.tasks.setup.npm-legacy]
conditions = ["{{ not ('package-lock.json' | file) }}"] conditions = ["{{ not ('package-lock.json' | file) }}"]
commands = ["npm install"] commands = ["{{ nvm }}npm install"]
# test # test
[presets.nodejs.tasks.test.jest] [presets.nodejs.tasks.test.jest]
conditions = ["{{ 'node_modules/jest' | file }}"] conditions = ["{{ 'node_modules/jest' | file }}"]
commands = ["npx jest"] commands = ["{{ nvm }}npx jest"]
# build # build
[presets.nodejs.tasks.build.npm] [presets.nodejs.tasks.build.npm]
conditions = ["{{ 'package.json' | read | get('scripts.build') }}"] conditions = ["{{ 'package.json' | read | get('scripts.build') }}"]
commands = ["npm run build"] commands = ["{{ nvm }}npm run build"]
# publish # publish
[presets.nodejs.tasks.publish.npm] [presets.nodejs.tasks.publish.npm]
conditions = ["{{ 'package.json' | read | get('scripts.publish') }}"] conditions = ["{{ 'package.json' | read | get('scripts.publish') }}"]
commands = ["npm run publish"] commands = ["{{ nvm }}npm run publish"]
# clean # clean
[presets.nodejs.tasks.clean.node_modules] [presets.nodejs.tasks.clean.node_modules]
@@ -34,4 +36,4 @@ commands = ["rm -rf node_modules"]
# version # version
[presets.nodejs.tasks.version.npm] [presets.nodejs.tasks.version.npm]
conditions = ["{{ version }}"] conditions = ["{{ version }}"]
commands = ["npm version {{ version }} --allow-same-version"] commands = ["{{ nvm }}npm version {{ version }} --allow-same-version"]
+6 -1
View File
@@ -6,7 +6,12 @@ variables.python_poetry = "{{ 'pyproject.toml' | read | get('build-system.build-
[presets.python.tasks.setup.poetry] [presets.python.tasks.setup.poetry]
conditions = ["{{ python_poetry }}"] conditions = ["{{ python_poetry }}"]
variables.python_version = "{{ 'pyproject.toml' | read | get('project.requires-python') | search('(\\d+.\\d+)') }}" variables.python_version = "{{ 'pyproject.toml' | read | get('project.requires-python') | search('(\\d+.\\d+)') }}"
commands = ["poetry env use {{ python_version }}", "poetry install"] commands = ["poetry env use {{ python_version }}", "poetry install --all-extras"]
# playwright
[presets.python.tasks.setup.playwright]
conditions = ["{{ '.venv/bin/playwright' | file }}"]
commands = [".venv/bin/playwright install chromium --with-deps --only-shell"]
# build # build
[presets.python.tasks.build.poetry] [presets.python.tasks.build.poetry]
+9 -1
View File
@@ -14,9 +14,14 @@ 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
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()
if preset.modules:
modules = [Path(item).resolve() for item in preset.modules]
if basepath not in modules:
return ENTRY_SKIP
if preset.skip: if preset.skip:
return ENTRY_SKIP return ENTRY_SKIP
tasks = [] tasks = []
@@ -25,7 +30,10 @@ class PresetEntryDetector(EntryDetector):
preset.presets.items(), key=lambda item: item[1].priority, reverse=True preset.presets.items(), key=lambda item: item[1].priority, reverse=True
) )
for tag, preset in presets: for tag, preset in presets:
preset_variables: Variables = {**global_variables, "entry_path": basepath} preset_variables: Variables = {
**global_variables,
"entry_path": str(basepath),
}
if preset.variables: if preset.variables:
if tag == "*": if tag == "*":
template.update_variables(preset.variables, global_variables) template.update_variables(preset.variables, global_variables)
+1
View File
@@ -20,4 +20,5 @@ class EntryPreset(BaseModel):
class Preset(BaseModel): class Preset(BaseModel):
skip: bool | None = None skip: bool | None = None
modules: list[str] | None = None
presets: dict[str, EntryPreset] presets: dict[str, EntryPreset]
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "project-manager" name = "project-manager"
description = "" description = ""
version = "0.2.0" version = "0.3.1"
readme = "README.md" readme = "README.md"
authors = [{ name = "shmyga", email = "shmyga.z@gmail.com" }] authors = [{ name = "shmyga", email = "shmyga.z@gmail.com" }]
requires-python = ">=3.14,<4.0" requires-python = ">=3.14,<4.0"