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
31 changes: 29 additions & 2 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,37 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

env:
REGISTRY: ghcr.io
IMAGE_NAME: mrmigles/py-manager

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: pip install -r requirements-dev.txt

- name: Run unit tests
run: pytest -v

build-and-push:
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
Expand All @@ -21,25 +43,30 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

# Not needed (and not possible for fork PRs) when we're only building,
# not pushing.
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push image
# On pull requests this just validates the image builds - it's never pushed.
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: true
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

deploy:
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name != 'pull_request'

steps:
- name: Checkout
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
*.pyc
.pytest_cache/
.venv/
venv/
*.egg-info/
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
DATA_DIR=/data

COPY main.py .
# git is needed to clone/sync GitHub-based apps
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*

# python-telegram-bot & psutil are installed system-wide: every per-app venv is
# created with --system-site-packages, so this is the one thing shared across apps.
COPY requirements.txt .
RUN python -m pip install --no-cache-dir -r requirements.txt

RUN python -m pip install --no-cache-dir python-telegram-bot==21.6 psutil
COPY main.py .

VOLUME ["/data"]

Expand Down
95 changes: 77 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# PyManager 🤖🧰
A Telegram bot to manage Python scripts from chat: upload/edit, run/stop/restart, logs, env vars, autostart, version rollback, and basic monitoring.
A Telegram bot to manage Python scripts and apps from chat: upload/edit, run/stop/restart, logs, env vars, autostart, version rollback, and basic monitoring.

![alt text](image.png)

Expand All @@ -8,22 +8,49 @@ Designed for **single-owner** usage (restricted by `OWNER_ID`).
---

## Features
- **Scripts**: upload `.py` as a file or paste as text, run/stop/restart, tail logs
- **ENV**: global env + per-script env, optional env key detection from code
- **Autostart**: run selected scripts automatically when the manager starts
- **Rollback**: keeps **last 10 versions** per script with timestamps, one-click rollback
- **Monitoring**: `/monitoring` shows CPU/MEM for running scripts (**psutil**)
- **📄 Scripts**: upload `.py` as a file or paste as text, run/stop/restart, tail logs
- **📦 Apps from archives**: send a `.zip` / `.tar.gz` / `.tgz` / `.tar` / `.7z` file
- Auto-extracted; the app name is the archive name (without extension)
- Auto-detects `requirements.txt` and offers to install it
- Auto-detects the entry point: `main.py` if present, the single `.py` file if there's
only one, or asks you which file to run otherwise
- Gets its **own isolated virtualenv** so its dependencies never affect other apps
- **🌐 Apps from GitHub repos**: just paste a link (or use `/repo <url>`)
- `https://github.com/owner/repo` — imports the whole repo, name = repo name
- `https://github.com/owner/repo/tree/branch/subdir` — imports only `subdir`,
name = `repo-subdir`
- **🔄 Sync** button re-pulls the repo, offers to install any new dependencies,
and restarts the app
- **🧪 ENV**: global env + per-script/app env, optional env key detection from code
(scans all `.py` files for apps, same detection logic as before for scripts)
- **🚀 Autostart**: run selected scripts/apps automatically when the manager starts
- **⏪ Rollback**: keeps **last 10 versions** per script/app with timestamps, one-click rollback
- **📈 Monitoring**: `/monitoring` shows CPU/MEM for running scripts/apps (**psutil**)

### Isolation model
- Each **app** (from an archive or a GitHub repo) gets its own Python virtualenv under
`venvs/<app_id>/`, created with `--system-site-packages`. This means:
- Installing a dependency for one app **never** impacts another app.
- The only thing shared between all apps (and the manager itself) is whatever is
installed system-wide in the image — i.e. `python-telegram-bot` and `psutil`.
- **Legacy single-file scripts** (`.py` upload/paste) keep working exactly as before:
they share the manager's own interpreter and `requirements.txt`. This keeps existing
setups 100% backward-compatible — nothing changes for scripts you already have.

