Monitor PHP package versions on Packagist and receive Slack notifications when new versions are released.
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ config.yml │────>│ Packagist API │────>│ Compare with │
│ (packages) │ │ (fetch latest) │ │ stored version │
└──────────────┘ └──────────────────┘ └────────┬────────┘
│
┌────────▼────────┐
│ New version? │
│ Send Slack msg │
└─────────────────┘
- Reads the list of packages from
config.yml - Queries the Packagist API for the latest version of each package
- Compares with the last known version (stored locally in
versions/) - If a new version is detected, sends a Slack notification
python main.py performs a single pass over all packages and then exits —
main.py itself has no loop or scheduler. Recurring checks are handled
outside the application:
-
Docker Compose: the
version-checkerservice's entrypoint wrapspython main.pyin a shell loop that sleeps forCHECK_INTERVALseconds between passes (default: 900 seconds / 15 minutes).CHECK_INTERVALis read by that shell loop, not bymain.py. -
Native (no Docker): run
python main.pyonce per interval using your own scheduler, e.g. cron:*/15 * * * * cd /path/to/packagist-tracker && python main.py
Note on first run: with an empty (or missing)
versions/directory, every package has no stored last version, so the first pass sends a Slack notification for every package inconfig.yml. This is expected — after that first pass, only new versions trigger notifications.
Go to Slack API Apps and create a new app (or use an existing one). The app needs the chat:write scope to send notifications.
Copy the example file and fill in your values:
cp .env.example .envEdit .env with your Slack token and channel ID:
SLACK_TOKEN=xoxb-your-token-here
SLACK_CHANNEL=C0123456789
Copy the example config and add your packages:
cp config.yml.example config.ymlEdit config.yml:
packages:
- symfony/symfony
- laravel/framework
- monolog/monologdocker compose up -dconfig.yml is mounted read-only into the container instead of being baked
into the image, so you can edit your tracked packages on the host and restart
the container to pick up changes. Version state is persisted through the
./versions bind mount.
Note on permissions: the container runs as a non-root
appuser. If the host./versionsdirectory is owned by another user, the container may not be able to write to it. Prefer one of these safer options over opening up permissions:
- Set
user: "${UID}:${GID}"on theversion-checkerservice indocker-compose.ymlso the container runs as your host user, andexport UID GID(or set them in.env) before runningdocker compose up.- Use a Docker named volume instead of a host bind mount for
./versions, letting Docker manage ownership.As a last resort, and only if the options above aren't feasible,
chownthe directory to a matching UID orchmod 777 ./versions— the latter grants write access to any local user and process, so avoid it where possible.
| Variable | Description | Default | Read by |
|---|---|---|---|
SLACK_TOKEN |
Slack Bot OAuth token | — | main.py |
SLACK_CHANNEL |
Slack channel ID for notifications | — | main.py |
CHECK_INTERVAL |
Seconds between checks, used by the Compose entrypoint's loop | 900 |
docker-compose.yml entrypoint and healthcheck |
LOG_LEVEL |
Logging level (DEBUG/INFO/WARNING) | INFO |
main.py |
HEARTBEAT_FILE |
Path touched after each completed check cycle, used for the container healthcheck | /tmp/last_run |
main.py and docker-compose.yml healthcheck |
CHECK_INTERVAL is a Docker Compose setting, not an application setting:
main.py never reads it. It only controls the sleep duration in the
container's shell loop (see How it works above). Running
main.py directly ignores it entirely.
Container health and shutdown: the
version-checkerservice setsinit: trueso Docker runs tini as PID 1. Without it, thesh -c "while true; ..."entrypoint doesn't forwardSIGTERM, so everydocker compose stopwould wait out the full stop timeout and getSIGKILLed instead of exiting promptly. The service also defines ahealthcheckthat checks whetherHEARTBEAT_FILEexists and was modified within roughly the last twoCHECK_INTERVALs;main.pytouches that file at the end of every completed check cycle (regardless of per-package success or failure), so the healthcheck can catch a wedged process even though the outer shell loop keeps the container itself running.
pip install -r requirements-dev.txtpytestruff check .
ruff format .mypy main.py testspre-commit installpackagist-tracker/
├── .github/
│ ├── workflows/ci.yml # CI pipeline (lint, test, docker build)
│ └── dependabot.yml # Automated dependency updates
├── tests/
│ └── test_main.py # Unit tests
├── main.py # Application entry point
├── config.yml # Packages to track (user-created)
├── config.yml.example # Example configuration
├── docker-compose.yml # Docker Compose orchestration
├── Dockerfile # Container definition
├── requirements.txt # Production dependencies
├── requirements-dev.txt # Development dependencies
└── pyproject.toml # Project metadata and tool config