Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365a3898b9 | ||
|
|
2bbd2dc7c8 | ||
|
|
93af230406 | ||
|
|
c98e46437e | ||
|
|
3bae89a203 | ||
|
|
123c0dbb7b | ||
|
|
57e1f63bcf | ||
|
|
bea4875b05 | ||
|
|
343f647770 | ||
|
|
7184d998e6 | ||
|
|
189bde2008 | ||
|
|
a090527037 | ||
|
|
1595327a80 | ||
|
|
b4b6158f2d | ||
|
|
0e56de3c56 | ||
|
|
f9b8611519 | ||
|
|
84e5119c41 | ||
|
|
67a28e0037 | ||
|
|
fb6c9d4d61 |
@@ -0,0 +1,157 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build & publish plugin zip
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build plugin zip
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
OUT="dist/projects-portfolio-v${VERSION}.zip"
|
||||
|
||||
rm -rf dist
|
||||
mkdir -p dist
|
||||
|
||||
# Use Python's stdlib zipfile module — guaranteed to be on ubuntu-latest.
|
||||
python3 - "$OUT" <<'PYEOF'
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
out_path = sys.argv[1]
|
||||
allowed = [
|
||||
"projects-portfolio.php",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"admin",
|
||||
"assets",
|
||||
"includes",
|
||||
"languages",
|
||||
"templates",
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(out_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for entry in allowed:
|
||||
if os.path.isdir(entry):
|
||||
for root, _, files in os.walk(entry):
|
||||
for name in files:
|
||||
abs_path = os.path.join(root, name)
|
||||
arcname = os.path.join("projects-portfolio", abs_path)
|
||||
zf.write(abs_path, arcname)
|
||||
elif os.path.isfile(entry):
|
||||
zf.write(entry, os.path.join("projects-portfolio", entry))
|
||||
else:
|
||||
sys.exit(f"Allowlisted path missing: {entry}")
|
||||
PYEOF
|
||||
|
||||
echo "Built $OUT"
|
||||
python3 -c "import zipfile; zf=zipfile.ZipFile('$OUT'); print('\n'.join(zf.namelist()))"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: projects-portfolio-${{ github.ref_name }}
|
||||
path: dist/projects-portfolio-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
GH_API_URL: ${{ github.api_url }}
|
||||
GH_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zip_path=( dist/projects-portfolio-*.zip )
|
||||
zip_path="${zip_path[0]}"
|
||||
asset_name=$(basename "$zip_path")
|
||||
|
||||
if [[ "$GH_API_URL" == "https://api.github.com" ]]; then
|
||||
# ---- GitHub.com path ----
|
||||
if [ -z "${GITHUB_TOKEN:-}" ]; then
|
||||
echo "::error::GITHUB_TOKEN is not available on this runner. Check repo permissions." >&2
|
||||
exit 1
|
||||
fi
|
||||
payload=$(python3 scripts/release-helper.py build-payload --github)
|
||||
# Look up existing release by tag (idempotent on re-run).
|
||||
release_json=$(curl -fsS \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"${GH_API_URL}/repos/${REPO}/releases/tags/${TAG}" \
|
||||
|| true)
|
||||
if [ -n "$release_json" ] && echo "$release_json" | python3 -c 'import json,sys; sys.exit(0 if json.loads(sys.stdin.read()).get("id") else 1)' 2>/dev/null; then
|
||||
release_id=$(echo "$release_json" | python3 scripts/release-helper.py extract-id)
|
||||
upload_url=$(echo "$release_json" | python3 scripts/release-helper.py extract-upload-url)
|
||||
echo "Reusing existing GitHub release id=$release_id for tag $TAG"
|
||||
else
|
||||
release_json=$(curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${GH_API_URL}/repos/${REPO}/releases")
|
||||
release_id=$(echo "$release_json" | python3 scripts/release-helper.py extract-id)
|
||||
upload_url=$(echo "$release_json" | python3 scripts/release-helper.py extract-upload-url)
|
||||
echo "Created GitHub release id=$release_id"
|
||||
fi
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Content-Type: application/zip" \
|
||||
--data-binary "@${zip_path}" \
|
||||
"${upload_url}?name=${asset_name}"
|
||||
echo "Uploaded GitHub asset: ${asset_name}"
|
||||
else
|
||||
# ---- Gitea path ----
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
echo "::error::GITEA_TOKEN secret is not set. Create a personal access token in Gitea with 'write:repository' scope and add it as a repository secret named GITEA_TOKEN." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${GH_API_URL:-}" ] || [[ "$GH_API_URL" == "https://git.keithsolomon.net"* ]]; then
|
||||
API="https://git.keithsolomon.net/api/v1"
|
||||
else
|
||||
API="${GH_API_URL%/}/api/v1"
|
||||
fi
|
||||
payload=$(python3 scripts/release-helper.py build-payload)
|
||||
# Look up existing release by tag (idempotent on re-run).
|
||||
release_json=$(curl -fsS \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/repos/${REPO}/releases/tags/${TAG}" \
|
||||
|| true)
|
||||
if [ -n "$release_json" ] && echo "$release_json" | python3 -c 'import json,sys; sys.exit(0 if json.loads(sys.stdin.read()).get("id") else 1)' 2>/dev/null; then
|
||||
release_id=$(echo "$release_json" | python3 scripts/release-helper.py extract-id)
|
||||
echo "Reusing existing Gitea release id=$release_id for tag $TAG"
|
||||
else
|
||||
release_json=$(curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${API}/repos/${REPO}/releases")
|
||||
release_id=$(echo "$release_json" | python3 scripts/release-helper.py extract-id)
|
||||
echo "Created Gitea release id=$release_id"
|
||||
fi
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/zip" \
|
||||
--data-binary "@${zip_path}" \
|
||||
"${API}/repos/${REPO}/releases/${release_id}/assets?name=${asset_name}" \
|
||||
|| echo "::warning::Asset upload failed (asset may already exist); continuing."
|
||||
echo "Gitea asset step complete: ${asset_name}"
|
||||
fi
|
||||
@@ -5,3 +5,5 @@ notes/
|
||||
vendor/
|
||||
phpunit.xml
|
||||
.phpunit.result.cache
|
||||
# But DO track the vendor/ that ships with the bundled plugin-update-checker library.
|
||||
!/includes/plugin-update-checker/vendor/
|
||||
|
||||
@@ -121,18 +121,26 @@ To avoid rate limits or improve reliability:
|
||||
1. In your Gitea instance, go to **Settings → Applications** and generate a token.
|
||||
2. Paste it into the plugin settings screen under **Gitea API Token**.
|
||||
|
||||
## Building a Release
|
||||
|
||||
Releases are automated via GitHub Actions. To cut a new release:
|
||||
|
||||
1. Bump `PROJECTS_PORTFOLIO_VERSION` and the plugin header `Version:` in `projects-portfolio.php`.
|
||||
2. Commit and push to `main`.
|
||||
3. Tag the release commit: `git tag -a v1.1.1 -m "v1.1.1 — short summary"`.
|
||||
4. Push the tag: `git push origin v1.1.1`.
|
||||
|
||||
The `.github/workflows/release.yml` workflow runs and:
|
||||
|
||||
- Builds `projects-portfolio-v<version>.zip` containing only the runtime files (`projects-portfolio.php`, `admin/`, `assets/`, `includes/`, `languages/`, `templates/`, `README.md`, `LICENSE`).
|
||||
- Uploads the zip as a workflow artifact.
|
||||
- Creates (or updates) the matching GitHub Release with the zip attached.
|
||||
|
||||
Dev-only paths (`composer.json`, `composer.lock`, `vendor/`, `tests/`, `plans/`, `specs/`, `.github/`, `.gitignore`, `.vscode/`, `.claude/`) are intentionally excluded.
|
||||
|
||||
## How to Zip the Plugin for GitHub Releases
|
||||
|
||||
When attaching the plugin to a GitHub release:
|
||||
|
||||
1. Zip **only the contents of the plugin folder**, not the parent folder.
|
||||
2. Ensure `projects-portfolio.php` and `/includes`, `/admin`, `/templates`, `/assets` are at the root level of the ZIP.
|
||||
3. Name the ZIP clearly (e.g. `projects-portfolio-1.0.0.zip`).
|
||||
4. Go to your GitHub repo → **Releases** → **New Release**.
|
||||
5. Tag the release with the version number (e.g., `1.0.0`).
|
||||
6. Upload your correctly structured ZIP as a release asset.
|
||||
|
||||
Your users will be redirected to this file when they use the `/download/` endpoint.
|
||||
This is now automated. See [Building a Release](#building-a-release) above — pushing a `v*` tag triggers `.github/workflows/release.yml` which builds and publishes the zip.
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace YahnisElsts\PluginUpdateChecker\v5;
|
||||
|
||||
if ( !class_exists(PucFactory::class, false) ):
|
||||
|
||||
class PucFactory extends \YahnisElsts\PluginUpdateChecker\v5p4\PucFactory {
|
||||
class PucFactory extends \YahnisElsts\PluginUpdateChecker\v5p7\PucFactory {
|
||||
}
|
||||
|
||||
endif;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(Autoloader::class, false) ):
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\PucFactory;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker;
|
||||
|
||||
if ( !class_exists(Extension::class, false) ):
|
||||
|
||||
+19
-11
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker;
|
||||
|
||||
if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false) ):
|
||||
|
||||
@@ -41,11 +41,11 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
|
||||
echo '<h3>Configuration</h3>';
|
||||
echo '<table class="puc-debug-data">';
|
||||
$this->displayConfigHeader();
|
||||
$this->row('Slug', htmlentities($this->updateChecker->slug));
|
||||
$this->row('DB option', htmlentities($this->updateChecker->optionName));
|
||||
$this->row('Slug', esc_html($this->updateChecker->slug));
|
||||
$this->row('DB option', esc_html($this->updateChecker->optionName));
|
||||
|
||||
$requestInfoButton = $this->getMetadataButton();
|
||||
$this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
|
||||
$this->row('Metadata URL', esc_html($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox);
|
||||
|
||||
$scheduler = $this->updateChecker->scheduler;
|
||||
if ( $scheduler->checkPeriod > 0 ) {
|
||||
@@ -86,14 +86,22 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
|
||||
echo '<h3>Status</h3>';
|
||||
echo '<table class="puc-debug-data">';
|
||||
$state = $this->updateChecker->getUpdateState();
|
||||
$checkNowButton = '';
|
||||
$checkButtonId = $this->updateChecker->getUniqueName('check-now-button');
|
||||
if ( function_exists('get_submit_button') ) {
|
||||
$checkNowButton = get_submit_button(
|
||||
'Check Now',
|
||||
'secondary',
|
||||
'puc-check-now-button',
|
||||
false,
|
||||
array('id' => $this->updateChecker->getUniqueName('check-now-button'))
|
||||
array('id' => $checkButtonId)
|
||||
);
|
||||
} else {
|
||||
//get_submit_button() is not available in the frontend. Make a button directly.
|
||||
//It won't look the same without admin styles, but it should still work.
|
||||
$checkNowButton = sprintf(
|
||||
'<input type="button" id="%1$s" name="puc-check-now-button" value="%2$s" class="button button-secondary" />',
|
||||
esc_attr($checkButtonId),
|
||||
esc_attr('Check Now')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,10 +115,10 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
|
||||
$this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck));
|
||||
|
||||
if ( $state->getCheckedVersion() !== '' ) {
|
||||
$this->row('Checked version', htmlentities($state->getCheckedVersion()));
|
||||
$this->row('Checked version', esc_html($state->getCheckedVersion()));
|
||||
$this->row('Cached update', $state->getUpdate());
|
||||
}
|
||||
$this->row('Update checker class', htmlentities(get_class($this->updateChecker)));
|
||||
$this->row('Update checker class', esc_html(get_class($this->updateChecker)));
|
||||
echo '</table>';
|
||||
}
|
||||
|
||||
@@ -124,7 +132,7 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
|
||||
if ( property_exists($update, $field) ) {
|
||||
$this->row(
|
||||
ucwords(str_replace('_', ' ', $field)),
|
||||
isset($update->$field) ? htmlentities($update->$field) : null
|
||||
isset($update->$field) ? esc_html($update->$field) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +170,7 @@ if ( !class_exists(Panel::class, false) && class_exists('Debug_Bar_Panel', false
|
||||
if ( is_object($value) || is_array($value) ) {
|
||||
//This is specifically for debugging, so print_r() is fine.
|
||||
//phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
$value = '<pre>' . htmlentities(print_r($value, true)) . '</pre>';
|
||||
$value = '<pre>' . esc_html(print_r($value, true)) . '</pre>';
|
||||
} else if ($value === null) {
|
||||
$value = '<code>null</code>';
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker;
|
||||
|
||||
if ( !class_exists(PluginExtension::class, false) ):
|
||||
|
||||
+11
-5
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker;
|
||||
|
||||
if ( !class_exists(PluginPanel::class, false) ):
|
||||
|
||||
@@ -12,19 +12,25 @@ if ( !class_exists(PluginPanel::class, false) ):
|
||||
protected $updateChecker;
|
||||
|
||||
protected function displayConfigHeader() {
|
||||
$this->row('Plugin file', htmlentities($this->updateChecker->pluginFile));
|
||||
$this->row('Plugin file', esc_html($this->updateChecker->pluginFile));
|
||||
parent::displayConfigHeader();
|
||||
}
|
||||
|
||||
protected function getMetadataButton() {
|
||||
$requestInfoButton = '';
|
||||
$buttonId = $this->updateChecker->getUniqueName('request-info-button');
|
||||
if ( function_exists('get_submit_button') ) {
|
||||
$requestInfoButton = get_submit_button(
|
||||
'Request Info',
|
||||
'secondary',
|
||||
'puc-request-info-button',
|
||||
false,
|
||||
array('id' => $this->updateChecker->getUniqueName('request-info-button'))
|
||||
array('id' => $buttonId)
|
||||
);
|
||||
} else {
|
||||
$requestInfoButton = sprintf(
|
||||
'<input type="button" name="puc-request-info-button" id="%1$s" value="%2$s" class="button button-secondary" />',
|
||||
esc_attr($buttonId),
|
||||
esc_attr('Request Info')
|
||||
);
|
||||
}
|
||||
return $requestInfoButton;
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Theme\UpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Theme\UpdateChecker;
|
||||
|
||||
if ( !class_exists(ThemePanel::class, false) ):
|
||||
|
||||
@@ -13,7 +13,7 @@ if ( !class_exists(ThemePanel::class, false) ):
|
||||
protected $updateChecker;
|
||||
|
||||
protected function displayConfigHeader() {
|
||||
$this->row('Theme directory', htmlentities($this->updateChecker->directoryName));
|
||||
$this->row('Theme directory', esc_html($this->updateChecker->directoryName));
|
||||
parent::displayConfigHeader();
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(InstalledPackage::class, false) ):
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
use LogicException;
|
||||
use stdClass;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(OAuthSignature::class, false) ):
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\PucFactory;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory;
|
||||
|
||||
if ( !class_exists(Package::class, false) ):
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Metadata;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Metadata;
|
||||
|
||||
if ( !class_exists(PluginInfo::class, false) ):
|
||||
|
||||
@@ -39,6 +39,7 @@ if ( !class_exists(PluginInfo::class, false) ):
|
||||
public $downloaded;
|
||||
public $active_installs;
|
||||
public $last_updated;
|
||||
public $autoupdate = false;
|
||||
|
||||
public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
if ( !class_exists(Ui::class, false) ):
|
||||
/**
|
||||
@@ -187,7 +187,7 @@ if ( !class_exists(Ui::class, false) ):
|
||||
}
|
||||
}
|
||||
|
||||
wp_redirect(add_query_arg(
|
||||
wp_safe_redirect(add_query_arg(
|
||||
array(
|
||||
'puc_update_check_result' => $status,
|
||||
'puc_slug' => $this->updateChecker->slug,
|
||||
+6
-4
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Update as BaseUpdate;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Update as BaseUpdate;
|
||||
|
||||
if ( !class_exists(Update::class, false) ):
|
||||
|
||||
@@ -20,9 +20,10 @@ if ( !class_exists(Update::class, false) ):
|
||||
public $requires_php = false;
|
||||
public $icons = array();
|
||||
public $filename; //Plugin filename relative to the plugins directory.
|
||||
public $autoupdate = false;
|
||||
|
||||
protected static $extraFields = array(
|
||||
'id', 'homepage', 'tested', 'requires_php', 'upgrade_notice', 'icons', 'filename',
|
||||
'id', 'homepage', 'tested', 'requires_php', 'upgrade_notice', 'icons', 'filename', 'autoupdate',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -86,6 +87,7 @@ if ( !class_exists(Update::class, false) ):
|
||||
$update->tested = $this->tested;
|
||||
$update->requires_php = $this->requires_php;
|
||||
$update->plugin = $this->filename;
|
||||
$update->autoupdate = $this->autoupdate;
|
||||
|
||||
if ( !empty($this->upgrade_notice) ) {
|
||||
$update->upgrade_notice = $this->upgrade_notice;
|
||||
@@ -102,7 +104,7 @@ if ( !class_exists(Update::class, false) ):
|
||||
$update->icons = $icons;
|
||||
|
||||
//It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
|
||||
//but lets set it just in case a future release needs it.
|
||||
//but let's set it just in case a future release needs it.
|
||||
if ( !isset($update->icons['default']) ) {
|
||||
$update->icons['default'] = current($update->icons);
|
||||
}
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker as BaseUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Scheduler;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker as BaseUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Scheduler;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
if ( !class_exists(UpdateChecker::class, false) ):
|
||||
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Theme;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Theme;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !class_exists(PucFactory::class, false) ):
|
||||
|
||||
@@ -86,7 +86,7 @@ if ( !class_exists(PucFactory::class, false) ):
|
||||
throw new \RuntimeException(sprintf(
|
||||
'The update checker cannot determine if "%s" is a plugin or a theme. ' .
|
||||
'This is a bug. Please contact the PUC developer.',
|
||||
htmlentities($fullPath)
|
||||
esc_html($fullPath)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ if ( !class_exists(PucFactory::class, false) ):
|
||||
*
|
||||
* Normalize a filesystem path. Introduced in WP 3.9.
|
||||
* Copying here allows use of the class on earlier versions.
|
||||
* This version adapted from WP 4.8.2 (unchanged since 4.5.4)
|
||||
* This version adapted from WP 4.8.2 (unchanged since 4.5.7)
|
||||
*
|
||||
* @param string $path Path to normalize.
|
||||
* @return string Normalized path.
|
||||
@@ -239,7 +239,7 @@ if ( !class_exists(PucFactory::class, false) ):
|
||||
|
||||
//URI was not found so throw an error.
|
||||
throw new \RuntimeException(
|
||||
sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath))
|
||||
sprintf('Unable to locate URI in header of "%s"', esc_html($fullPath))
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(Scheduler::class, false) ):
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(StateStore::class, false) ):
|
||||
|
||||
@@ -77,7 +77,7 @@ if ( !class_exists(StateStore::class, false) ):
|
||||
* @param Update|null $update
|
||||
* @return $this
|
||||
*/
|
||||
public function setUpdate(Update $update = null) {
|
||||
public function setUpdate($update = null) {
|
||||
$this->lazyLoad();
|
||||
$this->update = $update;
|
||||
return $this;
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Theme;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Theme;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
|
||||
|
||||
if ( !class_exists(Package::class, false) ):
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Theme;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Theme;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Update as BaseUpdate;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Update as BaseUpdate;
|
||||
|
||||
if ( !class_exists(Update::class, false) ):
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Theme;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Theme;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\UpdateChecker as BaseUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Scheduler;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\DebugBar;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker as BaseUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Scheduler;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
if ( !class_exists(UpdateChecker::class, false) ):
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
use stdClass;
|
||||
|
||||
+172
-20
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
use stdClass;
|
||||
use WP_Error;
|
||||
@@ -171,6 +171,10 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
//Allow HTTP requests to the metadata URL even if it's on a local host.
|
||||
add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);
|
||||
|
||||
//Potentially exclude information about this entity from core update check requests to api.wordpress.org.
|
||||
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- Doesn't modify timeouts.
|
||||
add_filter('http_request_args', array($this, 'excludeEntityFromWordPressAPI'), 10, 2);
|
||||
|
||||
//DebugBar integration.
|
||||
if ( did_action('plugins_loaded') ) {
|
||||
$this->maybeInitDebugBar();
|
||||
@@ -192,6 +196,7 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
|
||||
remove_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10);
|
||||
remove_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10);
|
||||
remove_filter('http_request_args', array($this, 'excludeEntityFromWordPressAPI'));
|
||||
remove_action('plugins_loaded', array($this, 'maybeInitDebugBar'));
|
||||
|
||||
remove_action('init', array($this, 'loadTextDomain'));
|
||||
@@ -266,6 +271,76 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
*/
|
||||
abstract protected function createScheduler($checkPeriod);
|
||||
|
||||
/**
|
||||
* Remove information about this plugin or theme from the requests that WordPress core sends
|
||||
* to api.wordpress.org when checking for updates.
|
||||
*
|
||||
* @param array $args
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function excludeEntityFromWordPressAPI($args, $url) {
|
||||
//Is this an api.wordpress.org update check request?
|
||||
$parsedUrl = wp_parse_url($url);
|
||||
if ( !isset($parsedUrl['host']) || (strtolower($parsedUrl['host']) !== 'api.wordpress.org') ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$typePluralised = $this->componentType . 's';
|
||||
$expectedPathPrefix = '/' . $typePluralised . '/update-check/1.'; //e.g. "/plugins/update-check/1.1/"
|
||||
if ( !isset($parsedUrl['path']) || !Utils::startsWith($parsedUrl['path'], $expectedPathPrefix) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
//Plugins and themes can disable this feature by using the filter below.
|
||||
if ( !apply_filters(
|
||||
$this->getUniqueName('remove_from_default_update_checks'),
|
||||
true, $this, $args, $url
|
||||
) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
if ( empty($args['body'][$typePluralised]) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$reportingItems = json_decode($args['body'][$typePluralised], true);
|
||||
if ( $reportingItems === null ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
//The list of installed items uses different key formats for plugins and themes.
|
||||
//Luckily, we can reuse the getUpdateListKey() method here.
|
||||
$updateListKey = $this->getUpdateListKey();
|
||||
if ( isset($reportingItems[$typePluralised][$updateListKey]) ) {
|
||||
unset($reportingItems[$typePluralised][$updateListKey]);
|
||||
}
|
||||
|
||||
if ( !empty($reportingItems['active']) ) {
|
||||
if ( is_array($reportingItems['active']) ) {
|
||||
foreach ($reportingItems['active'] as $index => $relativePath) {
|
||||
if ( $relativePath === $updateListKey ) {
|
||||
unset($reportingItems['active'][$index]);
|
||||
}
|
||||
}
|
||||
//Re-index the array.
|
||||
$reportingItems['active'] = array_values($reportingItems['active']);
|
||||
} else if ( $reportingItems['active'] === $updateListKey ) {
|
||||
//For themes, the "active" field is a string that contains the theme's directory name.
|
||||
//Pretend that the default theme is active so that we don't reveal the actual theme.
|
||||
if ( defined('WP_DEFAULT_THEME') ) {
|
||||
$reportingItems['active'] = WP_DEFAULT_THEME;
|
||||
}
|
||||
|
||||
//Unfortunately, it doesn't seem to be documented if we can safely remove the "active"
|
||||
//key. So when we don't know the default theme, we'll just leave it as is.
|
||||
}
|
||||
}
|
||||
|
||||
$args['body'][$typePluralised] = wp_json_encode($reportingItems);
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates. The results are stored in the DB option specified in $optionName.
|
||||
*
|
||||
@@ -384,7 +459,7 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
*
|
||||
* @param Metadata|null $update
|
||||
*/
|
||||
protected function fixSupportedWordpressVersion(Metadata $update = null) {
|
||||
protected function fixSupportedWordpressVersion($update = null) {
|
||||
if ( !isset($update->tested) || !preg_match('/^\d++\.\d++$/', $update->tested) ) {
|
||||
return;
|
||||
}
|
||||
@@ -557,7 +632,15 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
if ( !empty($update) ) {
|
||||
//Let plugins filter the update info before it's passed on to WordPress.
|
||||
$update = apply_filters($this->getUniqueName('pre_inject_update'), $update);
|
||||
$updates = $this->addUpdateToList($updates, $update->toWpFormat());
|
||||
//Convert the update into the format used by WordPress core.
|
||||
$wpUpdate = $update->toWpFormat();
|
||||
//Disable the "autoupdate" flag unless explicitly allowed. This is a safety precaution;
|
||||
//untrusted or compromised update sources could otherwise set this flag to true even
|
||||
//if the plugin/theme developer didn't intend to allow automatic update installation.
|
||||
if ( isset($wpUpdate->autoupdate) && !$this->isAutoupdateFieldAllowed($update) ) {
|
||||
$wpUpdate->autoupdate = false;
|
||||
}
|
||||
$updates = $this->addUpdateToList($updates, $wpUpdate);
|
||||
} else {
|
||||
//Clean up any stale update info.
|
||||
$updates = $this->removeUpdateFromList($updates);
|
||||
@@ -655,6 +738,38 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $autoupdateFieldAllowed = false;
|
||||
|
||||
/**
|
||||
* Allow the "autoupdate" field in incoming plugin updates to be set to `true`.
|
||||
*
|
||||
* By default, the update checker will parse the field (so it will be available in the update
|
||||
* object), but will set it to `false` before passing the update to WordPress.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function allowAutoupdateField() {
|
||||
$this->autoupdateFieldAllowed = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the "autoupdate" field for injected updates allowed to be set to true?
|
||||
*
|
||||
* @param object $incomingUpdate
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAutoupdateFieldAllowed($incomingUpdate) {
|
||||
return apply_filters(
|
||||
$this->getUniqueName('autoupdate_field_allowed'),
|
||||
$this->autoupdateFieldAllowed,
|
||||
$incomingUpdate
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------
|
||||
* JSON-based update API
|
||||
* -------------------------------------------------------------------
|
||||
@@ -698,7 +813,7 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
$result = wp_remote_get($url, $options);
|
||||
|
||||
$result = apply_filters($this->getUniqueName('request_metadata_http_result'), $result, $url, $options);
|
||||
|
||||
|
||||
//Try to parse the response
|
||||
$status = $this->validateApiResponse($result);
|
||||
$metadata = null;
|
||||
@@ -924,25 +1039,62 @@ if ( !class_exists(UpdateChecker::class, false) ):
|
||||
return $source;
|
||||
}
|
||||
|
||||
//Fix the remote source structure if necessary.
|
||||
//The update archive should contain a single directory that contains the rest of plugin/theme files.
|
||||
//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
|
||||
//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
|
||||
//after update.
|
||||
if ( $this->isBadDirectoryStructure($remoteSource) ) {
|
||||
//Create a new directory using the plugin slug.
|
||||
$newDirectory = trailingslashit($remoteSource) . $this->slug . '/';
|
||||
|
||||
if ( !$wp_filesystem->is_dir($newDirectory) ) {
|
||||
$wp_filesystem->mkdir($newDirectory);
|
||||
|
||||
//Move all files to the newly created directory.
|
||||
$sourceFiles = $wp_filesystem->dirlist($remoteSource);
|
||||
if ( is_array($sourceFiles) ) {
|
||||
$sourceFiles = array_keys($sourceFiles);
|
||||
$allMoved = true;
|
||||
foreach ($sourceFiles as $filename) {
|
||||
//Skip our newly created folder.
|
||||
if ( $filename === $this->slug ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$previousSource = trailingslashit($remoteSource) . $filename;
|
||||
$newSource = trailingslashit($newDirectory) . $filename;
|
||||
|
||||
if ( !$wp_filesystem->move($previousSource, $newSource, true) ) {
|
||||
$allMoved = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $allMoved ) {
|
||||
//Rename source.
|
||||
$source = $newDirectory;
|
||||
} else {
|
||||
//Delete our newly created folder including all files in it.
|
||||
$wp_filesystem->rmdir($newDirectory, true);
|
||||
|
||||
//And return a relevant error.
|
||||
return new WP_Error(
|
||||
'puc-incorrect-directory-structure',
|
||||
sprintf(
|
||||
'The directory structure of the update was incorrect. All files should be inside ' .
|
||||
'a directory named <span class="code">%s</span>, not at the root of the ZIP archive. Plugin Update Checker tried to fix the directory structure, but failed.',
|
||||
esc_html($this->slug)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Rename the source to match the existing directory.
|
||||
$correctedSource = trailingslashit($remoteSource) . $this->directoryName . '/';
|
||||
if ( $source !== $correctedSource ) {
|
||||
//The update archive should contain a single directory that contains the rest of plugin/theme files.
|
||||
//Otherwise, WordPress will try to copy the entire working directory ($source == $remoteSource).
|
||||
//We can't rename $remoteSource because that would break WordPress code that cleans up temporary files
|
||||
//after update.
|
||||
if ( $this->isBadDirectoryStructure($remoteSource) ) {
|
||||
return new WP_Error(
|
||||
'puc-incorrect-directory-structure',
|
||||
sprintf(
|
||||
'The directory structure of the update is incorrect. All files should be inside ' .
|
||||
'a directory named <span class="code">%s</span>, not at the root of the ZIP archive.',
|
||||
htmlentities($this->slug)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var \WP_Upgrader_Skin $upgrader ->skin */
|
||||
$upgrader->skin->feedback(sprintf(
|
||||
'Renaming %s to %s…',
|
||||
'<span class="code">' . basename($source) . '</span>',
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(UpgraderStatus::class, false) ):
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
if ( !class_exists(Utils::class, false) ):
|
||||
|
||||
+151
-2
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use Parsedown;
|
||||
use PucReadmeParser;
|
||||
@@ -51,6 +51,16 @@ if ( !class_exists(Api::class, false) ):
|
||||
*/
|
||||
protected $credentials = null;
|
||||
|
||||
/**
|
||||
* @var string|null The value of the "Authorization" header for API requests.
|
||||
*/
|
||||
private $authorizationHeader = null;
|
||||
|
||||
/**
|
||||
* @var string|null If set, add the "Authorization" header to update downloads that start with this prefix.
|
||||
*/
|
||||
private $downloadUrlPrefixRequiringAuth = null;
|
||||
|
||||
/**
|
||||
* @var string The filter tag that's used to filter options passed to wp_remote_get.
|
||||
* For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
|
||||
@@ -322,6 +332,22 @@ if ( !class_exists(Api::class, false) ):
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getApiRequestHttpOptions() {
|
||||
$options = ['timeout' => wp_doing_cron() ? 10 : 3];
|
||||
if ( $this->isAuthenticationEnabled() && !empty($this->authorizationHeader) ) {
|
||||
$options['headers'] = ['Authorization' => $this->authorizationHeader];
|
||||
}
|
||||
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication credentials.
|
||||
*
|
||||
@@ -332,7 +358,130 @@ if ( !class_exists(Api::class, false) ):
|
||||
}
|
||||
|
||||
public function isAuthenticationEnabled() {
|
||||
return !empty($this->credentials);
|
||||
return !empty($this->credentials) || !empty($this->authorizationHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the "Authorization" header for API requests, if any.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizationHeader() {
|
||||
return $this->authorizationHeader ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable basic access authentication with the specified username and password.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string|null $downloadUrlPrefix Optionally, add the same Authorization header to update
|
||||
* downloads where the download URL starts with this prefix.
|
||||
*/
|
||||
protected function enableBasicAuth($username, $password, $downloadUrlPrefix = null) {
|
||||
$this->authorizationHeader = 'Basic ' . base64_encode($username . ':' . $password);
|
||||
|
||||
$this->downloadUrlPrefixRequiringAuth = $downloadUrlPrefix;
|
||||
if ( !empty($this->downloadUrlPrefixRequiringAuth) ) {
|
||||
$this->enableDownloadRequestFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool Whether the hook that registers download request filter(s) has already been added.
|
||||
*/
|
||||
private $preDownloadHookAdded = false;
|
||||
|
||||
/**
|
||||
* Enable the hooks that let you filter HTTP requests for update downloads.
|
||||
*/
|
||||
protected function enableDownloadRequestFilter() {
|
||||
if ( $this->preDownloadHookAdded ) {
|
||||
return;
|
||||
}
|
||||
$this->preDownloadHookAdded = true;
|
||||
|
||||
//Optimization: Instead of filtering all HTTP requests, let's do it only when
|
||||
//WordPress is about to download an update. So this is a two-step process;
|
||||
//the actual request arg filter is added in the following hook.
|
||||
add_filter('upgrader_pre_download', [$this, 'addDownloadRequestFilters']); //WP 3.7+
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $downloadFiltersAdded = false;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param bool $result Pass-through value for the "upgrader_pre_download" filter. Ignored by this callback.
|
||||
* @return bool
|
||||
*/
|
||||
public function addDownloadRequestFilters($result) {
|
||||
if ( !$this->downloadFiltersAdded ) {
|
||||
$this->downloadFiltersAdded = true;
|
||||
|
||||
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout.
|
||||
add_filter('http_request_args', [$this, 'filterUpdateDownloadRequestArgs'], 10, 2);
|
||||
|
||||
$authorizationHeader = $this->getAuthorizationHeader();
|
||||
if ( $this->isAuthenticationEnabled() && !empty($authorizationHeader) ) {
|
||||
add_action('requests-requests.before_redirect', [$this, 'removeAuthHeaderFromRedirects'], 10, 2);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter request arguments/options for update downloads.
|
||||
*
|
||||
* Note that this callback can potentially be called for *any* update download. You still
|
||||
* need to verify that the URL is one of yours.
|
||||
*
|
||||
* @internal
|
||||
* @param array $requestArgs
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') {
|
||||
//Add an authorization header to our downloads if needed.
|
||||
$authHeader = $this->getAuthorizationHeader();
|
||||
if (
|
||||
$this->isAuthenticationEnabled()
|
||||
&& !empty($authHeader)
|
||||
&& !empty($this->downloadUrlPrefixRequiringAuth)
|
||||
&& ((strpos($url, $this->downloadUrlPrefixRequiringAuth)) === 0)
|
||||
) {
|
||||
$requestArgs['headers']['Authorization'] = $authHeader;
|
||||
}
|
||||
return $requestArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* At least in older WP versions, when following a redirect, the Requests library will
|
||||
* automatically forward the Authorization header to other hosts. We don't want that
|
||||
* because it breaks AWS downloads and can leak authorization information.
|
||||
*
|
||||
* @param string $location
|
||||
* @param array $headers
|
||||
* @internal
|
||||
*/
|
||||
public function removeAuthHeaderFromRedirects(&$location, &$headers) {
|
||||
if (
|
||||
//If there's no download URL prefix configured, we would not have added an auth header,
|
||||
//and there's also no way to check if this URL needs auth or not.
|
||||
empty($this->downloadUrlPrefixRequiringAuth)
|
||||
//If this request goes to our download URL, we can leave the header.
|
||||
|| ((strpos($location, $this->downloadUrlPrefixRequiringAuth)) === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Remove the header.
|
||||
$authorizationHeader = $this->getAuthorizationHeader();
|
||||
if ( isset($headers['Authorization']) && ($headers['Authorization'] === $authorizationHeader) ) {
|
||||
unset($headers['Authorization']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !interface_exists(BaseChecker::class, false) ):
|
||||
|
||||
+23
-36
@@ -1,18 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\OAuthSignature;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Utils;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Utils;
|
||||
|
||||
if ( !class_exists(BitBucketApi::class, false) ):
|
||||
|
||||
class BitBucketApi extends Api {
|
||||
/**
|
||||
* @var OAuthSignature
|
||||
*/
|
||||
private $oauth = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
@@ -148,11 +142,19 @@ if ( !class_exists(BitBucketApi::class, false) ):
|
||||
* @return string
|
||||
*/
|
||||
protected function getDownloadUrl($ref) {
|
||||
return $this->getDownloadBaseUrl() . $ref . '.zip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for ZIP downloads from our repo. Includes the trailing slash.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDownloadBaseUrl() {
|
||||
return sprintf(
|
||||
'https://bitbucket.org/%s/%s/get/%s.zip',
|
||||
'https://bitbucket.org/%s/%s/get/',
|
||||
$this->username,
|
||||
$this->repository,
|
||||
$ref
|
||||
$this->repository
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,15 +208,7 @@ if ( !class_exists(BitBucketApi::class, false) ):
|
||||
));
|
||||
$baseUrl = $url;
|
||||
|
||||
if ( $this->oauth ) {
|
||||
$url = $this->oauth->sign($url,'GET');
|
||||
}
|
||||
|
||||
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
$response = wp_remote_get($url, $options);
|
||||
$response = wp_remote_get($url, $this->getApiRequestHttpOptions());
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
@@ -248,25 +242,18 @@ if ( !class_exists(BitBucketApi::class, false) ):
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
|
||||
if ( !empty($credentials) && !empty($credentials['consumer_key']) ) {
|
||||
$this->oauth = new OAuthSignature(
|
||||
$credentials['consumer_key'],
|
||||
$credentials['consumer_secret']
|
||||
if (
|
||||
is_array($credentials)
|
||||
&& !empty($credentials['username'])
|
||||
&& !empty($credentials['api_token'])
|
||||
) {
|
||||
$this->enableBasicAuth(
|
||||
$credentials['username'],
|
||||
$credentials['api_token'],
|
||||
$this->getDownloadBaseUrl()
|
||||
);
|
||||
} else {
|
||||
$this->oauth = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function signDownloadUrl($url) {
|
||||
//Add authentication data to download URLs. Since OAuth signatures incorporate
|
||||
//timestamps, we have to do this immediately before inserting the update. Otherwise,
|
||||
//authentication could fail due to a stale timestamp.
|
||||
if ( $this->oauth ) {
|
||||
$url = $this->oauth->sign($url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
+15
-81
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use Parsedown;
|
||||
|
||||
@@ -29,11 +29,6 @@ if ( !class_exists(GitHubApi::class, false) ):
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $downloadFilterAdded = false;
|
||||
|
||||
public function __construct($repositoryUrl, $accessToken = null) {
|
||||
$path = wp_parse_url($repositoryUrl, PHP_URL_PATH);
|
||||
if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
|
||||
@@ -248,15 +243,7 @@ if ( !class_exists(GitHubApi::class, false) ):
|
||||
$baseUrl = $url;
|
||||
$url = $this->buildApiUrl($url, $queryParams);
|
||||
|
||||
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
|
||||
if ( $this->isAuthenticationEnabled() ) {
|
||||
$options['headers'] = array('Authorization' => $this->getAuthorizationHeader());
|
||||
}
|
||||
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
$response = wp_remote_get($url, $options);
|
||||
$response = wp_remote_get($url, $this->getApiRequestHttpOptions());
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
@@ -348,11 +335,17 @@ if ( !class_exists(GitHubApi::class, false) ):
|
||||
|
||||
public function setAuthentication($credentials) {
|
||||
parent::setAuthentication($credentials);
|
||||
//This property is no longer used internally, but is kept for backwards compatibility.
|
||||
$this->accessToken = is_string($credentials) ? $credentials : null;
|
||||
|
||||
//Optimization: Instead of filtering all HTTP requests, let's do it only when
|
||||
//WordPress is about to download an update.
|
||||
add_filter('upgrader_pre_download', array($this, 'addHttpRequestFilter'), 10, 1); //WP 3.7+
|
||||
if ( is_string($credentials) && !empty($credentials) ) {
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', []);
|
||||
$this->enableBasicAuth($this->userName, $credentials, $repoApiBaseUrl);
|
||||
}
|
||||
|
||||
//Assets sometimes need an Accept header, so we add a request filter even if basic
|
||||
//authentication isn't enabled.
|
||||
$this->enableDownloadRequestFilter();
|
||||
}
|
||||
|
||||
protected function getUpdateDetectionStrategies($configBranch) {
|
||||
@@ -393,74 +386,15 @@ if ( !class_exists(GitHubApi::class, false) ):
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $result
|
||||
* @return bool
|
||||
* @internal
|
||||
*/
|
||||
public function addHttpRequestFilter($result) {
|
||||
if ( !$this->downloadFilterAdded && $this->isAuthenticationEnabled() ) {
|
||||
//phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args -- The callback doesn't change the timeout.
|
||||
add_filter('http_request_args', array($this, 'setUpdateDownloadHeaders'), 10, 2);
|
||||
add_action('requests-requests.before_redirect', array($this, 'removeAuthHeaderFromRedirects'), 10, 4);
|
||||
$this->downloadFilterAdded = true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP headers that are necessary to download updates from private repositories.
|
||||
*
|
||||
* See GitHub docs:
|
||||
*
|
||||
* @link https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
|
||||
* @link https://developer.github.com/v3/auth/#basic-authentication
|
||||
*
|
||||
* @internal
|
||||
* @param array $requestArgs
|
||||
* @param string $url
|
||||
* @return array
|
||||
*/
|
||||
public function setUpdateDownloadHeaders($requestArgs, $url = '') {
|
||||
public function filterUpdateDownloadRequestArgs($requestArgs, $url = '') {
|
||||
//Release assets need an "Accept" header. See GitHub Docs:
|
||||
//https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
|
||||
//Is WordPress trying to download one of our release assets?
|
||||
if ( $this->releaseAssetsEnabled && (strpos($url, $this->getAssetApiBaseUrl()) !== false) ) {
|
||||
$requestArgs['headers']['Accept'] = 'application/octet-stream';
|
||||
}
|
||||
//Use Basic authentication, but only if the download is from our repository.
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
|
||||
if ( $this->isAuthenticationEnabled() && (strpos($url, $repoApiBaseUrl)) === 0 ) {
|
||||
$requestArgs['headers']['Authorization'] = $this->getAuthorizationHeader();
|
||||
}
|
||||
return $requestArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* When following a redirect, the Requests library will automatically forward
|
||||
* the authorization header to other hosts. We don't want that because it breaks
|
||||
* AWS downloads and can leak authorization information.
|
||||
*
|
||||
* @param string $location
|
||||
* @param array $headers
|
||||
* @internal
|
||||
*/
|
||||
public function removeAuthHeaderFromRedirects(&$location, &$headers) {
|
||||
$repoApiBaseUrl = $this->buildApiUrl('/repos/:user/:repo/', array());
|
||||
if ( strpos($location, $repoApiBaseUrl) === 0 ) {
|
||||
return; //This request is going to GitHub, so it's fine.
|
||||
}
|
||||
//Remove the header.
|
||||
if ( isset($headers['Authorization']) ) {
|
||||
unset($headers['Authorization']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the value of the "Authorization" header.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizationHeader() {
|
||||
return 'Basic ' . base64_encode($this->userName . ':' . $this->accessToken);
|
||||
return parent::filterUpdateDownloadRequestArgs($requestArgs, $url);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !class_exists(GitLabApi::class, false) ):
|
||||
|
||||
@@ -260,12 +260,7 @@ if ( !class_exists(GitLabApi::class, false) ):
|
||||
$baseUrl = $url;
|
||||
$url = $this->buildApiUrl($url, $queryParams);
|
||||
|
||||
$options = array('timeout' => wp_doing_cron() ? 10 : 3);
|
||||
if ( !empty($this->httpFilterName) ) {
|
||||
$options = apply_filters($this->httpFilterName, $options);
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, $options);
|
||||
$response = wp_remote_get($url, $this->getApiRequestHttpOptions());
|
||||
if ( is_wp_error($response) ) {
|
||||
do_action('puc_api_error', $response, null, $url, $this->slug);
|
||||
return $response;
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Plugin;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
if ( !class_exists(PluginUpdateChecker::class, false) ):
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !class_exists(Reference::class, false) ):
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !trait_exists(ReleaseAssetSupport::class, false) ) :
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !trait_exists(ReleaseFilteringFeature::class, false) ) :
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Theme;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Utils;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Theme;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Utils;
|
||||
|
||||
if ( !class_exists(ThemeUpdateChecker::class, false) ):
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4\Vcs;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Vcs;
|
||||
|
||||
if ( !trait_exists(VcsCheckerMethods::class, false) ) :
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
use WP_CLI;
|
||||
|
||||
@@ -29,7 +29,7 @@ From the users' perspective, it works just like with plugins and themes hosted o
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
*Note:* In each of the below examples, part of the instructions are to create an instance of the update checker class. It's recommended to do this either during the `plugins_loaded` action or outside of any hooks. If you do it only during an `admin_*` action, then updates will not be visible to a wide variety of WordPress maanagement tools; they will only be visible to logged-in users on dashboard pages.
|
||||
*Note:* In each of the below examples, part of the instructions is to create an instance of the update checker class. It's recommended to do this either during the `plugins_loaded` action or outside of any hooks. If you do it only during an `admin_*` action, then updates will not be visible to a wide variety of WordPress management tools; they will only be visible to logged-in users on dashboard pages.
|
||||
|
||||
### Self-hosted Plugins and Themes
|
||||
|
||||
@@ -128,6 +128,11 @@ This library supports a couple of different ways to release updates on GitHub. P
|
||||
$myUpdateChecker->getVcsApi()->enableReleaseAssets();
|
||||
```
|
||||
|
||||
By default, PUC will simply use the first available asset. You can pass a regular expression to `enableReleaseAssets()` to filter assets by name. For example:
|
||||
```php
|
||||
$myUpdateChecker->getVcsApi()->enableReleaseAssets('/\.zip($|[?&#])/i');
|
||||
```
|
||||
|
||||
- **Tags**
|
||||
|
||||
To release version 1.2.3, create a new Git tag named `v1.2.3` or `1.2.3`. That's it.
|
||||
@@ -188,14 +193,12 @@ The library will pull update details from the following parts of a release/tag/b
|
||||
'unique-plugin-or-theme-slug'
|
||||
);
|
||||
|
||||
//Optional: If you're using a private repository, create an OAuth consumer
|
||||
//and set the authentication credentials like this:
|
||||
//Note: For now you need to check "This is a private consumer" when
|
||||
//creating the consumer to work around #134:
|
||||
// https://github.com/YahnisElsts/plugin-update-checker/issues/134
|
||||
//Optional: If you're using a private repository, create an API token
|
||||
//with the "read:repository:bitbucket" scope and set the authentication
|
||||
//credentials like this:
|
||||
$myUpdateChecker->setAuthentication(array(
|
||||
'consumer_key' => '...',
|
||||
'consumer_secret' => '...',
|
||||
'username' => 'example@example.com', //Your BitBucket email address.
|
||||
'api_token' => '...',
|
||||
));
|
||||
|
||||
//Optional: Set the branch that contains the stable release.
|
||||
@@ -252,8 +255,8 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli
|
||||
|
||||
Alternatively, if you're using a self-hosted GitLab instance, initialize the update checker like this:
|
||||
```php
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi;
|
||||
|
||||
$myUpdateChecker = new PluginUpdateChecker(
|
||||
new GitLabApi('https://myserver.com/user-name/repo-name/'),
|
||||
@@ -264,8 +267,8 @@ BitBucket doesn't have an equivalent to GitHub's releases, so the process is sli
|
||||
```
|
||||
If you're using a self-hosted GitLab instance and [subgroups or nested groups](https://docs.gitlab.com/ce/user/group/subgroups/index.html), you have to tell the update checker which parts of the URL are subgroups:
|
||||
```php
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi;
|
||||
|
||||
$myUpdateChecker = new PluginUpdateChecker(
|
||||
new GitLabApi(
|
||||
@@ -347,14 +350,14 @@ Other classes have also been renamed, usually by simply removing the `Puc_vXpY_`
|
||||
| Old class name | New class name |
|
||||
|-------------------------------------|----------------------------------------------------------------|
|
||||
| `Puc_v4_Factory` | `YahnisElsts\PluginUpdateChecker\v5\PucFactory` |
|
||||
| `Puc_v4p13_Factory` | `YahnisElsts\PluginUpdateChecker\v5p4\PucFactory` |
|
||||
| `Puc_v4p13_Plugin_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Plugin\UpdateChecker` |
|
||||
| `Puc_v4p13_Theme_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Theme\UpdateChecker` |
|
||||
| `Puc_v4p13_Vcs_PluginUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\PluginUpdateChecker` |
|
||||
| `Puc_v4p13_Vcs_ThemeUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\ThemeUpdateChecker` |
|
||||
| `Puc_v4p13_Vcs_GitHubApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitHubApi` |
|
||||
| `Puc_v4p13_Vcs_GitLabApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\GitLabApi` |
|
||||
| `Puc_v4p13_Vcs_BitBucketApi` | `YahnisElsts\PluginUpdateChecker\v5p4\Vcs\BitBucketApi` |
|
||||
| `Puc_v4p13_Factory` | `YahnisElsts\PluginUpdateChecker\v5p7\PucFactory` |
|
||||
| `Puc_v4p13_Plugin_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Plugin\UpdateChecker` |
|
||||
| `Puc_v4p13_Theme_UpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Theme\UpdateChecker` |
|
||||
| `Puc_v4p13_Vcs_PluginUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\PluginUpdateChecker` |
|
||||
| `Puc_v4p13_Vcs_ThemeUpdateChecker` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\ThemeUpdateChecker` |
|
||||
| `Puc_v4p13_Vcs_GitHubApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitHubApi` |
|
||||
| `Puc_v4p13_Vcs_GitLabApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\GitLabApi` |
|
||||
| `Puc_v4p13_Vcs_BitBucketApi` | `YahnisElsts\PluginUpdateChecker\v5p7\Vcs\BitBucketApi` |
|
||||
|
||||
License Management
|
||||
------------------
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
"ext-json": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"files": ["load-v5p4.php"]
|
||||
"files": ["load-v5p7.php"]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,52 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: plugin-update-checker\n"
|
||||
"POT-Creation-Date: 2022-07-29 15:34+0300\n"
|
||||
"PO-Revision-Date: 2024-05-09 22:22+0000\n"
|
||||
"Last-Translator: theogk\n"
|
||||
"Language-Team: Ελληνικά\n"
|
||||
"Language: el\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Loco https://localise.biz/\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"X-Loco-Version: 2.6.9; wp-6.5.3"
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:128
|
||||
msgid "Check for updates"
|
||||
msgstr "Έλεγχος για ενημερώσεις"
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:214
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "The %s plugin is up to date."
|
||||
msgstr "Το πρόσθετο %s είναι ενημερωμένο."
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:216
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "A new version of the %s plugin is available."
|
||||
msgstr "Μία νέα έκδοση είναι διαθέσιμη για το πρόσθετο %s."
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:218
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "Could not determine if updates are available for %s."
|
||||
msgstr ""
|
||||
"Δεν ήταν εφικτό να εκτελεστεί ο έλεγχος για νέες ενημερώσεις για το πρόσθετο "
|
||||
"%s."
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:224
|
||||
#, php-format
|
||||
msgid "Unknown update checker status \"%s\""
|
||||
msgstr "Άγνωστο πρόβλημα του ενημερωτή προσθέτων \"%s\""
|
||||
|
||||
#: Puc/v5p4/Vcs/PluginUpdateChecker.php:100
|
||||
msgid "There is no changelog available."
|
||||
msgstr "Δεν υπάρχει διαθέσιμο αρχείο αλλαγών."
|
||||
Binary file not shown.
@@ -1,38 +1,50 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: plugin-update-checker\n"
|
||||
"POT-Creation-Date: 2016-02-17 14:21+0100\n"
|
||||
"PO-Revision-Date: 2016-10-28 14:30+0330\n"
|
||||
"Last-Translator: studio RVOLA <hello@rvola.com>\n"
|
||||
"Language-Team: Pro Style <info@prostyle.ir>\n"
|
||||
"POT-Creation-Date: 2025-06-12 23:40+0100\n"
|
||||
"PO-Revision-Date: 2025-06-12 23:49+0100\n"
|
||||
"Last-Translator: Pro Style <info@prostyle.ir>\n"
|
||||
"Language-Team: Alex Javadi <alex@aljm.org>\n"
|
||||
"Language: fa_IR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.8.8\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Generator: Poedit 3.6\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: __;_e\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: github-checker.php:120
|
||||
msgid "There is no changelog available."
|
||||
msgstr "شرحی برای تغییرات یافت نشد"
|
||||
#: Puc/v5p6/Plugin/Ui.php:56
|
||||
msgid "View details"
|
||||
msgstr "مشاهده جزئیات"
|
||||
|
||||
#: plugin-update-checker.php:637
|
||||
#: Puc/v5p6/Plugin/Ui.php:79
|
||||
#, php-format
|
||||
msgid "More information about %s"
|
||||
msgstr "اطلاعات بیشتر درباره %s"
|
||||
|
||||
# It had some “potential” grammar issues and also didn’t sound native.
|
||||
# P.S. I know the current translation is literally “Checking for new updates”, however, I thought it might sound more natural and known to others.
|
||||
#: Puc/v5p6/Plugin/Ui.php:130
|
||||
msgid "Check for updates"
|
||||
msgstr "بررسی برای بروزرسانی "
|
||||
msgstr "بررسی بروزرسانی جدید"
|
||||
|
||||
#: plugin-update-checker.php:681
|
||||
msgid "This plugin is up to date."
|
||||
msgstr "شما از آخرین نسخه استفاده میکنید . بهروز باشید"
|
||||
|
||||
#: plugin-update-checker.php:683
|
||||
msgid "A new version of this plugin is available."
|
||||
msgstr "نسخه جدیدی برای افزونه ارائه شده است ."
|
||||
|
||||
#: plugin-update-checker.php:685
|
||||
# The word “ناشناخته” is seems to be translated directly from the word (Un-known), rather than checking for the context.
|
||||
# I think “نامشخص” (unknown) might be a suitable version in this scenario.
|
||||
#: Puc/v5p6/Plugin/Ui.php:227
|
||||
#, php-format
|
||||
msgid "Unknown update checker status \"%s\""
|
||||
msgstr "وضعیت ناشناخته برای بروزرسانی \"%s\""
|
||||
msgstr "وضعیت نامشخص برای بروزرسانی \"%s\""
|
||||
|
||||
# The previous translation was okay, however, it didn’t sound native to me.
|
||||
#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113
|
||||
msgid "There is no changelog available."
|
||||
msgstr "آخرین تغییراتی یافت نشد."
|
||||
|
||||
#~ msgid "This plugin is up to date."
|
||||
#~ msgstr "شما از آخرین نسخه استفاده میکنید . بهروز باشید"
|
||||
|
||||
#~ msgid "A new version of this plugin is available."
|
||||
#~ msgstr "نسخه جدیدی برای افزونه ارائه شده است ."
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: plugin-update-checker\n"
|
||||
"POT-Creation-Date: 2024-12-09 11:45+0100\n"
|
||||
"PO-Revision-Date: 2024-12-09 12:20+0100\n"
|
||||
"Last-Translator: Aleksandar Urošević <urke.kg@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: sr_RS\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 2.4.3\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:56
|
||||
msgid "View details"
|
||||
msgstr "Види детаље"
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:79
|
||||
#, php-format
|
||||
msgid "More information about %s"
|
||||
msgstr "Више информација о %s"
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:130
|
||||
msgid "Check for updates"
|
||||
msgstr "Провера ажурирања"
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:217
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "The %s plugin is up to date."
|
||||
msgstr "Додатак %s је у најновијем издању."
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:219
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "A new version of the %s plugin is available."
|
||||
msgstr "Доступно је ново издање за %s."
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:221
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "Could not determine if updates are available for %s."
|
||||
msgstr "Није могуће утврдити да ли су доступне исправке за %s."
|
||||
|
||||
#: Puc/v5p5/Plugin/Ui.php:227
|
||||
#, php-format
|
||||
msgid "Unknown update checker status \"%s\""
|
||||
msgstr "Непознат статус провере ажурирања \"%s\""
|
||||
|
||||
#: Puc/v5p5/Vcs/PluginUpdateChecker.php:113
|
||||
msgid "There is no changelog available."
|
||||
msgstr "Белешке о изменама нису доступне."
|
||||
Binary file not shown.
@@ -1,57 +1,57 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: plugin-update-checker\n"
|
||||
"POT-Creation-Date: 2022-01-29 12:09+0800\n"
|
||||
"PO-Revision-Date: 2022-01-29 12:10+0800\n"
|
||||
"POT-Creation-Date: 2025-11-21 10:40+0800\n"
|
||||
"PO-Revision-Date: 2025-11-21 10:40+0800\n"
|
||||
"Last-Translator: Seaton Jiang <hi@seatonjiang.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.4.3\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Poedit 3.8\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:54
|
||||
#: Puc/v5p6/Plugin/Ui.php:56
|
||||
msgid "View details"
|
||||
msgstr "查看详情"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:77
|
||||
#: Puc/v5p6/Plugin/Ui.php:79
|
||||
#, php-format
|
||||
msgid "More information about %s"
|
||||
msgstr "%s 的更多信息"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:128
|
||||
#: Puc/v5p6/Plugin/Ui.php:130
|
||||
msgid "Check for updates"
|
||||
msgstr "检查更新"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:214
|
||||
#: Puc/v5p6/Plugin/Ui.php:217
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "The %s plugin is up to date."
|
||||
msgstr "%s 目前是最新版本。"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:216
|
||||
#: Puc/v5p6/Plugin/Ui.php:219
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "A new version of the %s plugin is available."
|
||||
msgstr "%s 当前有可用的更新。"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:218
|
||||
#: Puc/v5p6/Plugin/Ui.php:221
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "Could not determine if updates are available for %s."
|
||||
msgstr "%s 无法确定是否有可用的更新。"
|
||||
|
||||
#: Puc/v4p11/Plugin/Ui.php:224
|
||||
#: Puc/v5p6/Plugin/Ui.php:227
|
||||
#, php-format
|
||||
msgid "Unknown update checker status \"%s\""
|
||||
msgstr "未知的更新检查状态:%s"
|
||||
|
||||
#: Puc/v4p11/Vcs/PluginUpdateChecker.php:100
|
||||
#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113
|
||||
msgid "There is no changelog available."
|
||||
msgstr "没有可用的更新日志。"
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,66 @@
|
||||
# Blank Plugin POT Template
|
||||
# Copyright 2025 ...
|
||||
# This file is distributed under the GNU General Public License v3 or later.
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Plugin Update Checker\n"
|
||||
"Report-Msgid-Bugs-To: Alex Lion <learnwithalex@gmail.com>\n"
|
||||
"POT-Creation-Date: 2025-09-19 14:05-0700\n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Alex Lion <learnwithalex@gmail.com>\n"
|
||||
"Language-Team: Alex Lion <learnwithalex@gmail.com>\n"
|
||||
"Language: zh_TW\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Poedit-WPHeader: plugin-update-checker.php\n"
|
||||
"X-Textdomain-Support: yesX-Generator: Poedit 1.6.4\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: "
|
||||
"__;_e;esc_html_e;esc_html_x:1,2c;esc_html__;esc_attr_e;esc_attr_x:1,2c;esc_attr__;_ex:1,2c;_nx:4c,1,2;_nx_noop:4c,1,2;_x:1,2c;_n:1,2;_n_noop:1,2;__ngettext:1,2;__ngettext_noop:1,2;_c,_nc:4c,1,2\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"X-Poedit-Bookmarks: \n"
|
||||
"X-Generator: Poedit 3.7\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:56
|
||||
msgid "View details"
|
||||
msgstr "檢視詳細資料"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:79
|
||||
#, php-format
|
||||
msgid "More information about %s"
|
||||
msgstr "進一步了解 %s 的相關資訊"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:130
|
||||
msgid "Check for updates"
|
||||
msgstr "檢查更新"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:217
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "The %s plugin is up to date."
|
||||
msgstr "%s 外掛已為最新版本。"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:219
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "A new version of the %s plugin is available."
|
||||
msgstr "%s 外掛已有新版本可供更新。"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:221
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "Could not determine if updates are available for %s."
|
||||
msgstr "無法確定 %s 是否有可用的更新。"
|
||||
|
||||
#: Puc/v5p6/Plugin/Ui.php:227
|
||||
#, php-format
|
||||
msgid "Unknown update checker status \"%s\""
|
||||
msgstr "未知的更新檢查程式狀態: %s"
|
||||
|
||||
#: Puc/v5p6/Vcs/PluginUpdateChecker.php:113
|
||||
msgid "There is no changelog available."
|
||||
msgstr "目前沒有可供檢閱的變更記錄。"
|
||||
@@ -2,7 +2,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: plugin-update-checker\n"
|
||||
"POT-Creation-Date: 2022-07-29 15:34+0300\n"
|
||||
"POT-Creation-Date: 2025-05-20 15:27+0300\n"
|
||||
"PO-Revision-Date: 2016-01-10 20:59+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
@@ -11,39 +11,39 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 3.1.1\n"
|
||||
"X-Generator: Poedit 3.6\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: __;_e;_x:1,2c;_x\n"
|
||||
"X-Poedit-SearchPath-0: .\n"
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:128
|
||||
#: Puc/v5p7/Plugin/Ui.php:130
|
||||
msgid "Check for updates"
|
||||
msgstr ""
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:214
|
||||
#: Puc/v5p7/Plugin/Ui.php:217
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "The %s plugin is up to date."
|
||||
msgstr ""
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:216
|
||||
#: Puc/v5p7/Plugin/Ui.php:219
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "A new version of the %s plugin is available."
|
||||
msgstr ""
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:218
|
||||
#: Puc/v5p7/Plugin/Ui.php:221
|
||||
#, php-format
|
||||
msgctxt "the plugin title"
|
||||
msgid "Could not determine if updates are available for %s."
|
||||
msgstr ""
|
||||
|
||||
#: Puc/v5p4/Plugin/Ui.php:224
|
||||
#: Puc/v5p7/Plugin/Ui.php:227
|
||||
#, php-format
|
||||
msgid "Unknown update checker status \"%s\""
|
||||
msgstr ""
|
||||
|
||||
#: Puc/v5p4/Vcs/PluginUpdateChecker.php:100
|
||||
#: Puc/v5p7/Vcs/PluginUpdateChecker.php:113
|
||||
msgid "There is no changelog available."
|
||||
msgstr ""
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p4;
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5\PucFactory as MajorFactory;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p4\PucFactory as MinorFactory;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory as MinorFactory;
|
||||
|
||||
require __DIR__ . '/Puc/v5p4/Autoloader.php';
|
||||
require __DIR__ . '/Puc/v5p7/Autoloader.php';
|
||||
new Autoloader();
|
||||
|
||||
require __DIR__ . '/Puc/v5p4/PucFactory.php';
|
||||
require __DIR__ . '/Puc/v5p7/PucFactory.php';
|
||||
require __DIR__ . '/Puc/v5/PucFactory.php';
|
||||
|
||||
//Register classes defined in this version with the factory.
|
||||
@@ -26,9 +26,9 @@ foreach (
|
||||
)
|
||||
as $pucGeneralClass => $pucVersionedClass
|
||||
) {
|
||||
MajorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.4');
|
||||
MajorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.7');
|
||||
//Also add it to the minor-version factory in case the major-version factory
|
||||
//was already defined by another, older version of the update checker.
|
||||
MinorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.4');
|
||||
MinorFactory::addVersion($pucGeneralClass, $pucVersionedClass, '5.7');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Update Checker Library 5.4
|
||||
* Plugin Update Checker Library 5.7
|
||||
* http://w-shadow.com/
|
||||
*
|
||||
* Copyright 2024 Janis Elsts
|
||||
* Copyright 2026 Janis Elsts
|
||||
* Released under the MIT license. See license.txt for details.
|
||||
*/
|
||||
|
||||
require dirname(__FILE__) . '/load-v5p4.php';
|
||||
require dirname(__FILE__) . '/load-v5p7.php';
|
||||
@@ -648,7 +648,7 @@ class Parsedown
|
||||
#
|
||||
# Setext
|
||||
|
||||
protected function blockSetextHeader($Line, array $Block = null)
|
||||
protected function blockSetextHeader($Line, $Block = null)
|
||||
{
|
||||
if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
|
||||
{
|
||||
@@ -786,7 +786,7 @@ class Parsedown
|
||||
#
|
||||
# Table
|
||||
|
||||
protected function blockTable($Line, array $Block = null)
|
||||
protected function blockTable($Line, $Block = null)
|
||||
{
|
||||
if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# Release Workflow Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a GitHub Actions workflow that, on every `v*` tag push, builds `projects-portfolio-v<version>.zip` from an allowlist of runtime files and publishes it both as a workflow artifact and as an attachment to a matching GitHub release.
|
||||
|
||||
**Architecture:** Single workflow file at `.github/workflows/release.yml`. One job (`release`) on `ubuntu-latest` performs: checkout → bash build step (allowlisted `cp -r` into a staging dir, then `zip -r`) → `actions/upload-artifact@v4` → `softprops/action-gh-release@v2`. Uses the runner-provided `GITHUB_TOKEN` with `contents: write` permission.
|
||||
|
||||
**Tech Stack:** GitHub Actions YAML, bash, `zip` CLI (preinstalled on `ubuntu-latest`), `actions/checkout@v4`, `actions/upload-artifact@v4`, `softprops/action-gh-release@v2`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Workflow file lives at `.github/workflows/release.yml`.
|
||||
- Trigger: `on.push.tags: ['v*']`. No `workflow_dispatch`.
|
||||
- Permission: `contents: write`.
|
||||
- Zip filename: `projects-portfolio-v<version>.zip` where `<version>` is `GITHUB_REF_NAME` with leading `v` stripped.
|
||||
- Zip internal layout: `projects-portfolio/` directory at zip root, with `projects-portfolio.php` inside it (WordPress install-from-zip convention).
|
||||
- Allowlisted source paths copied into staging: `projects-portfolio.php`, `README.md`, `LICENSE`, `admin/`, `assets/`, `includes/`, `languages/`, `templates/`.
|
||||
- Excluded paths (no `cp` of these): `composer.json`, `composer.lock`, `vendor/`, `tests/`, `phpunit.xml`, `phpunit.xml.dist`, `.phpunit.result.cache`, `plans/`, `specs/`, `.github/`, `.gitignore`, `.vscode/`, `.claude/`.
|
||||
- Build step uses `set -euo pipefail` and an `unzip -l "$OUT"` debug print at the end so the action log self-documents the contents.
|
||||
- Artifact upload uses `if-no-files-found: error` so a missing zip fails the job.
|
||||
- `softprops/action-gh-release@v2` is given the tag name, release name = tag name, `generate_release_notes: true`, and `files:` pointing at the zip.
|
||||
- README gets a "Building a Release" section after "Connect Your Gitea Repo".
|
||||
- First release is `v1.1.1`: implementation includes a version bump from `1.1.0` → `1.1.1` (header `Version:` + `PROJECTS_PORTFOLIO_VERSION` constant).
|
||||
- No PHP files outside of the version bump are touched. The PHPUnit suite is unaffected.
|
||||
|
||||
## File Structure
|
||||
|
||||
**Created:**
|
||||
- `.github/workflows/release.yml` — the workflow.
|
||||
|
||||
**Modified:**
|
||||
- `README.md` — adds a "Building a Release" section after the existing "Connect Your Gitea Repo" section.
|
||||
- `projects-portfolio.php` — bumps header `Version:` and `PROJECTS_PORTFOLIO_VERSION` from `1.1.0` to `1.1.1` as part of the first-release rollout.
|
||||
|
||||
No other files touched. No tests added.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add the release workflow file
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/release.yml`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: a workflow that triggers on `v*` tag push and produces a release zip + GitHub release.
|
||||
|
||||
- [ ] **Step 1: Create `.github/workflows/release.yml`**
|
||||
|
||||
Create the directory if missing, then create the file with this exact content:
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build & publish plugin zip
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build plugin zip
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
STAGE="dist/staging"
|
||||
OUT="dist/projects-portfolio-v${VERSION}.zip"
|
||||
|
||||
rm -rf dist
|
||||
mkdir -p "$STAGE/projects-portfolio"
|
||||
|
||||
cp projects-portfolio.php "$STAGE/projects-portfolio/"
|
||||
cp README.md "$STAGE/projects-portfolio/"
|
||||
cp LICENSE "$STAGE/projects-portfolio/"
|
||||
cp -r admin/ "$STAGE/projects-portfolio/"
|
||||
cp -r assets/ "$STAGE/projects-portfolio/"
|
||||
cp -r includes/ "$STAGE/projects-portfolio/"
|
||||
cp -r languages/ "$STAGE/projects-portfolio/"
|
||||
cp -r templates/ "$STAGE/projects-portfolio/"
|
||||
|
||||
( cd "$STAGE" && zip -r "../../$OUT" projects-portfolio )
|
||||
|
||||
echo "Built $OUT"
|
||||
unzip -l "$OUT"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: projects-portfolio-v${{ github.ref_name }}
|
||||
path: dist/projects-portfolio-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
generate_release_notes: true
|
||||
files: dist/projects-portfolio-*.zip
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Validate YAML syntax locally**
|
||||
|
||||
Run from the project root:
|
||||
```bash
|
||||
php -r "require 'vendor/autoload.php';" 2>/dev/null || true
|
||||
# PHP doesn't have a built-in YAML parser; use Python's if available
|
||||
python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release.yml').read()); print('OK')"
|
||||
```
|
||||
|
||||
If `python` is not on PATH, use any other YAML validator you have. If none is available, skip this step — GitHub's own workflow validation will catch syntax errors on the next push.
|
||||
|
||||
Expected (if `python` is available): `OK` on stdout, exit 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add .github/workflows/release.yml
|
||||
git commit -m "Add GitHub Actions release workflow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add "Building a Release" section to README
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: a "Building a Release" section placed after the "Connect Your Gitea Repo" section (which was added in the previous feature).
|
||||
|
||||
- [ ] **Step 1: Insert the new section**
|
||||
|
||||
Find the existing `## Connect Your Gitea Repo` section in `README.md`. Locate the heading `## Building a Release` (it does not yet exist; that's the insertion target).
|
||||
|
||||
Insert the following block directly after the closing line of the Gitea repo subsection (`Paste it into the plugin settings screen under **Gitea API Token**.`) and before any subsequent `## ` heading:
|
||||
|
||||
```markdown
|
||||
|
||||
## Building a Release
|
||||
|
||||
Releases are automated via GitHub Actions. To cut a new release:
|
||||
|
||||
1. Bump `PROJECTS_PORTFOLIO_VERSION` and the plugin header `Version:` in `projects-portfolio.php`.
|
||||
2. Commit and push to `main`.
|
||||
3. Tag the release commit: `git tag -a v1.1.0 -m "v1.1.0 — short summary"`.
|
||||
4. Push the tag: `git push origin v1.1.0`.
|
||||
|
||||
The `.github/workflows/release.yml` workflow runs and:
|
||||
|
||||
- Builds `projects-portfolio-v<version>.zip` containing only the runtime files (`projects-portfolio.php`, `admin/`, `assets/`, `includes/`, `languages/`, `templates/`, `README.md`, `LICENSE`).
|
||||
- Uploads the zip as a workflow artifact.
|
||||
- Creates (or updates) the matching GitHub Release with the zip attached.
|
||||
|
||||
Dev-only paths (`composer.json`, `composer.lock`, `vendor/`, `tests/`, `plans/`, `specs/`, `.github/`, `.gitignore`, `.vscode/`, `.claude/`) are intentionally excluded.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify with grep**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -n "Building a Release" README.md
|
||||
```
|
||||
Expected: one matching line, located after the "Connect Your Gitea Repo" section.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "Document automated release process in README"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Bump version to 1.1.1 for first automated release
|
||||
|
||||
**Files:**
|
||||
- Modify: `projects-portfolio.php` — header `Version:` and `PROJECTS_PORTFOLIO_VERSION` constant.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: in-tree version `1.1.1`, header comment line `* Version: 1.1.1`, constant `define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' );`.
|
||||
|
||||
- [ ] **Step 1: Update the plugin header**
|
||||
|
||||
In `projects-portfolio.php`, in the file header comment block near the top, change:
|
||||
```
|
||||
* Version: 1.1.0
|
||||
```
|
||||
to:
|
||||
```
|
||||
* Version: 1.1.1
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the version constant**
|
||||
|
||||
In `projects-portfolio.php`, find the line:
|
||||
```php
|
||||
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.0' );
|
||||
```
|
||||
Replace with:
|
||||
```php
|
||||
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' );
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the test suite to confirm no regression**
|
||||
|
||||
Run: `php vendor/bin/phpunit`
|
||||
Expected: `OK (24 tests, 45 assertions)` (same as before — no PHP behavior change).
|
||||
|
||||
- [ ] **Step 4: Verify with grep**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -n "Version:" projects-portfolio.php | head -1
|
||||
grep -n "PROJECTS_PORTFOLIO_VERSION" projects-portfolio.php
|
||||
```
|
||||
Expected output:
|
||||
- First grep: a line containing `* Version: 1.1.1`.
|
||||
- Second grep: two lines — the `define(...)` and (possibly) usage sites. The `define` line must show `'1.1.1'`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add projects-portfolio.php
|
||||
git commit -m "Bump version to 1.1.1 for first automated release"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Final verification
|
||||
|
||||
**Files:** none (verification only).
|
||||
|
||||
- [ ] **Step 1: Confirm all three commits landed**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git log --oneline -5
|
||||
```
|
||||
Expected (most recent at top): three commits from this plan are visible:
|
||||
- "Bump version to 1.1.1 for first automated release"
|
||||
- "Document automated release process in README"
|
||||
- "Add GitHub Actions release workflow"
|
||||
|
||||
- [ ] **Step 2: Confirm working tree is clean**
|
||||
|
||||
Run: `git status`
|
||||
Expected: `nothing to commit, working tree clean`.
|
||||
|
||||
- [ ] **Step 3: Run full PHPUnit suite**
|
||||
|
||||
Run: `php vendor/bin/phpunit`
|
||||
Expected: `OK (24 tests, 45 assertions)`.
|
||||
|
||||
- [ ] **Step 4: Confirm workflow file content**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
head -30 .github/workflows/release.yml
|
||||
```
|
||||
Expected: matches the YAML produced in Task 1, with `name: Release`, `on.push.tags: ['v*']`, `permissions.contents: write`, and the `release` job.
|
||||
|
||||
- [ ] **Step 5: Confirm README has the new section**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -c "Building a Release" README.md
|
||||
```
|
||||
Expected: `1`.
|
||||
|
||||
- [ ] **Step 6: Note (do NOT execute): the first automated release**
|
||||
|
||||
The user will tag `v1.1.1` and push the tag themselves, which triggers the workflow. The workflow file is in place; the in-tree version is bumped; the first release will produce a zip with this code. The implementer does NOT push the tag or the branch — that's the user's call (their existing git history is ahead of `origin/main`).
|
||||
|
||||
- [ ] **Step 7: Final commit if anything changed**
|
||||
|
||||
```bash
|
||||
git status
|
||||
# If clean, skip. Otherwise:
|
||||
git add -A
|
||||
git commit -m "Final verification fixes"
|
||||
```
|
||||
@@ -11,7 +11,7 @@
|
||||
* Plugin Name: Projects Portfolio
|
||||
* Description: Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.
|
||||
* Plugin URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio
|
||||
* Version: 1.1.0
|
||||
* Version: 1.1.1
|
||||
* Author: Keith Solomon
|
||||
* Author URI: https://keithsolomon.net
|
||||
* License: GPL-2.0+
|
||||
@@ -34,12 +34,12 @@ $myUpdateChecker = PucFactory::buildUpdateChecker(
|
||||
__FILE__,
|
||||
'projects-portfolio'
|
||||
);
|
||||
|
||||
// Set the branch that contains the stable release.
|
||||
$myUpdateChecker->setBranch( 'main' );
|
||||
// Note: PUC's setBranch() is only available when the host is recognized as a
|
||||
// VCS provider (GitHub, GitLab, Bitbucket). The Gitea URL here falls back to
|
||||
// PUC's plain JSON metadata mode, so we skip the call rather than throw.
|
||||
|
||||
// Current plugin version.
|
||||
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.0' );
|
||||
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.1' );
|
||||
|
||||
// Add the required files.
|
||||
require 'admin/admin-settings.php';
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Helper script for the release workflow (.github/workflows/release.yml).
|
||||
|
||||
Subcommands:
|
||||
|
||||
build-payload [--github] Build the JSON payload for creating a release.
|
||||
Reads TAG, GH_SHA from env.
|
||||
With --github, includes generate_release_notes=True.
|
||||
Writes JSON to stdout.
|
||||
|
||||
extract-id < response.json Print the release id from a release API response.
|
||||
extract-upload-url < response.json Print the upload_url from a GitHub release
|
||||
response (used as the asset upload endpoint template).
|
||||
|
||||
This script exists to avoid bash heredoc-inside-YAML-literal indentation traps.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def build_payload(github: bool) -> int:
|
||||
tag = os.environ.get("TAG", "")
|
||||
sha = os.environ.get("GH_SHA", "unknown")
|
||||
payload = {
|
||||
"tag_name": tag,
|
||||
"name": tag,
|
||||
"body": (
|
||||
"Automated release.\n\n"
|
||||
f"Built from commit {sha}.\n\n"
|
||||
"See the workflow run for the artifact."
|
||||
),
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}
|
||||
if github:
|
||||
payload["generate_release_notes"] = True
|
||||
print(json.dumps(payload))
|
||||
return 0
|
||||
|
||||
|
||||
def extract_field(field: str) -> int:
|
||||
raw = sys.stdin.read()
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"::error::Failed to parse JSON response: {e}", file=sys.stderr)
|
||||
return 1
|
||||
value = data.get(field, "")
|
||||
if not value:
|
||||
print(f"::error::Field {field!r} missing from response: {raw}", file=sys.stderr)
|
||||
return 1
|
||||
print(value)
|
||||
return 0
|
||||
|
||||
|
||||
def usage() -> int:
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
return usage()
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "build-payload":
|
||||
return build_payload(github="--github" in sys.argv)
|
||||
if cmd == "extract-id":
|
||||
return extract_field("id")
|
||||
if cmd == "extract-upload-url":
|
||||
return extract_field("upload_url")
|
||||
if cmd == "extract-tag":
|
||||
return extract_field("tag_name")
|
||||
return usage()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,166 @@
|
||||
# Release Workflow Design
|
||||
|
||||
- **Date:** 2026-08-11
|
||||
- **Plugin:** Projects Portfolio (`projects-wp`)
|
||||
- **Status:** Approved design, awaiting implementation plan
|
||||
|
||||
## 1. Goals
|
||||
|
||||
- A single tag push (`v*`) automatically produces a release-ready zip named `projects-portfolio-v<version>.zip`.
|
||||
- The zip contains exactly the files WordPress needs to install and use the plugin: `projects-portfolio.php` at the root of `projects-portfolio/`, with `admin/`, `assets/`, `includes/`, `languages/`, `templates/` subdirectories plus `README.md` and `LICENSE`.
|
||||
- The same zip is uploaded as a workflow artifact AND attached to a GitHub release matching the tag.
|
||||
- README gets a short "Building a release" section explaining how the workflow runs and what it produces.
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
- Publishing to WordPress.org — out of scope (different credentials, different approval workflow).
|
||||
- Cross-repo publishing or PAT-based auth.
|
||||
- Signing, cosign, or checksums for the zip.
|
||||
- Auto-bumping `PROJECTS_PORTFOLIO_VERSION` or the plugin header before a tag is cut. The workflow assumes the tag is created *after* the version has been bumped in-tree.
|
||||
- Backporting tags. The workflow only acts on the tag push, not on retags of older SHAs.
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
A single GitHub Actions workflow file at `.github/workflows/release.yml` runs on every `v*` tag push. It performs four sequential steps inside a single job:
|
||||
|
||||
1. **Checkout** — `actions/checkout@v4` at the tag's commit.
|
||||
2. **Build zip** — bash script copies allowlisted runtime files into a staging directory `dist/staging/projects-portfolio/`, then runs `zip -r` from the staging root to produce `dist/projects-portfolio-v<version>.zip`.
|
||||
3. **Upload artifact** — `actions/upload-artifact@v4` with name `projects-portfolio-v<tag>` and path `dist/projects-portfolio-*.zip`. Fails if no file matches.
|
||||
4. **Create release** — `softprops/action-gh-release@v2` with the tag, generated release notes, and the zip file. Replaces the asset if a release with the same tag already exists (default `action-gh-release@v2` behavior).
|
||||
|
||||
The workflow uses the runner-provided `GITHUB_TOKEN` with `contents: write` permission. No additional secrets are needed.
|
||||
|
||||
### 3.1 Why a staging directory
|
||||
|
||||
WordPress's "Upload Plugin" zip-install expects `<plugin-slug>/<plugin-file>.php` at the zip root. A direct `zip -r` of the working tree would put everything at the zip root — no enclosing folder — which would fail to install. The staging directory ensures the zip's internal layout has `projects-portfolio/` at the top with `projects-portfolio.php` inside it.
|
||||
|
||||
### 3.2 Files added
|
||||
|
||||
- `.github/workflows/release.yml` — the workflow.
|
||||
- `README.md` — modified, with a new "Building a release" section after the existing "Connect Your Gitea Repo" section.
|
||||
|
||||
No PHP files are touched. The PHPUnit suite is unaffected (no test bootstrap or production code changes).
|
||||
|
||||
## 4. Trigger & permissions
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
```
|
||||
|
||||
`workflow_dispatch` is intentionally omitted. To re-run for an existing tag, use the GitHub UI's "Re-run all jobs" button.
|
||||
|
||||
## 5. Zip filename
|
||||
|
||||
`projects-portfolio-v<version>.zip`, where `<version>` is the tag name with the leading `v` stripped. Examples:
|
||||
|
||||
- Tag `v1.1.0` → `projects-portfolio-v1.1.0.zip`
|
||||
- Tag `v2.0.0-rc1` → `projects-portfolio-v2.0.0-rc1.zip`
|
||||
|
||||
## 6. Allowlisted zip contents
|
||||
|
||||
The build step copies these paths from the working tree into `dist/staging/projects-portfolio/`:
|
||||
|
||||
```
|
||||
projects-portfolio.php
|
||||
README.md
|
||||
LICENSE
|
||||
admin/
|
||||
assets/
|
||||
includes/
|
||||
languages/
|
||||
templates/
|
||||
```
|
||||
|
||||
The bundled `includes/plugin-update-checker/` library IS included — it's a runtime dependency used by `projects-portfolio.php`.
|
||||
|
||||
## 7. Excluded paths
|
||||
|
||||
The build step does **not** copy any of these. Most are already not tracked in git; the explicit list prevents accidental inclusion if any are added later:
|
||||
|
||||
- `composer.json`, `composer.lock` — dev-only.
|
||||
- `vendor/` — Composer dev dependencies.
|
||||
- `tests/`, `phpunit.xml`, `phpunit.xml.dist`, `.phpunit.result.cache` — test infrastructure.
|
||||
- `plans/`, `specs/` — design artifacts (not tracked, but listed for safety).
|
||||
- `.github/` — the workflow file itself (would otherwise be included by the catch-all `cp -r`).
|
||||
- `.gitignore`, `.vscode/`, `.claude/` — editor / project config (not tracked except `.gitignore`).
|
||||
|
||||
If the build script can't find a tracked source path it expects (e.g. `projects-portfolio.php` renamed), `set -e` fails the step.
|
||||
|
||||
## 8. Build step (verbatim)
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
STAGE="dist/staging"
|
||||
OUT="dist/projects-portfolio-v${VERSION}.zip"
|
||||
|
||||
rm -rf dist
|
||||
mkdir -p "$STAGE/projects-portfolio"
|
||||
|
||||
cp projects-portfolio.php "$STAGE/projects-portfolio/"
|
||||
cp README.md "$STAGE/projects-portfolio/"
|
||||
cp LICENSE "$STAGE/projects-portfolio/"
|
||||
cp -r admin/ "$STAGE/projects-portfolio/"
|
||||
cp -r assets/ "$STAGE/projects-portfolio/"
|
||||
cp -r includes/ "$STAGE/projects-portfolio/"
|
||||
cp -r languages/ "$STAGE/projects-portfolio/"
|
||||
cp -r templates/ "$STAGE/projects-portfolio/"
|
||||
|
||||
( cd "$STAGE" && zip -r "../../$OUT" projects-portfolio )
|
||||
|
||||
echo "Built $OUT"
|
||||
unzip -l "$OUT"
|
||||
```
|
||||
|
||||
`unzip -l` prints the table of contents to the action log so the run is self-documenting.
|
||||
|
||||
## 9. README addition
|
||||
|
||||
Add a "Building a Release" section after the existing "Connect Your Gitea Repo" section:
|
||||
|
||||
```markdown
|
||||
## Building a Release
|
||||
|
||||
Releases are automated via GitHub Actions. To cut a new release:
|
||||
|
||||
1. Bump `PROJECTS_PORTFOLIO_VERSION` and the plugin header `Version:` in `projects-portfolio.php`.
|
||||
2. Commit and push to `main`.
|
||||
3. Tag the release commit: `git tag -a v1.1.0 -m "v1.1.0 — short summary"`.
|
||||
4. Push the tag: `git push origin v1.1.0`.
|
||||
|
||||
The `.github/workflows/release.yml` workflow runs and:
|
||||
|
||||
- Builds `projects-portfolio-v<version>.zip` containing only the runtime files (`projects-portfolio.php`, `admin/`, `assets/`, `includes/`, `languages/`, `templates/`, `README.md`, `LICENSE`).
|
||||
- Uploads the zip as a workflow artifact.
|
||||
- Creates (or updates) the matching GitHub Release with the zip attached.
|
||||
|
||||
Dev-only paths (`composer.json`, `composer.lock`, `vendor/`, `tests/`, `plans/`, `specs/`, `.github/`, `.gitignore`, `.vscode/`, `.claude/`) are intentionally excluded.
|
||||
```
|
||||
|
||||
## 10. Error handling
|
||||
|
||||
| Failure | Behavior |
|
||||
|---|---|
|
||||
| Tag doesn't match `v*` | Workflow doesn't run. |
|
||||
| `cp -r` finds a missing source path | `set -e` fails the step; no zip, no release. |
|
||||
| `zip` command fails | `set -e` fails the step; no artifact upload, no release. |
|
||||
| Artifact upload finds no file | `if-no-files-found: error` fails the step. |
|
||||
| Release already exists for tag | `softprops/action-gh-release@v2` replaces the asset and updates notes (default behavior). |
|
||||
| Missing `contents: write` permission | Job fails at checkout or release step with a clear permissions error. |
|
||||
|
||||
## 11. Rollout
|
||||
|
||||
1. Implement on a feature branch.
|
||||
2. Merge into `main` (the workflow file is tracked and applies to all future tags).
|
||||
3. To produce the first automated release: bump `PROJECTS_PORTFOLIO_VERSION` and the plugin header from `1.1.0` to `1.1.1`, commit, tag `v1.1.1`, push the tag.
|
||||
4. Verify on the GitHub Actions run page that: artifact uploads, zip contents look right (via the `unzip -l` log line), GitHub release is created with the zip attached.
|
||||
|
||||
## 12. New translatable strings
|
||||
|
||||
None. The workflow file and README change introduce no user-facing strings.
|
||||
Reference in New Issue
Block a user