Skip to content
Open
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
12 changes: 12 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"WebFetch(domain:packagist.org)",
"Bash(composer dump-autoload *)",
"Bash(composer test *)",
"Bash(composer stan *)",
"Bash(vendor/bin/phpstan analyse *)",
"Bash(composer cs-check *)"
]
}
}
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ data/*.data

# Ignore the content of the log folter
logs/*.log

# Tooling caches
.php-cs-fixer.cache
.phpunit.result.cache
.phpunit.cache

# Local PR draft (not part of the codebase)
pr-body.md
21 changes: 21 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

$finder = (new PhpCsFixer\Finder())
->in([__DIR__ . '/src', __DIR__ . '/cli', __DIR__ . '/includes', __DIR__ . '/tests'])
->exclude(['vendor']);

return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setIndent("\t")
->setLineEnding("\n")
->setRules([
'@PSR12' => true,
'declare_strict_types' => true,
'no_unused_imports' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'single_quote' => true,
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
])
->setFinder($finder);
46 changes: 46 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Setup & Run

- PHP 8.5+ required
- Install dependencies: `composer install`
- Configure: `cp includes/constants.dist.php includes/constants.php` and fill in values
- Run the bot: `php cli/jgerman-github-bot.php` (intended to be triggered by a daily cron)

## Tooling

- `composer test` — PHPUnit suite (`tests/Unit/`)
- `composer stan` — PHPStan at `level: max`
- `composer cs-check` / `composer cs-fix` — PHP-CS-Fixer (PSR-12 + tab indent)

## Architecture

This is a single-purpose cron-driven PHP CLI bot. It mirrors merged language-changing PRs from one GitHub repo into translation-request issues on another, then notifies chat channels.

**Entry point flow** (`cli/jgerman-github-bot.php`):
1. Loads `includes/constants.php` (defines `ROOT_PATH` and all `GITHUB_*` / `NOTIFYER_*` config) and Composer autoload.
2. Instantiates `joomlagerman\Helper\Bootstrap`, which wires the configured constants into a typed Registry and constructs the three helpers (`github`, `log`, `notifier`).
3. Reads `data/lastrun.data`. If today's date matches, exits early — **the script self-enforces "run once per day"**, so multiple cron firings on the same day are a no-op.
4. Pulls closed issues since `lastrun` from the source repo with the watch label, filters to those actually merged, creates a translation-request issue in the translation repo for each, fires a notification per created issue, then writes today's date back to `lastrun.data`.

**Helpers** (`src/`, PSR-4 namespace `joomlagerman\Helper\`):
- `Bootstrap` — composes the three helpers from `includes/constants.php` so the CLI script stays thin and the wiring is testable.
- `GithubApiHelper` — wraps `Joomla\Github\Github`. Owns the lastrun + lastrelease state files in `data/`. Key non-obvious behaviors:
- `getClosedAndMergedTranslationIssuesList()` filters by `closed_at == $since` (date-only) to avoid re-processing issues that just had a comment update.
- `createNewTranslationRequestIssueFromMergedTranslationIssue()` fetches the raw diff via a hand-rolled HTTP call (the Joomla GitHub package can't request the `application/vnd.github.v3.diff` Accept header) and embeds it inside `<details>`. If issue creation throws (typically because the diff body is too large), it retries once **without** the diff. The auth token is pulled from the options Registry (`github.authtoken`), not a global constant.
- Branch-label mapping is delegated to `joomlagerman\Enum\JoomlaBranch::labelFor()`. Joomla 5+ caps minors at `.4-dev` before bumping the major (`5.1-dev` … `5.4-dev`, then `6.0-dev`), but Joomla 3 and 4 had multi-digit minors (`3.10-dev`, `4.10-dev`) — the parser handles both forms.
- PRs authored by `joomla-translation-bot` are skipped to avoid loops.
- `pickLatestTag()` is a pure static helper (extracted for testability) that selects the highest `version_compare` tag matching a given `major.minor` branch.
- `NotifyerHelper` — fan-out to Slack, Mattermost, and Telegram via `joomlagerman\Enum\NotificationChannel`. Each channel case carries its own `isEnabled()`/`endpoint()`/`payload()` logic. Accepts an injected `Joomla\Http\Http` for testing; defaults to `(new HttpFactory())->getHttp()`.
- `LogHelper` — appends to `logs/YYYYMM_jgerman.log`.

**Persistent state** lives in `data/*.data` (gitignored): `lastrun.data` for the daily guard, `lastrelease<branch>.data` for release tracking helpers. Deleting `lastrun.data` is the way to force a same-day re-run.

## Conventions

- PHP 8.5+ platform target (`composer.json`); use modern idioms (`readonly`, constructor property promotion, `final`, `match`, enums, strict types).
- All PHP files start with `declare(strict_types=1);`.
- Tab indentation, LF line endings, final newline (`.editorconfig` + `.php-cs-fixer.dist.php`).
- `includes/constants.php` is gitignored — never commit it. Update `includes/constants.dist.php` (and `tools/phpstan-stubs/constants.stub.php` so static analysis sees the constant) whenever a new constant is introduced.
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ J!German Bot

This repo holds the code powering automation for the JGerman Team. As of today it has the following feature: Autocreate issues to the [joomlagerman/joomla](https://github.com/joomlagerman/joomla) repo when an language changing PR at the [joomla/joomla-cms](https://github.com/joomla/joomla-cms) repo got merged (using [@jgerman-bot](https://github.com/jgerman-bot))

## Requirements

- PHP 8.5 or higher
- Composer

## Initial Setup

- `cd /repo/path/jgerman-bot`
- `git clone git@github.com:joomlagerman/jgerman-bot.git .`
- `composer install --no-dev`
- `cp includes/constants.dist.php includes/constants.php`
- `nano includes/constants.php` (Add the access data)
- Setup an daily cronjob on this script: `php cli/jgerman-github-bot.php`
Expand All @@ -17,6 +23,23 @@ This repo holds the code powering automation for the JGerman Team. As of today i

- `cd /repo/path/jgerman-bot`
- `git reset --hard HEAD && git pull origin master`
- `composer install --no-dev`

## Upgrade Notes

The `NOTIFYER_*_ENABED` constants were renamed to `NOTIFYER_*_ENABLED` (typo fix).
After pulling the new version, update your existing `includes/constants.php`:

- `NOTIFYER_SLACK_ENABED` → `NOTIFYER_SLACK_ENABLED`
- `NOTIFYER_MATTERMOST_ENABED` → `NOTIFYER_MATTERMOST_ENABLED`
- `NOTIFYER_TELEGRAM_ENABED` → `NOTIFYER_TELEGRAM_ENABLED`

## Development

- Run tests: `composer test`
- Run static analysis: `composer stan`
- Check code style: `composer cs-check`
- Auto-fix code style: `composer cs-fix`

## Configuration

Expand Down Expand Up @@ -69,7 +92,7 @@ Holds a template of the body to be posted by the bot. This string allows the fol
- `[sourcePullDiff]` => The full diff of the original PR via an `<details>` tab

### Notifyer Config
#### NOTIFYER_SLACK_ENABED
#### NOTIFYER_SLACK_ENABLED

True or False whether the Slack notification should be anabled

Expand All @@ -81,15 +104,15 @@ The webhook URL pointing to the slack channel where the message shoud be send to

THe Slack username the message should be send as

#### NOTIFYER_MATTERMOST_ENABED
#### NOTIFYER_MATTERMOST_ENABLED

True or False whether the Slack notification should be anabled

#### NOTIFYER_MATTERMOST_WEBHOOKURL

The webhook URL pointing to the mattermost channel where the message should be send to

#### NOTIFYER_TELEGRAM_ENABED
#### NOTIFYER_TELEGRAM_ENABLED

True or False whether the Slack notification should be anabled

Expand Down
93 changes: 45 additions & 48 deletions cli/jgerman-github-bot.php
Original file line number Diff line number Diff line change
@@ -1,83 +1,80 @@
<?php

declare(strict_types=1);

/**
* JGerman GitHub Bot based on the Joomla! Framework
*
* @copyright Copyright (C) 2020 J!German (www.jgerman.de) All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/

if (PHP_SAPI != 'cli')
{
use joomlagerman\Helper\Bootstrap;
use joomlagerman\Notification\RunStatus;
use joomlagerman\Notification\RunSummary;

if (PHP_SAPI !== 'cli') {
echo 'This script needs to be called via CLI!' . PHP_EOL;
exit;
}

// Set error reporting for development
error_reporting(-1);

// Load the contstants
require dirname(__DIR__) . '/includes/constants.php';

// Ensure we've initialized Composer
if (!file_exists(ROOT_PATH . '/vendor/autoload.php'))
{
if (!file_exists(ROOT_PATH . '/vendor/autoload.php')) {
exit(1);
}

require ROOT_PATH . '/vendor/autoload.php';

// Load the github base configuration
require dirname(__DIR__) . '/includes/github-base.php';
$services = new Bootstrap();
$summary = new RunSummary();

$services->log->writeLogMessage('Start JGerman GitHub Bot');

$logHelper->writeLogMessage('Start JGerman GitHub Bot');
$notifierHelper->sendLogNotification('Start JGerman GitHub Bot');
$currentRunDateTime = new DateTimeImmutable('now');
$lastRunDate = $services->github->getLatestRunDateTime();

$currentRunDateTime = new DateTime('now');
$lastRunDate = $githubApiHelper->getLatestRunDateTime();
// Self-enforced once-per-day guard.
if ($currentRunDateTime->format('Y-m-d') === $lastRunDate->format('Y-m-d')) {
$services->log->writeLogMessage('We only run once a day so exiting here.');
$services->log->writeLogMessage('End JGerman GitHub Bot');

// Make sure we only run once a day
if ($currentRunDateTime->format('Y-m-d') === $lastRunDate->format('Y-m-d'))
{
$logHelper->writeLogMessage('We only run once a day so exiting here.');
$notifierHelper->sendLogNotification('We only run once a day so exiting here.');
$logHelper->writeLogMessage('End JGerman GitHub Bot');
$notifierHelper->sendLogNotification('End JGerman GitHub Bot');
$summary->setStatus(RunStatus::Noop);
$summary->addLine('Already ran today — skipped.');
$services->notifier->sendRunSummary($summary);
exit;
}

$closedTranslationIssues = $githubApiHelper->getClosedAndMergedTranslationIssuesList($lastRunDate);
$closedTranslationIssues = $services->github->getClosedAndMergedTranslationIssuesList($lastRunDate);
$closedCount = count($closedTranslationIssues);

$logHelper->writeLogMessage('We have ' . count($closedTranslationIssues) . ' closed translation issues since the last run.');
$notifierHelper->sendLogNotification('We have ' . count($closedTranslationIssues) . ' closed translation issues since the last run.');
$services->log->writeLogMessage('We have ' . $closedCount . ' closed translation issues since the last run.');
$summary->addLine('Closed translation issues since last run: ' . $closedCount);

if (!empty($closedTranslationIssues) || !is_array($closedTranslationIssues))
{
$createdTranslationRequestIssues = 0;
$createdTranslationRequestIssues = 0;

// We have issues to check
foreach ($closedTranslationIssues as $translationIssue)
{
$createdIssue = $githubApiHelper->createNewTranslationRequestIssueFromMergedTranslationIssue($translationIssue);
foreach ($closedTranslationIssues as $translationIssue) {
$createdIssue = $services->github->createNewTranslationRequestIssueFromMergedTranslationIssue($translationIssue);

if (!$createdIssue)
{
continue;
}

$notifierHelper->sendMessageTemplateNotification([
'title' => $createdIssue->title,
'issueUrl' => $createdIssue->html_url,
]
);
$createdTranslationRequestIssues++;
if ($createdIssue === null) {
continue;
}

$logHelper->writeLogMessage('We have ' . $createdTranslationRequestIssues . ' translation request issues created.');
$notifierHelper->sendLogNotification('We have ' . $createdTranslationRequestIssues . ' translation request issues created.');
/** @var object{title: string, html_url: string} $createdIssue */
$summary->addCreatedIssue($createdIssue->title, $createdIssue->html_url);
$createdTranslationRequestIssues++;
}

