Skip to content

Commit

Permalink
Make things work including tests
Browse files Browse the repository at this point in the history
  • Loading branch information
loevgaard committed Oct 3, 2024
1 parent bc0aa6e commit 360a071
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 57 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,43 @@ jobs:

- name: "Static analysis"
run: "composer analyse"
unit-tests:
name: "Unit tests"

runs-on: "ubuntu-latest"

strategy:
matrix:
php-version:
- "8.1"
- "8.2"
- "8.3"

dependencies:
- "lowest"
- "highest"

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

- name: "Build Docker image"
run: "docker build -t setono/deployer-cron --no-cache ./tests/docker"

- name: "Run Docker container"
run: "docker run -d -p 2222:22 setono/deployer-cron"

- name: "Setup PHP, with composer and extensions"
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
extensions: "${{ env.PHP_EXTENSIONS }}"
php-version: "${{ matrix.php-version }}"

- name: "Install composer dependencies"
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "${{ matrix.dependencies }}"

- name: "Run phpunit"
run: "vendor/bin/phpunit"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/vendor/
/composer.lock
/.phpunit.result.cache
/.phpunit.cache
/build/
61 changes: 34 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Simple handling of cronjobs in your deployment process using the [Cron builder l
## Installation

```bash
$ composer require setono/deployer-cron
composer require setono/deployer-cron
```

## Usage
The easiest usage is to include the cron recipe which hooks into default Deployer events:
The easiest usage is to include the cron recipe which hooks into default Deployer lifecycle:

```php
<?php
Expand All @@ -30,48 +30,55 @@ The following Deployer parameters are defined:
| Parameter | Description | Default value |
|-------------------------|--------------------------------------------------------------------------------------------------|-----------------------------------------------|
| cron_config_dir | The directory to search for cronjob config files | `etc/cronjobs` |
| cron_delimiter | The marker in the crontab file that delimits the generated cronjobs from manually added cronjobs | `{{application}} ({{stage}})` |
| cron_delimiter | The marker in the crontab file that delimits the generated cronjobs from manually added cronjobs | The stage. If not set, the default is `prod`. |
| cron_user | The user onto which the crontab should be added (default is `remote_user`) | `get('http_user')` if you are root, else `''` |

## Build context
## Cron builder context

The default build context is defined in the Deployer parameter `cron_context`. It adds the stage as context which means
you can use the `condition` key in your cronjob config:
The cron builder context is set to the Deployer configuration parameters. This means you can use variables in your
cronjob config files. For example:

```yaml
# /etc/cronjobs/jobs.yaml
```php
<?php
# etc/cronjobs/jobs.php

- schedule: "0 0 * * *"
command: "%php_bin% %release_path%/bin/console my:dev:command"
condition: "context.stage === 'dev'"
```
declare(strict_types=1);

use Setono\CronBuilder\Context;
use Setono\CronBuilder\CronJob;

The above cronjob will only be added to the final cron file if the deployment stage equals `dev`.
return static function (Context $context): iterable {
yield new CronJob('0 0 * * *', '/usr/bin/php {{ release_path }}/send-report.php', 'Run every day at midnight');

## Extra variables available
if ($context->get('stage') === 'prod') {
yield new CronJob('0 0 * * *', '/usr/bin/php {{ release_path }}/process.php');
}
};
```

Notice the usage of `release_path` and `stage` in the cronjob config file.

This library also adds more variables you can use in your cronjob configs:
## Testing

- `%application%`: Will output the application name
- `%stage%`: Will output the stage, i.e. `dev`, `staging`, or `prod`
- `%php_bin%`: Will output the path to the PHP binary
- `%release_path%`: Will output the release path on the server
1. Build the Docker image:

With these variables you can define a cronjob like:
```shell
docker build -t setono/deployer-cron --no-cache ./tests/docker
```

```yaml
# /etc/cronjobs/jobs.yaml
2. Run the Docker container:

- schedule: "0 0 * * *"
command: "%php_bin% %release_path%/bin/console my:command"
```shell
docker run -d -p 2222:22 setono/deployer-cron
```

And that will translate into the following line in your crontab:
3. Run the tests:

```text
0 0 * * * /usr/bin/php /var/www/your_application/releases/23/bin/console my:command
```shell
vendor/bin/phpunit
```


[ico-version]: https://poser.pugx.org/setono/deployer-cron/v/stable
[ico-unstable-version]: https://poser.pugx.org/setono/deployer-cron/v/unstable
[ico-license]: https://poser.pugx.org/setono/deployer-cron/license
Expand Down
7 changes: 3 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"webmozart/assert": "^1.10"
},
"require-dev": {
"phpseclib/phpseclib": "^3.0",
"phpunit/phpunit": "^10.5",
"setono/code-quality-pack": "^2.8.2"
},
"prefer-stable": true,
Expand All @@ -33,10 +35,7 @@
"psr-4": {
"Setono\\Deployer\\Cron\\": "tests/",
"Deployer\\": "vendor/deployer/deployer/src/"
},
"files": [
"vendor/deployer/deployer/src/functions.php"
]
}
},
"config": {
"allow-plugins": {
Expand Down
15 changes: 15 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php"
colors="true" cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Deployer Cron Testsuite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
14 changes: 14 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,18 @@
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<UndefinedFunction>
<errorLevel type="suppress">
<!-- This could be fixed by adding the Deployer's functions.php to our autoload, but that makes our deploy.php file not work -->
<referencedFunction name="Deployer\after"/>
<referencedFunction name="Deployer\before"/>
<referencedFunction name="Deployer\get"/>
<referencedFunction name="Deployer\run"/>
<referencedFunction name="Deployer\set"/>
<referencedFunction name="Deployer\task"/>
<referencedFunction name="Deployer\upload"/>
</errorLevel>
</UndefinedFunction>
</issueHandlers>
</psalm>
4 changes: 2 additions & 2 deletions src/recipe/setono_cron.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
before('deploy:prepare', 'cron:prepare');

// apply the cron just before symlinking. This is where the release_path is available
before('deploy:symlink', 'cron:build');
before('deploy:symlink', 'cron:apply');

// cleanup created files
after('cleanup', 'cron:cleanup');
after('deploy:cleanup', 'cron:cleanup');
after('deploy:failed', 'cron:cleanup');
69 changes: 46 additions & 23 deletions src/task/setono_cron.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Deployer\Deployer;
use function Deployer\get;
use function Deployer\parse;
use function Deployer\run;
use function Deployer\set;
use function Deployer\task;
Expand All @@ -18,8 +17,22 @@

set('cron_config_dir', 'etc/cronjobs');
set('cron_delimiter', static function (): string {
return parse('{{application}} ({{stage}})');
$labels = get('labels');
if (!is_array($labels)) {
return 'prod';
}

if (!isset($labels['stage'])) {
return 'prod';
}

$stage = $labels['stage'];
Assert::stringNotEmpty($stage);

return $stage;
});
set('crontab_filename', 'crontab.txt');
set('crontab_backup_filename', 'crontab.backup.txt');

// If you're deploying as root you have the option to edit other users' crontabs
// So this parameter is the http_user if you're deploying as root else we don't set it
Expand All @@ -37,7 +50,6 @@
task('cron:prepare', [
'cron:validate',
'cron:backup',
'cron:build',
]);

task('cron:validate', static function (): void {
Expand All @@ -55,10 +67,10 @@
return;
}

file_put_contents('crontab.backup.txt', $crontab); // todo allow to set the backup file name
file_put_contents(get('crontab_backup_filename'), $crontab);
})->desc('Backups the old crontab and stores it locally');

task('cron:build', static function (): void {
task('cron:apply', static function (): void {
$cronUser = getCronUser();

$cronBuilder = (new CronBuilder())
Expand All @@ -71,35 +83,46 @@
)
;

$config = Deployer::get()->config->ownValues();
$config = [];
foreach (Deployer::get()->config->ownValues() as $key => $value) {
if (is_callable($value)) {
continue;
}

$config[$key] = get($key);
}

if (Context::has()) {
$context = Context::get();
if (false !== $context) {
$config = array_merge($config, $context->getConfig()->ownValues());
foreach ($context->getConfig()->ownValues() as $key => $value) {
if (is_callable($value)) {
continue;
}

$config[$key] = get($key);
}
}
}

foreach ($config as $key => $value) {
$cronBuilder->addContext($key, $value);
}
$cronBuilder->setContext($config);

$existingCrontab = run(sprintf('crontab -l%s 2>/dev/null || true', $cronUser !== '' ? (' -u ' . $cronUser) : ''));
$newCrontab = CronBuilder::merge($existingCrontab, $cronBuilder);
file_put_contents(get('crontab_filename'), CronBuilder::merge(
file_get_contents(get('crontab_backup_filename')),
$cronBuilder,
));

file_put_contents('crontab.txt', $newCrontab);
upload('crontab.txt', '{{release_path}}/crontab.txt');
});

task('cron:apply', static function (): void {
$cronUser = getCronUser();

run(sprintf('cat {{release_path}}/crontab.txt | crontab%s -', $cronUser !== '' ? (' -u ' . $cronUser) : ''));
upload(get('crontab_filename'), '{{release_path}}/{{crontab_filename}}');
run(sprintf('cat {{release_path}}/{{crontab_filename}} | crontab%s -', $cronUser !== '' ? (' -u ' . $cronUser) : ''));
});

task('cron:cleanup', static function (): void {
// delete local file
if (file_exists('crontab.txt')) {
@unlink('crontab.txt');
if (file_exists(get('crontab_filename'))) {
@unlink(get('crontab_filename'));
}

if (file_exists(get('crontab_backup_filename'))) {
@unlink(get('crontab_backup_filename'));
}
});

Expand Down
47 changes: 47 additions & 0 deletions tests/Integration/DeployerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);

namespace Setono\Deployer\Cron\Integration;

use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Net\SSH2;
use PHPUnit\Framework\TestCase;

final class DeployerTest extends TestCase
{
protected function assertPreConditions(): void
{
self::assertFileExists(__DIR__ . '/deploy.php');
}

/**
* @test
*/
public function it_deploys(): void
{
exec(sprintf('cd %s && php %s deploy -n', __DIR__, '../../vendor/bin/dep'), $output, $return);

$this->assertSame(0, $return);

$key = PublicKeyLoader::load(file_get_contents(__DIR__ . '/../docker/ssh_key'));

$ssh = new SSH2('127.0.0.1', 2222);
if (!$ssh->login('root', $key)) {
throw new \RuntimeException('Could not connection to the server');
}

$release = basename(trim($ssh->exec('realpath ~/deployer/current')));
self::assertIsNumeric($release);

$release = (int) $release;

$output = $ssh->exec('crontab -l -u root');

self::assertStringContainsString(<<<CRON
###> prod ###
0 0 * * * /usr/bin/php ~/deployer/releases/$release/send-report.php # Run every day at midnight
###< prod ###
CRON
, $output);
}
}
Loading

0 comments on commit 360a071

Please sign in to comment.