3 Commits
Author SHA1 Message Date
MeghdadFadaee e7d718cd0e fix: build Gitea release archive without rsync
Build plugin release / release (push) Successful in 4s
2026-08-24 10:09:08 +03:30
MeghdadFadaee 79564cfaa2 refactor: migrate plugin updates from GitHub to Mahgit
Build plugin release / release (push) Failing after 5s
2026-08-24 10:06:49 +03:30
MeghdadFadaee 322f484144 fix: add safe clock diagnostics to debug reports 2026-08-24 09:53:16 +03:30
10 changed files with 354 additions and 80 deletions
+4
View File
@@ -0,0 +1,4 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/.gitea export-ignore
/test export-ignore
+50
View File
@@ -0,0 +1,50 @@
name: Build plugin release
on:
push:
tags:
- 'v*'
permissions:
code: read
releases: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate version and build ZIP
env:
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
VERSION="${RELEASE_TAG#v}"
grep -q "Version: ${VERSION}$" dabestaniha-authenticate-bridge.php
mkdir -p dist
git archive \
--format=zip \
--prefix=mahak-authenticate-bridge/ \
--output=dist/mahak-authenticate-bridge.zip \
HEAD
- name: Publish Gitea release
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
RELEASE_JSON="$(curl --fail-with-body --silent --show-error \
--request POST \
--header "Authorization: token ${GITEA_TOKEN}" \
--header 'Content-Type: application/json' \
--data "{\"tag_name\":\"${RELEASE_TAG}\",\"name\":\"${RELEASE_TAG}\",\"draft\":false,\"prerelease\":false}" \
"${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/releases")"
RELEASE_ID="$(printf '%s' "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -n 1 | cut -d: -f2)"
test -n "$RELEASE_ID"
curl --fail-with-body --silent --show-error \
--request POST \
--header "Authorization: token ${GITEA_TOKEN}" \
--form 'attachment=@dist/mahak-authenticate-bridge.zip' \
"${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=mahak-authenticate-bridge.zip"
-38
View File
@@ -1,38 +0,0 @@
name: Build plugin release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate version and build ZIP
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
VERSION="${RELEASE_TAG#v}"
grep -q "Version: ${VERSION}$" dabestaniha-authenticate-bridge.php
mkdir -p build/mahak-authenticate-bridge dist
rsync -a \
--exclude='.git/' \
--exclude='.github/' \
--exclude='build/' \
--exclude='dist/' \
--exclude='test/' \
./ build/mahak-authenticate-bridge/
cd build
zip -qr ../dist/mahak-authenticate-bridge.zip mahak-authenticate-bridge
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: gh release create "$RELEASE_TAG" dist/mahak-authenticate-bridge.zip --generate-notes --title "$RELEASE_TAG"
+2 -2
View File
@@ -10,13 +10,13 @@ Authenticate WordPress user via json web token from Laravel Application.
- Custom invalid token page
- Optional debug mode with full exception details and stack traces
Debug mode is disabled by default. An administrator can enable it from **Settings > Mahak Authentication** while troubleshooting. Because diagnostic pages expose server paths and other technical details to visitors of the login URL, disable it again after diagnosing the problem.
Debug mode is disabled by default. An administrator can enable it from **Settings > Mahak Authentication** while troubleshooting. The debug page includes server clock/timezone data, safe JWT timing claims, and a Copy Markdown report. JWT contents, secrets, request parameters, and stack-trace arguments are omitted. Because diagnostic pages still expose server paths and other technical details to visitors of the login URL, disable it again after diagnosing the problem.
## Updates
Administrators can check for and install published releases from **Settings > Mahak Authentication**. Updates also appear in WordPress's standard **Plugins** and **Updates** screens.
To publish an update, change the plugin `Version` header and `MAHAK_AUTHENTICATE_BRIDGE_VERSION` constant to the same version, commit the change, and push a matching tag such as `v2.3.0` to GitHub. The release workflow builds and attaches the ZIP required by the WordPress updater.
To publish an update, change the plugin `Version` header and `MAHAK_AUTHENTICATE_BRIDGE_VERSION` constant to the same version, commit the change, and push a matching tag such as `v2.3.0` to Mahgit. The Gitea Actions workflow builds and attaches the ZIP required by the WordPress updater.
## Usage
+4 -4
View File
@@ -2,11 +2,11 @@
/**
* Plugin Name: Mahak Authenticate Bridge
* Plugin URI: https://github.com/dabestaniha/mahak-authenticate-bridge
* Plugin URI: https://mahgit.ir/dabestaniha/mahak-authenticate-bridge
* Description: Authenticate WordPress user via json web token from Mahakiha Application.
* Version: 2.2.0
* Version: 2.2.3
* Requires PHP: 7.4
* Update URI: https://github.com/dabestaniha/mahak-authenticate-bridge
* Update URI: https://mahgit.ir/dabestaniha/mahak-authenticate-bridge
* Author: Dabestaniha
*
* Example: https://wordpress.test/mahak/login/?token=eyJ0...
@@ -18,7 +18,7 @@ use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageMenuControll
use Dabestaniha\AuthenticateBridge\App\Http\Controllers\SettingsPageFormController;
use Dabestaniha\AuthenticateBridge\App\Support\PluginUpdater;
define('MAHAK_AUTHENTICATE_BRIDGE_VERSION', '2.2.0');
define('MAHAK_AUTHENTICATE_BRIDGE_VERSION', '2.2.3');
define('MAHAK_AUTHENTICATE_BRIDGE_FILE', __FILE__);
require_once __DIR__.'/src/Autoloader.php';
@@ -10,10 +10,16 @@ use UnexpectedValueException;
class AuthenticateController extends InvocableController
{
private array $debugContext = [];
public function __invoke(MahakAuthenticateRequest $request)
{
$token = $request->string('token');
if (mahak_debug_enabled()) {
$this->debugContext['jwt_timestamps'] = $this->inspectJwtTimestamps($token);
}
$payload = $this->decrypt_jwt_token($token);
$identifier_name = get_configured_option('user-identifier');
@@ -85,7 +91,36 @@ class AuthenticateController extends InvocableController
$status = $exception->getCode() === 403 ? 403 : 500;
$fallbackView = $exception instanceof UnexpectedValueException ? 'invalid-token' : 'server-error';
mahak_render_error($exception, $status, $fallbackView);
mahak_render_error($exception, $status, $fallbackView, $this->debugContext);
}
private function inspectJwtTimestamps(string $token): array
{
$segments = explode('.', $token);
if (count($segments) !== 3) {
return [];
}
$encodedPayload = strtr($segments[1], '-_', '+/');
$encodedPayload .= str_repeat('=', (4 - strlen($encodedPayload) % 4) % 4);
$json = base64_decode($encodedPayload, true);
$payload = $json === false ? null : json_decode($json, true);
if (!is_array($payload)) {
return [];
}
$timestamps = [];
foreach (['iat', 'nbf', 'exp'] as $claim) {
if (array_key_exists($claim, $payload)) {
$value = is_scalar($payload[$claim]) ? (string) $payload[$claim] : '[non-scalar]';
$timestamps[$claim] = strlen($value) > 64 ? substr($value, 0, 64).'…' : $value;
}
}
return $timestamps;
}
+9 -9
View File
@@ -4,9 +4,9 @@ namespace Dabestaniha\AuthenticateBridge\App\Support;
final class PluginUpdater
{
private const API_URL = 'https://api.github.com/repos/dabestaniha/mahak-authenticate-bridge/releases/latest';
private const API_URL = 'https://mahgit.ir/api/v1/repos/dabestaniha/mahak-authenticate-bridge/releases/latest';
private const RELEASE_ASSET = 'mahak-authenticate-bridge.zip';
private const RELEASE_CACHE = 'mahak_authenticate_bridge_release';
private const RELEASE_CACHE = 'mahak_authenticate_bridge_gitea_release';
private const CHECK_ACTION = 'mahak_authenticate_check_updates';
private static ?self $instance = null;
@@ -74,7 +74,7 @@ final class PluginUpdater
'slug' => $this->slug,
'version' => $this->releaseVersion($release),
'author' => 'Dabestaniha',
'homepage' => $release['html_url'] ?? 'https://github.com/dabestaniha/mahak-authenticate-bridge',
'homepage' => $release['html_url'] ?? 'https://mahgit.ir/dabestaniha/mahak-authenticate-bridge',
'download_link' => $package,
'requires_php' => '7.4',
'sections' => [
@@ -177,30 +177,30 @@ final class PluginUpdater
$response = wp_remote_get(self::API_URL, [
'headers' => [
'Accept' => 'application/vnd.github+json',
'Accept' => 'application/json',
'User-Agent' => 'Mahak-Authenticate-Bridge/'.MAHAK_AUTHENTICATE_BRIDGE_VERSION,
],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return new \WP_Error('mahak_update_request_failed', 'Could not contact GitHub to check for plugin updates.');
return new \WP_Error('mahak_update_request_failed', 'Could not contact Mahgit to check for plugin updates.');
}
if (wp_remote_retrieve_response_code($response) !== 200) {
return new \WP_Error('mahak_update_response_invalid', 'No published plugin release is currently available on GitHub.');
return new \WP_Error('mahak_update_response_invalid', 'No published plugin release is currently available on Mahgit.');
}
$release = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($release) || !empty($release['draft']) || !empty($release['prerelease'])) {
return new \WP_Error('mahak_update_release_invalid', 'GitHub returned invalid plugin release information.');
return new \WP_Error('mahak_update_release_invalid', 'Mahgit returned invalid plugin release information.');
}
if ($this->releaseVersion($release) === '' || $this->releasePackage($release) === null) {
return new \WP_Error(
'mahak_update_asset_missing',
'The latest GitHub release does not contain a valid mahak-authenticate-bridge.zip package.'
'The latest Mahgit release does not contain a valid mahak-authenticate-bridge.zip package.'
);
}
@@ -228,7 +228,7 @@ final class PluginUpdater
if (
$url !== ''
&& wp_parse_url($url, PHP_URL_SCHEME) === 'https'
&& wp_parse_url($url, PHP_URL_HOST) === 'github.com'
&& wp_parse_url($url, PHP_URL_HOST) === 'mahgit.ir'
&& strpos($path, '/dabestaniha/mahak-authenticate-bridge/releases/download/') === 0
) {
return $url;
+136 -2
View File
@@ -20,11 +20,16 @@ if (!function_exists('mahak_debug_enabled')) {
}
if (!function_exists('mahak_render_error')) {
function mahak_render_error(\Throwable $exception, int $status = 500, string $fallbackView = 'server-error'): void
function mahak_render_error(
\Throwable $exception,
int $status = 500,
string $fallbackView = 'server-error',
array $context = []
): void
{
if (mahak_debug_enabled()) {
wp_die(
view('debug-error', ['exception' => $exception]),
view('debug-error', ['report' => mahak_build_debug_report($exception, $context)]),
'Mahak Authentication Error',
['response' => $status]
);
@@ -34,6 +39,135 @@ if (!function_exists('mahak_render_error')) {
}
}
if (!function_exists('mahak_build_debug_report')) {
function mahak_build_debug_report(\Throwable $exception, array $context = []): array
{
$now = time();
$wordpressTimezone = function_exists('wp_timezone_string')
? wp_timezone_string()
: (string) get_option('timezone_string', '');
if ($wordpressTimezone === '') {
$wordpressTimezone = 'UTC offset '.(string) get_option('gmt_offset', 0);
}
$server = [
'Unix timestamp' => (string) $now,
'UTC time' => gmdate('Y-m-d H:i:s \U\T\C', $now),
'PHP local time' => date('Y-m-d H:i:s P T', $now),
'PHP timezone' => date_default_timezone_get(),
'PHP date.timezone' => (string) (ini_get('date.timezone') ?: '[not set]'),
'WordPress timezone' => $wordpressTimezone,
'WordPress version' => get_bloginfo('version'),
'PHP version' => PHP_VERSION,
'Plugin version' => defined('MAHAK_AUTHENTICATE_BRIDGE_VERSION')
? MAHAK_AUTHENTICATE_BRIDGE_VERSION
: '[unknown]',
];
$jwtTimestamps = [];
foreach (($context['jwt_timestamps'] ?? []) as $claim => $value) {
$jwtTimestamps[strtoupper((string) $claim)] = mahak_format_jwt_timestamp((string) $value, $now);
}
$trace = mahak_sanitized_exception_trace($exception);
$exceptionData = [
'Type' => get_class($exception),
'Message' => $exception->getMessage(),
'Location' => $exception->getFile().':'.$exception->getLine(),
];
$markdown = "# Mahak Authentication Error\n\n## Exception\n\n";
foreach ($exceptionData as $label => $value) {
$markdown .= '- '.$label.': `'.mahak_markdown_value($value)."`\n";
}
$markdown .= "\n## WordPress server diagnostics\n\n| Field | Value |\n|---|---|\n";
foreach ($server as $label => $value) {
$markdown .= '| '.mahak_markdown_value($label).' | `'.mahak_markdown_value($value)."` |\n";
}
$markdown .= "\n## JWT timing claims\n\n";
if ($jwtTimestamps === []) {
$markdown .= "No readable JWT timing claims were available.\n";
} else {
$markdown .= "| Claim | Value |\n|---|---|\n";
foreach ($jwtTimestamps as $label => $value) {
$markdown .= '| '.mahak_markdown_value($label).' | `'.mahak_markdown_value($value)."` |\n";
}
}
$markdown .= "\n## Sanitized stack trace\n\n```text\n".str_replace('```', "'''", $trace)."\n```\n";
$markdown .= "\n> JWT contents, request parameters, and function arguments are intentionally omitted.\n";
return [
'exception' => $exceptionData,
'server' => $server,
'jwt_timestamps' => $jwtTimestamps,
'trace' => $trace,
'markdown' => $markdown,
];
}
}
if (!function_exists('mahak_format_jwt_timestamp')) {
function mahak_format_jwt_timestamp(string $value, int $serverNow): string
{
if (!is_numeric($value)) {
return $value.' (not numeric)';
}
$numericValue = (float) $value;
$note = '';
if (is_infinite($numericValue) || is_nan($numericValue)) {
return $value.' (invalid numeric value)';
}
if (abs($numericValue) >= 100000000000) {
$numericValue /= 1000;
$note = '; appears to use milliseconds';
}
$timestamp = (int) $numericValue;
$difference = $timestamp - $serverNow;
$differenceLabel = ($difference >= 0 ? '+' : '').$difference.' seconds vs server';
return $value.' ('.gmdate('Y-m-d H:i:s \U\T\C', $timestamp).'; '.$differenceLabel.$note.')';
}
}
if (!function_exists('mahak_sanitized_exception_trace')) {
function mahak_sanitized_exception_trace(\Throwable $exception): string
{
$lines = [];
foreach ($exception->getTrace() as $index => $frame) {
$location = isset($frame['file'])
? $frame['file'].':'.($frame['line'] ?? '?')
: '[internal function]';
$call = ($frame['class'] ?? '').($frame['type'] ?? '').($frame['function'] ?? '[unknown]').'()';
$lines[] = '#'.$index.' '.$location.' '.$call;
}
$lines[] = '#'.count($lines).' {main}';
return implode("\n", $lines);
}
}
if (!function_exists('mahak_markdown_value')) {
function mahak_markdown_value(string $value): string
{
return str_replace(["\r", "\n", '|', '`'], [' ', ' ', '\\|', "'"], $value);
}
}
if (!function_exists('dd')) {
function dd(): void
{
+72 -13
View File
@@ -8,24 +8,83 @@
body { margin: 2rem; color: #1d2327; background: #f0f0f1; font: 14px/1.5 monospace; }
main { max-width: 1100px; margin: auto; padding: 2rem; background: #fff; border-left: 4px solid #d63638; box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
h1 { margin-top: 0; color: #d63638; font: 24px/1.3 sans-serif; }
dt { margin-top: 1rem; font-weight: 700; }
dd { margin: .25rem 0 0; overflow-wrap: anywhere; }
pre { overflow: auto; padding: 1rem; color: #f0f0f1; background: #1d2327; white-space: pre-wrap; }
h2 { margin-top: 2rem; font: 20px/1.3 sans-serif; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: .55rem; border: 1px solid #c3c4c7; text-align: left; vertical-align: top; overflow-wrap: anywhere; }
th { width: 220px; background: #f6f7f7; }
pre, textarea { box-sizing: border-box; width: 100%; padding: 1rem; color: #f0f0f1; background: #1d2327; white-space: pre-wrap; }
textarea { min-height: 240px; resize: vertical; }
button { padding: .55rem 1rem; border: 1px solid #2271b1; border-radius: 3px; color: #fff; background: #2271b1; cursor: pointer; }
#copy-status { margin-left: .75rem; font-family: sans-serif; }
.privacy-note { padding: .75rem; border-left: 4px solid #72aee6; background: #f0f6fc; font-family: sans-serif; }
</style>
</head>
<body>
<main>
<h1>Mahak Authentication Error</h1>
<dl>
<dt>Exception</dt>
<dd><?= esc_html(get_class($exception)) ?></dd>
<dt>Message</dt>
<dd><?= esc_html($exception->getMessage()) ?></dd>
<dt>Location</dt>
<dd><?= esc_html($exception->getFile().':'.$exception->getLine()) ?></dd>
</dl>
<h2>Stack trace</h2>
<pre><?= esc_html($exception->getTraceAsString()) ?></pre>
<h2>Exception</h2>
<table>
<?php foreach ($report['exception'] as $label => $value): ?>
<tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<?php endforeach; ?>
</table>
<h2>WordPress server diagnostics</h2>
<table>
<?php foreach ($report['server'] as $label => $value): ?>
<tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<?php endforeach; ?>
</table>
<h2>JWT timing claims</h2>
<?php if ($report['jwt_timestamps'] === []): ?>
<p>No readable JWT timing claims were available.</p>
<?php else: ?>
<table>
<?php foreach ($report['jwt_timestamps'] as $label => $value): ?>
<tr><th><?= esc_html($label) ?></th><td><?= esc_html($value) ?></td></tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>Sanitized stack trace</h2>
<pre><?= esc_html($report['trace']) ?></pre>
<p class="privacy-note">JWT contents, request parameters, and function arguments are intentionally omitted.</p>
<h2>Copyable report</h2>
<p><button type="button" id="copy-markdown">Copy Markdown</button><span id="copy-status" role="status"></span></p>
<textarea id="debug-markdown" readonly><?= esc_textarea($report['markdown']) ?></textarea>
</main>
<script>
(function () {
var button = document.getElementById('copy-markdown');
var report = document.getElementById('debug-markdown');
var status = document.getElementById('copy-status');
button.addEventListener('click', function () {
var copied = function () { status.textContent = 'Copied.'; };
var failed = function () {
report.focus();
report.select();
status.textContent = 'Select the report and copy it manually.';
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(report.value).then(copied, failed);
return;
}
report.focus();
report.select();
try {
document.execCommand('copy') ? copied() : failed();
} catch (error) {
failed();
}
});
}());
</script>
</body>
</html>
+41 -11
View File
@@ -258,11 +258,20 @@ function test_debug_error_details(): void
{
update_option('mahak_debug_mode', 1);
$response = http_request(TEST_SITE_URL.'/mahak/login/?token=not-a-jwt');
$futureIat = time() + 300;
$token = make_jwt([
'iat' => $futureIat,
'exp' => $futureIat + 600,
'data' => [
'name' => 'Future User',
'email' => 'future-user@example.test',
],
], TEST_SECRET);
$response = http_request(TEST_SITE_URL.'/mahak/login/?token='.rawurlencode($token));
assert_true($response['status'] === 500, 'Debug error response should return HTTP 500.');
assert_true(
strpos($response['body'], 'JWT must contain header, payload, and signature.') !== false,
strpos($response['body'], 'JWT cannot be used before iat.') !== false,
'Debug response did not contain the underlying exception message.'
);
assert_true(
@@ -270,19 +279,40 @@ function test_debug_error_details(): void
'Debug response did not contain the exception class.'
);
assert_true(
strpos($response['body'], 'Stack trace') !== false,
'Debug response did not contain a stack trace.'
strpos($response['body'], 'WordPress server diagnostics') !== false
&& strpos($response['body'], 'Unix timestamp') !== false
&& strpos($response['body'], 'WordPress timezone') !== false,
'Debug response did not contain WordPress server clock and timezone details.'
);
assert_true(
strpos($response['body'], 'JWT timing claims') !== false
&& strpos($response['body'], (string) $futureIat) !== false
&& strpos($response['body'], 'seconds vs server') !== false,
'Debug response did not contain safe JWT timing diagnostics.'
);
assert_true(
strpos($response['body'], 'Copy Markdown') !== false
&& strpos($response['body'], '# Mahak Authentication Error') !== false,
'Debug response did not contain a copyable Markdown report.'
);
assert_true(
strpos($response['body'], TEST_SECRET) === false && strpos($response['body'], $token) === false,
'Debug response exposed the JWT token or signing secret.'
);
assert_true(
strpos($response['body'], 'Sanitized stack trace') !== false,
'Debug response did not contain the sanitized stack trace.'
);
update_option('mahak_debug_mode', 0);
pass('Debug mode displays full exception details and stack trace');
pass('Debug report includes clock diagnostics and Markdown without token or secret leakage');
}
function test_plugin_update_discovery(): void
{
$packageUrl = 'https://github.com/dabestaniha/mahak-authenticate-bridge/releases/download/v2.3.0/mahak-authenticate-bridge.zip';
$packageUrl = 'https://mahgit.ir/dabestaniha/mahak-authenticate-bridge/releases/download/v2.3.0/mahak-authenticate-bridge.zip';
$mockRelease = function ($response, array $request, string $url) use ($packageUrl) {
if ($url !== 'https://api.github.com/repos/dabestaniha/mahak-authenticate-bridge/releases/latest') {
if ($url !== 'https://mahgit.ir/api/v1/repos/dabestaniha/mahak-authenticate-bridge/releases/latest') {
return $response;
}
@@ -290,7 +320,7 @@ function test_plugin_update_discovery(): void
'headers' => [],
'body' => json_encode([
'tag_name' => 'v2.3.0',
'html_url' => 'https://github.com/dabestaniha/mahak-authenticate-bridge/releases/tag/v2.3.0',
'html_url' => 'https://mahgit.ir/dabestaniha/mahak-authenticate-bridge/releases/tag/v2.3.0',
'body' => 'Test release',
'draft' => false,
'prerelease' => false,
@@ -305,7 +335,7 @@ function test_plugin_update_discovery(): void
];
};
delete_site_transient('mahak_authenticate_bridge_release');
delete_site_transient('mahak_authenticate_bridge_gitea_release');
add_filter('pre_http_request', $mockRelease, 10, 3);
$updates = apply_filters('pre_set_site_transient_update_plugins', (object) [
@@ -315,11 +345,11 @@ function test_plugin_update_discovery(): void
remove_filter('pre_http_request', $mockRelease, 10);
assert_true(isset($updates->response[TEST_PLUGIN]), 'A newer GitHub release was not offered as a WordPress update.');
assert_true(isset($updates->response[TEST_PLUGIN]), 'A newer Mahgit release was not offered as a WordPress update.');
assert_true($updates->response[TEST_PLUGIN]->new_version === '2.3.0', 'The offered plugin version was incorrect.');
assert_true($updates->response[TEST_PLUGIN]->package === $packageUrl, 'The release package URL was incorrect.');
pass('Published GitHub releases are discovered by the WordPress updater');
pass('Published Mahgit releases are discovered by the WordPress updater');
}
boot_wordpress();