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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ package-lock.json
**/.DS_Store

public/uploads/

# Generated by bin/generate-packages (daily cron); not tracked so it never conflicts on pull
/public/packages.json
/public/packages.json.tmp
60 changes: 60 additions & 0 deletions bin/generate-packages
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env php
<?php

/**
* Rebuilds the Dotkernel packages listing from the GitHub organisation.
*
* Intended to run from cron, e.g. daily:
* 0 4 * * * cd /path/to/dotkernel.com && /usr/bin/php bin/generate-packages >> log/generate-packages.log 2>&1
*
* Exits non-zero without touching the data file when the run cannot be trusted, so the
* previously generated listing keeps serving.
*/

declare(strict_types=1);

use Light\App\Service\PackageGenerator;

chdir(__DIR__ . '/../');

require 'vendor/autoload.php';

$container = require 'config/container.php';
$packageGenerator = $container->get(PackageGenerator::class);

try {
$report = $packageGenerator->write();
} catch (Throwable $exception) {
fwrite(STDERR, sprintf(
'[%s] generate-packages failed: %s%s',
date('c'),
$exception->getMessage(),
PHP_EOL
));
exit(1);
}

printf(
'[%s] Done. %d package%s written to %s%s',
date('c'),
$report['written'],
$report['written'] === 1 ? '' : 's',
$packageGenerator->getDataFile(),
PHP_EOL
);

if ($report['skipped'] !== []) {
printf('Skipped by ignoreRepos: %s%s', implode(', ', $report['skipped']), PHP_EOL);
}

if ($report['ignoredMisses'] !== []) {
printf(
'WARNING: ignoreRepos entries matched nothing (renamed or deleted?): %s%s',
implode(', ', $report['ignoredMisses']),
PHP_EOL
);
}