if ($closedCount > 0) {
$services->log->writeLogMessage('We have ' . $createdTranslationRequestIssues . ' translation request issues created.');
$summary->addLine('Created translation requests: ' . $createdTranslationRequestIssues);
}

$logHelper->writeLogMessage('Set the new latest run date to: ' . $currentRunDateTime->format('Y-m-d'));
$notifierHelper->sendLogNotification('Set the new latest run date to: ' . $currentRunDateTime->format('Y-m-d'));
$githubApiHelper->setLatestRunDateTime($currentRunDateTime);
$logHelper->writeLogMessage('End JGerman GitHub Bot');
$notifierHelper->sendLogNotification('End JGerman GitHub Bot');
$services->log->writeLogMessage('Set the new latest run date to: ' . $currentRunDateTime->format('Y-m-d'));
$summary->addLine('Last run date set to: ' . $currentRunDateTime->format('Y-m-d'));
$services->github->setLatestRunDateTime($currentRunDateTime);

$services->log->writeLogMessage('End JGerman GitHub Bot');
$services->notifier->sendRunSummary($summary);
29 changes: 25 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,37 @@
"config": {
"optimize-autoloader": true,
"platform": {
"php": "7.3.0"
"php": "8.5.0"
}
},
"require": {
"joomla/github": "^1.7",
"joomla/http": "~1.3"
"php": "^8.5",
"joomla/github": "^4.0",
"joomla/http": "^4.0",
"joomla/registry": "^4.0",
"joomla/uri": "^4.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^2.0",
"friendsofphp/php-cs-fixer": "^3.60"
},
"autoload": {
"psr-4": {
"joomlagerman\\Helper\\": "src/"
"joomlagerman\\Helper\\": "src/",
"joomlagerman\\Enum\\": "src/Enum/",
"joomlagerman\\Notification\\": "src/Notification/"
}
},
"autoload-dev": {
"psr-4": {
"joomlagerman\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"stan": "phpstan analyse",
"cs-check": "php-cs-fixer fix --dry-run --diff",
"cs-fix": "php-cs-fixer fix"
}
}
12 changes: 7 additions & 5 deletions includes/constants.dist.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<?php

