Skip to content
Draft
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
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,76 @@ The following commands need to be set up to run on a regular basis:

Additionally, `queue/listen` should run as a daemon or `queue/run` as a cronjob.

### GitHub Webhook for Automatic Version Updates

This feature allows the website to automatically update framework version information when new releases are published on GitHub.

#### Setup

##### 1. Configure Webhook Secret

Add the webhook secret to your configuration:

```php
// config/params-local.php
return [
// ... other params
'github-webhook-secret' => 'your-secret-key-here',
];
```

Or set via environment variable and reference it in params.

##### 2. Configure GitHub Webhooks

For each repository you want to track (e.g., `yiisoft/yii2`, `yiisoft/yii`):

1. Go to repository Settings → Webhooks
2. Click "Add webhook"
3. Configure:
- **Payload URL**: `https://www.yiiframework.com/site/github-webhook`
- **Content type**: `application/json`
- **Secret**: Use the same secret as configured above
- **Events**: Select "Releases"
- **Active**: ✓ Checked

##### 3. Supported Repositories

Currently supported repositories:
- `yiisoft/yii2` → Updates `2.0` version series
- `yiisoft/yii` → Updates `1.1` version series

##### 4. How It Works

When a new release is published:

1. GitHub sends a webhook to `/site/github-webhook`
2. The webhook validates the signature for security
3. If the release is from a supported repository and matches the expected version format:
- The new version is added to the top of the appropriate array in `config/versions.php`
- The date is formatted as "Month Day, Year"

##### 5. Security

- Webhook signatures are validated using HMAC-SHA256
- Only "published" release events are processed
- Only releases from whitelisted repositories are accepted
- Only versions matching expected patterns are processed

##### 6. Testing

To test the webhook:

```bash
# Send a test payload (replace with actual secret)
curl -X POST https://www.yiiframework.com/site/github-webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-d '{"zen": "Design for failure."}'
```

Expected response: "pong"

### Deployment

This section covers notes for deployment on a server, you may not need this for your dev env. OS is assumed to be Debian
Expand Down
245 changes: 245 additions & 0 deletions components/GitHubWebhookAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<?php

namespace app\components;

use yii\base\Action;
use yii\base\Exception;
use yii\web\BadRequestHttpException;
use yii\web\ForbiddenHttpException;
use yii\web\MethodNotAllowedHttpException;
use Yii;

/**
* Action handles GitHub release webhooks to automatically update version information.
*
* This action can be used in any controller as:
*
* public function actions()
* {
* return [
* 'github-webhook' => [
* 'class' => 'app\components\GitHubWebhookAction',
* 'versionsFile' => '@app/config/versions.php',
* ]
* ];
* }
*
* GitHub webhook should be configured with:
* - Payload URL: https://www.yiiframework.com/site/github-webhook
* - Content type: application/json
* - Secret: configure in config/params.php as "github-webhook-secret"
* - Events: Release
*/
class GitHubWebhookAction extends Action
{
/**
* @var string Path to the versions.php file
*/
public $versionsFile = '@app/config/versions.php';

/**
* Handles the GitHub webhook request
*
* @return string
* @throws BadRequestHttpException
* @throws ForbiddenHttpException
* @throws MethodNotAllowedHttpException
*/
public function run()
{
if (!Yii::$app->request->isPost) {
Yii::$app->response->getHeaders()->set('Allow', 'POST');
throw new MethodNotAllowedHttpException('Only POST requests are allowed');
}

$event = Yii::$app->request->headers->get('x-github-event');

// Handle ping event
if ($event === 'ping') {
return 'pong';
}

// Only process release events
if ($event !== 'release') {
return 'Event not handled';
}

$this->validateSignature();
$this->processReleaseEvent();

return 'Version updated successfully';
}

/**
* Validates the GitHub webhook signature
*
* @param string|null $payload Optional payload for testing
* @throws BadRequestHttpException
* @throws ForbiddenHttpException
*/
protected function validateSignature($payload = null)
{
$secret = Yii::$app->params['github-webhook-secret'] ?? null;

if (empty($secret)) {
// If no secret is configured, skip validation (not recommended for production)
return;
}

$signature = Yii::$app->request->headers->get('x-hub-signature-256');

if (empty($signature)) {
throw new ForbiddenHttpException('Missing signature header');
}

if ($payload === null) {
$payload = file_get_contents('php://input');
}

$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expectedSignature, $signature)) {
throw new ForbiddenHttpException('Invalid signature');
}
}