foreach ($report['warnings'] as $warning) {
printf('WARNING: %s%s', $warning, PHP_EOL);
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"require": {
"php": "~8.5.0",
"ext-curl": "*",
"ext-dom": "*",
"doctrine/data-fixtures": "^2.2",
"doctrine/doctrine-fixtures-bundle": "^4.3",
Expand Down
19 changes: 15 additions & 4 deletions config/autoload/local.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,24 @@ return [
'feed' => [
'path' => realpath(__DIR__ . '/../../public/feed.xml'),
],
'routes' => [
/**
* Credentials for `bin/generate-packages`. Reading public repositories needs no token
* scopes at all; without a token the generator still runs, at a lower rate limit.
* Non-credential settings (ignore list, output path) live in `packages.global.php`.
*/
'github' => [
'userAgent' => 'dotkernel.com',
'authBearer' => '',
'org' => 'dotkernel',
],
// `dotkernel-packages-oss-lifecycle` is intentionally absent: it has a dedicated handler
// and is routed in Light\App\RoutesDelegator. Re-adding it here registers the path twice.
'routes' => [
'page' => [
'contact' => 'contact',
'dotkernel-packages-oss-lifecycle' => 'dotkernel-packages-oss-lifecycle',
'contact' => 'contact',
],
],
'twig' => [
'twig' => [
'globals' => [
'app' => $app,
],
Expand Down
77 changes: 77 additions & 0 deletions config/autoload/packages.global.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/**
* Dotkernel packages listing.
*
* Consumed by `bin/generate-packages`, which queries the GitHub organisation and writes the
* result to `dataFile`. Credentials are intentionally NOT stored here - the GitHub token lives
* in `config/autoload/local.php` under the `github` key, which is ignored by git.
*
* `local.php` is merged after every `*.global.php`, so any value below can be overridden locally.
*/

declare(strict_types=1);

return [
'packages' => [
/**
* ignored repositories
* matched on the bare repository name (case-insensitive)
*/
'ignoreRepos' => [
'.github',
'admin',
'admin-documentation',
'apidemia.com',
'api',
'api-documentation',
'app-packages',
'api-tools-migration',
'core',
'development',
'documentation',
'documentation-theme',
'dotboost',
'dotkernel',
'dotkernel.com',
'dotkernel.github.io',
'dotkernel.org',
'dotkernel-v1',
'dot-opensearch',
'dot-privy',
'dot-queue',
'dot-sso-entra',
'frontend',
'frontend-documentation',
'fullview',
'headless-documentation',
'light',
'light-documentation',
'mezzio-hal',
'ng-admin',
'ngx-admin',
'php-llm-examples',
'pingu',
'plugin-mail-transporter',
'pong',
'presentation',
'queue',
'queue-documentation',
'template',
'template-expressive',
'tutorial-101',
'vue',
'workflow-automatic-releases',
'workflow-continuous-integration',
'workshops',
'ws-entities-collections',
'zend',
'zend-expressive-hal',
'zf1',
],
'dataFile' => __DIR__ . '/../../public/packages.json',
Comment thread
bidi47 marked this conversation as resolved.
'includeArchived' => true,
'timeout' => 10,
'connectTimeout' => 5,
],
];
11 changes: 11 additions & 0 deletions src/App/src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@
use Light\App\Factory\GetFeedViewHandlerFactory;
use Light\App\Factory\GetIndexViewHandlerFactory;
use Light\App\Factory\GetMarkdownArticleHandlerFactory;
use Light\App\Factory\GetPackagesViewHandlerFactory;
use Light\App\Factory\GitHubClientFactory;
use Light\App\Factory\PackageGeneratorFactory;
use Light\App\Handler\GetFeedViewHandler;
use Light\App\Handler\GetIndexViewHandler;
use Light\App\Handler\GetMarkdownArticleHandler;
use Light\App\Handler\GetPackagesViewHandler;
use Light\App\Resolver\EntityListenerResolver;
use Light\App\Service\FeedGenerator;
use Light\App\Service\GitHubClient;
use Light\App\Service\GitHubClientInterface;
use Light\App\Service\PackageGenerator;
use Mezzio\Application;
use Roave\PsrContainerDoctrine\EntityManagerFactory;
use Symfony\Component\Cache\Adapter\AdapterInterface;
Expand Down Expand Up @@ -121,11 +128,15 @@ public function getDependencies(): array
GetIndexViewHandler::class => GetIndexViewHandlerFactory::class,
GetFeedViewHandler::class => GetFeedViewHandlerFactory::class,
GetMarkdownArticleHandler::class => GetMarkdownArticleHandlerFactory::class,
GetPackagesViewHandler::class => GetPackagesViewHandlerFactory::class,
FeedGenerator::class => FeedGeneratorFactory::class,
GitHubClient::class => GitHubClientFactory::class,
PackageGenerator::class => PackageGeneratorFactory::class,
],
'aliases' => [
EntityManager::class => 'doctrine.entity_manager.orm_default',
EntityManagerInterface::class => 'doctrine.entity_manager.orm_default',
GitHubClientInterface::class => GitHubClient::class,
],
];
}
Expand Down
34 changes: 34 additions & 0 deletions src/App/src/Factory/GetPackagesViewHandlerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Light\App\Factory;

use Light\App\Handler\GetPackagesViewHandler;
use Light\App\Service\PackageGenerator;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerInterface;

use function assert;

class GetPackagesViewHandlerFactory
{
public function __invoke(ContainerInterface $container): GetPackagesViewHandler
{
$template = $container->get(TemplateRendererInterface::class);
assert($template instanceof TemplateRendererInterface);

$categoryRepository = $container->get(CategoryRepository::class);
assert($categoryRepository instanceof CategoryRepository);

$postRepository = $container->get(PostRepository::class);
assert($postRepository instanceof PostRepository);

$packageGenerator = $container->get(PackageGenerator::class);
assert($packageGenerator instanceof PackageGenerator);

return new GetPackagesViewHandler($template, $categoryRepository, $postRepository, $packageGenerator);
}
}
36 changes: 36 additions & 0 deletions src/App/src/Factory/GitHubClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Light\App\Factory;

use Light\App\Service\GitHubClient;
use Psr\Container\ContainerInterface;

use function is_array;

class GitHubClientFactory
{
public function __invoke(ContainerInterface $container): GitHubClient
{
$config = $container->get('config');
if (! is_array($config)) {
$config = [];
}
// the token is in `config/autoload/local.php`
// if unauthenticated, the client has a lower call limit
$github = isset($config['github']) && is_array($config['github'])
? $config['github']
: [];
$packages = isset($config['packages']) && is_array($config['packages'])
? $config['packages']
: [];
Comment thread
bidi47 marked this conversation as resolved.

return new GitHubClient(
(string) ($github['authBearer'] ?? ''),
(string) ($github['userAgent'] ?? 'dotkernel.com'),
(int) ($packages['timeout'] ?? 10),
(int) ($packages['connectTimeout'] ?? 5),
);
}
}
50 changes: 50 additions & 0 deletions src/App/src/Factory/PackageGeneratorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Light\App\Factory;

use Light\App\Service\GitHubClientInterface;
use Light\App\Service\PackageGenerator;
use Psr\Container\ContainerInterface;

use function assert;
use function is_array;
use function is_scalar;

class PackageGeneratorFactory
{
public function __invoke(ContainerInterface $container): PackageGenerator
{
$client = $container->get(GitHubClientInterface::class);
assert($client instanceof GitHubClientInterface);

$config = $container->get('config');
if (! is_array($config)) {
$config = [];
}
$github = isset($config['github']) && is_array($config['github'])
? $config['github']
: [];
$packages = isset($config['packages']) && is_array($config['packages'])
? $config['packages']
: [];
Comment thread
bidi47 marked this conversation as resolved.

$ignoreRepos = [];
if (isset($packages['ignoreRepos']) && is_array($packages['ignoreRepos'])) {
foreach ($packages['ignoreRepos'] as $repository) {
if (is_scalar($repository)) {
$ignoreRepos[] = (string) $repository;
}
}
}

return new PackageGenerator(
$client,
(string) ($packages['dataFile'] ?? 'data/packages.json'),
(string) ($github['org'] ?? 'dotkernel'),
$ignoreRepos,
(bool) ($packages['includeArchived'] ?? true),
);
}
}
55 changes: 55 additions & 0 deletions src/App/src/Handler/GetPackagesViewHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Light\App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Light\App\Service\PackageGenerator;
use Light\Blog\Repository\CategoryRepository;
use Light\Blog\Repository\PostRepository;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

use function is_array;
use function is_string;

class GetPackagesViewHandler implements RequestHandlerInterface
{
public const string TEMPLATE = 'page::dotkernel-packages-oss-lifecycle';

public function __construct(
private readonly TemplateRendererInterface $template,
private readonly CategoryRepository $categoryRepository,
private readonly PostRepository $postRepository,
private readonly PackageGenerator $packageGenerator,
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
// no inline regeneration
// data is cached in `public/packages.json` by `bin/generate-packages`
$data = $this->packageGenerator->read();
if (! is_array($data)) {
$data = [];
}
$packages = isset($data['packages']) && is_array($data['packages'])
? $data['packages']
: [];
$generatedAt = isset($data['generated_at']) && is_string($data['generated_at'])
? $data['generated_at']
: null;
Comment thread
bidi47 marked this conversation as resolved.

return new HtmlResponse(
$this->template->render(self::TEMPLATE, [
'posts' => $this->postRepository->getRecentPosts(3),
'categories' => $this->categoryRepository->getCategories(),
'packages' => $packages,
'generatedAt' => $generatedAt,
])
);
}
}
Loading