declare(strict_types=1);

/**
* JGerman Bot Configuration
*
Expand All @@ -20,16 +23,15 @@
define('GITHUB_TRANSLATION_OWNER', '');
define('GITHUB_TRANSLATION_REPO', '');
define('GITHUB_TRANSLATION_LABEL', '');
define('GITHUB_TRANSLATION_ASSIGMENTS', '');
define('GITHUB_TRANSLATION_ASSIGMENTS', []);
define('GITHUB_TRANSLATION_TEMPLATE_BODY', '');

// Notifyer Config
define('NOTIFYER_SLACK_ENABED', '');
define('NOTIFYER_SLACK_ENABLED', false);
define('NOTIFYER_SLACK_WEBHOOKURL', '');
define('NOTIFYER_SLACK_USERNAME', '');
define('NOTIFYER_MATTERMOST_ENABED', '');
define('NOTIFYER_MATTERMOST_ENABLED', false);
define('NOTIFYER_MATTERMOST_WEBHOOKURL', '');
define('NOTIFYER_TELEGRAM_ENABED', '');
define('NOTIFYER_TELEGRAM_ENABLED', false);
define('NOTIFYER_TELEGRAM_BOTTOKEN', '');
define('NOTIFYER_TELEGRAM_CHATID', '');
define('NOTIFYER_GITHUB_ISSUE_MESSAGE_TEMPLATE', '');
Loading