/**
* Processes the release event and updates version information
*
* @throws BadRequestHttpException
* @throws Exception
*/
protected function processReleaseEvent()
{
// Parse JSON payload
Yii::$app->request->parsers = [
'application/json' => 'yii\web\JsonParser',
];

$payload = Yii::$app->request->post();

if (!isset($payload['action']) || !isset($payload['release'])) {
throw new BadRequestHttpException('Invalid payload format');
}

// Only process published releases
if ($payload['action'] !== 'published') {
return;
}

$release = $payload['release'];
$repository = $payload['repository'] ?? [];

if (!isset($release['tag_name']) || !isset($repository['full_name'])) {
throw new BadRequestHttpException('Missing required release information');
}

$this->updateVersionsFile($release, $repository);
}

/**
* Updates the versions.php file with new release information
*
* @param array $release Release information from GitHub
* @param array $repository Repository information from GitHub
* @throws Exception
*/
protected function updateVersionsFile($release, $repository)
{
$versionsPath = Yii::getAlias($this->versionsFile);

if (!is_file($versionsPath)) {
throw new Exception('Versions file not found: ' . $versionsPath);
}

if (!is_writable($versionsPath)) {
throw new Exception('Versions file is not writable: ' . $versionsPath);
}

$tagName = $release['tag_name'];
$publishedAt = $release['published_at'];
$repoName = $repository['full_name'];

// Determine which version series this belongs to
$versionSeries = $this->getVersionSeries($tagName, $repoName);

if ($versionSeries === null) {
Yii::info("Skipping release $tagName from $repoName - not a supported version", __METHOD__);
return;
}

// Format the date
$date = date('M j, Y', strtotime($publishedAt));

// Read current versions file
$content = file_get_contents($versionsPath);

// Check if version already exists
if (strpos($content, "'$tagName'") !== false) {
Yii::info("Version $tagName already exists in versions file", __METHOD__);
return;
}

// Add the new version to the appropriate series
$pattern = '/(\'' . preg_quote($versionSeries, '/') . '\'\s*=>\s*\[)(.*?)(\],)/s';

if (preg_match($pattern, $content, $matches)) {
$beforeArray = $matches[1];
$arrayContent = $matches[2];
$afterArray = $matches[3];

// Add new version at the beginning of the array
$newVersionLine = "\n '$tagName' => '$date',";
$newArrayContent = $newVersionLine . $arrayContent;

$newContent = str_replace(
$beforeArray . $arrayContent . $afterArray,
$beforeArray . $newArrayContent . $afterArray,
$content
);

if (file_put_contents($versionsPath, $newContent) !== false) {
Yii::info("Successfully updated $versionSeries with version $tagName", __METHOD__);
} else {
throw new Exception("Failed to write updated versions file");
}
} else {
throw new Exception("Could not find version series '$versionSeries' in versions file");
}
}

/**
* Determines the version series based on tag name and repository
*
* @param string $tagName
* @param string $repoName
* @return string|null
*/
protected function getVersionSeries($tagName, $repoName)
{
// Map repositories to version series
$repoMapping = [
'yiisoft/yii2' => '2.0',
'yiisoft/yii' => '1.1', // Yii 1.1 releases
];

// Get base version series from repository
$baseSeries = $repoMapping[$repoName] ?? null;

if ($baseSeries === null) {
return null;
}

// For Yii 2.0, ensure the tag matches 2.x.x pattern
if ($baseSeries === '2.0' && !preg_match('/^2\.\d+\.\d+/', $tagName)) {
return null;
}

// For Yii 1.1, ensure the tag matches 1.1.x pattern
if ($baseSeries === '1.1' && !preg_match('/^1\.1\.\d+/', $tagName)) {
return null;
}

return $baseSeries;
}
}
4 changes: 4 additions & 0 deletions config/params.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,8 @@
'discourse.sso_secret' => '',
'discourse.sso_url' => 'https://forum.yiiframework.com',
'slack.invite.link' => 'https://join.slack.com/t/yii/shared_invite/enQtMzQ4MDExMDcyNTk2LTc0NDQ2ZTZhNjkzZDgwYjE4YjZlNGQxZjFmZDBjZTU3NjViMDE4ZTMxNDRkZjVlNmM1ZTA1ODVmZGUwY2U3NDA',

// GitHub webhook secret for automatic version updates
// Configure this in params-local.php or set via environment variable
'github-webhook-secret' => null,
];
4 changes: 4 additions & 0 deletions controllers/SiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public function actions()
'class' => \yii\captcha\CaptchaAction::class,
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
'github-webhook' => [
'class' => \app\components\GitHubWebhookAction::class,
'versionsFile' => '@app/config/versions.php',
],
];
}

Expand Down
Loading
Loading