---

## Data layout
Stored under `DATA_DIR` (default `/data`, mounted as a volume in Docker):

- `scripts/` — current scripts (`<name>.py`)
- `versions/<script_id>/` — saved versions (`<script_id>_<timestamp>.py`)
- `logs/` — per-script logs (`<script_id>.log`)
- `meta.json` — scripts metadata (env/autostart/versions)
- `requirements.txt` — saved dependencies (from `/pip ...`)
- `scripts/` — current legacy scripts (`<name>.py`)
- `versions/<script_id>/` — saved script versions (`<script_id>_<timestamp>.py`)
- `apps/<app_id>/` — current app source tree (from an archive or a GitHub repo)
- `app_versions/<app_id>/` — saved app versions (`<app_id>_<timestamp>.tar.gz`)
- `venvs/<app_id>/` — isolated virtualenv per app
- `gitrepos/<app_id>/` — working git clone backing a GitHub app (used by Sync)
- `logs/` — per-script/app logs (`<id>.log`)
- `meta.json` — scripts/apps metadata (type/env/autostart/versions/entry/...)
- `requirements.txt` — shared dependencies for legacy scripts (from `/pip ...`)

---

Expand Down Expand Up @@ -70,19 +97,51 @@ docker compose down
---

## Main bot commands
- `/menu` — interactive scripts menu (buttons)
- `/new <name>` — create script (send code as the next message)
- `/run <id>` — start script
- `/stop <id>` — stop script
- `/menu` — interactive scripts/apps menu (buttons)
- `/list` — rich status list of all scripts/apps
- `/new <name>` — create a script (send code as the next message)
- `/repo <github_url>` — import an app from a GitHub repo (or just paste the link)
- `/run <id>` — start script/app
- `/stop <id>` — stop script/app
- `/logs <id>` — show last log lines
- `/monitoring` — CPU/MEM for running scripts (install psutil if needed)
- `/pip <args>` — install packages and save into `requirements.txt`
- `/monitoring` — CPU/MEM for running scripts/apps (install psutil if needed)
- `/pip <args>` — install packages (shared `requirements.txt` for scripts, per-app venv + `requirements.txt` for apps)

### Importing an app
- Just **send an archive file** (`.zip`/`.tar.gz`/`.tgz`/`.tar`/`.7z`) — the bot unpacks it,
detects `requirements.txt` and the entry point, and gets you set up.
- Or **send a GitHub link** as a plain message, e.g.:
- `https://github.com/owner/repo`
- `https://github.com/owner/repo/tree/main/subdir`
- Legend in menus: `📄` script · `📦` archive app · `🌐` GitHub app.

---

## Notes
- Access is restricted to **OWNER_ID**.
- Dependencies installed via `/pip` are persisted in `requirements.txt` and installed on manager startup.
- Dependencies installed via `/pip` for legacy scripts are persisted in `/data/requirements.txt`
and installed on manager startup; for apps they go into the app's own venv +
its own `requirements.txt`. (Not to be confused with the repo-root `requirements.txt`,
which lists the manager's own runtime dependencies and is installed in the `Dockerfile`.)
- `git` and `py7zr` are required in the runtime image to clone repos / extract `.7z`
archives — already included in the provided `Dockerfile`.

---

## Development & tests
Unit tests cover the venv isolation, archive extraction (incl. path-traversal
safety), entry-point detection, GitHub import/sync, BWC of legacy scripts, and
version/rollback handling (including the timestamp-collision edge case).

Run them locally:
```bash
pip install -r requirements-dev.txt
pytest -v
```

The `test` job in `.github/workflows/build-and-deploy.yml` runs the full suite
on every push to `main`; `build-and-push` (and therefore `deploy`) only runs
if it's green.

---

Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ services:
image: ghcr.io/mrmigles/py-manager:latest
container_name: script-bot
restart: unless-stopped
pids_limit: 512
security_opt:
- seccomp=unconfined

environment:
# required
Expand Down
Loading
Loading