Skip to content

Commit 0e9a69f

Browse files
ilicfilipclaude
andcommitted
Sync known-plugins.json to wp.org SVN assets daily
Daily GitHub Action validates known-plugins.json and publishes it to the plugin's wp.org /assets/ directory so installs can fetch updates without waiting for a release. Validator rejects malformed JSON, missing fields, and dangerously generic prefixes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f4f89fa commit 0e9a69f

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Sync known-plugins.json to wp.org SVN
2+
3+
on:
4+
schedule:
5+
- cron: '0 3 * * *'
6+
push:
7+
branches:
8+
- develop
9+
- main
10+
paths:
11+
- 'known-plugins/known-plugins.json'
12+
workflow_dispatch:
13+
14+
jobs:
15+
sync:
16+
name: Publish known-plugins.json to wp.org assets
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up PHP
23+
uses: shivammathur/setup-php@v2
24+
with:
25+
php-version: '8.2'
26+
coverage: none
27+
28+
- name: Validate known-plugins.json
29+
run: php bin/validate-known-plugins.php
30+
31+
- name: Install Subversion
32+
run: sudo apt-get update && sudo apt-get install -y subversion
33+
34+
- name: Sparse-checkout SVN assets
35+
env:
36+
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
37+
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
38+
run: |
39+
svn checkout \
40+
--depth immediates \
41+
--username "$SVN_USERNAME" \
42+
--password "$SVN_PASSWORD" \
43+
--non-interactive \
44+
--no-auth-cache \
45+
https://plugins.svn.wordpress.org/aaa-option-optimizer/ svn-repo
46+
svn update --set-depth infinity svn-repo/assets || svn mkdir --parents svn-repo/assets
47+
48+
- name: Copy JSON into assets and detect changes
49+
id: diff
50+
run: |
51+
mkdir -p svn-repo/assets
52+
cp known-plugins/known-plugins.json svn-repo/assets/known-plugins.json
53+
cd svn-repo
54+
# Add the file if it's new
55+
if ! svn info assets/known-plugins.json >/dev/null 2>&1; then
56+
svn add assets/known-plugins.json
57+
fi
58+
# Capture status; empty means nothing to commit
59+
STATUS=$(svn status assets/known-plugins.json)
60+
echo "svn-status: '$STATUS'"
61+
if [ -z "$STATUS" ]; then
62+
echo "changed=false" >> "$GITHUB_OUTPUT"
63+
else
64+
echo "changed=true" >> "$GITHUB_OUTPUT"
65+
fi
66+
67+
- name: Commit to SVN
68+
if: steps.diff.outputs.changed == 'true'
69+
env:
70+
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
71+
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
72+
run: |
73+
cd svn-repo
74+
svn commit assets/known-plugins.json \
75+
--username "$SVN_USERNAME" \
76+
--password "$SVN_PASSWORD" \
77+
--non-interactive \
78+
--no-auth-cache \
79+
-m "Update assets/known-plugins.json"
80+
81+
- name: No changes
82+
if: steps.diff.outputs.changed != 'true'
83+
run: echo "known-plugins.json on SVN is already up to date."

bin/validate-known-plugins.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
/**
3+
* Validate known-plugins/known-plugins.json before publishing.
4+
*
5+
* Exits non-zero on any problem so CI can fail.
6+
*
7+
* @package Progress_Planner\OptionOptimizer
8+
*/
9+
10+
$file = dirname( __DIR__ ) . '/known-plugins/known-plugins.json';
11+
12+
if ( ! file_exists( $file ) ) {
13+
fwrite( STDERR, "Missing file: {$file}\n" );
14+
exit( 1 );
15+
}
16+
17+
$raw = file_get_contents( $file );
18+
if ( false === $raw || '' === trim( $raw ) ) {
19+
fwrite( STDERR, "Empty file: {$file}\n" );
20+
exit( 1 );
21+
}
22+
23+
$data = json_decode( $raw, true );
24+
if ( JSON_ERROR_NONE !== json_last_error() ) {
25+
fwrite( STDERR, 'Invalid JSON: ' . json_last_error_msg() . "\n" );
26+
exit( 1 );
27+
}
28+
29+
if ( ! is_array( $data ) || empty( $data ) ) {
30+
fwrite( STDERR, "Top-level JSON must be a non-empty object.\n" );
31+
exit( 1 );
32+
}
33+
34+
$errors = [];
35+
$warnings = [];
36+
$seen_prefixes = [];
37+
38+
foreach ( $data as $slug => $entry ) {
39+
if ( ! is_string( $slug ) || '' === $slug ) {
40+
$errors[] = 'Slug must be a non-empty string.';
41+
continue;
42+
}
43+
44+
if ( ! is_array( $entry ) ) {
45+
$errors[] = "Entry for '{$slug}' must be an object.";
46+
continue;
47+
}
48+
49+
if ( empty( $entry['name'] ) || ! is_string( $entry['name'] ) ) {
50+
$errors[] = "Entry '{$slug}' missing 'name' string.";
51+
}
52+
53+
if ( empty( $entry['option_prefixes'] ) || ! is_array( $entry['option_prefixes'] ) ) {
54+
$errors[] = "Entry '{$slug}' missing non-empty 'option_prefixes' array.";
55+
continue;
56+
}
57+
58+
foreach ( $entry['option_prefixes'] as $prefix ) {
59+
if ( ! is_string( $prefix ) || '' === $prefix ) {
60+
$errors[] = "Entry '{$slug}' has empty/non-string prefix.";
61+
continue;
62+
}
63+
64+
// Reject dangerously generic prefixes that would match thousands of options.
65+
if ( strlen( $prefix ) < 3 ) {
66+
$errors[] = "Entry '{$slug}' prefix '{$prefix}' is too short (<3 chars).";
67+
}
68+
69+
if ( in_array( $prefix, [ 'wp_', 'option_', '_transient_' ], true ) ) {
70+
$errors[] = "Entry '{$slug}' prefix '{$prefix}' is reserved/dangerous.";
71+
}
72+
73+
if ( isset( $seen_prefixes[ $prefix ] ) && $seen_prefixes[ $prefix ] !== $slug ) {
74+
$warnings[] = "Prefix '{$prefix}' claimed by both '{$seen_prefixes[ $prefix ]}' and '{$slug}'.";
75+
}
76+
$seen_prefixes[ $prefix ] = $slug;
77+
}
78+
}
79+
80+
foreach ( $warnings as $warn ) {
81+
fwrite( STDERR, "WARNING: {$warn}\n" );
82+
}
83+
84+
if ( ! empty( $errors ) ) {
85+
fwrite( STDERR, "Validation failed:\n" );
86+
foreach ( $errors as $err ) {
87+
fwrite( STDERR, " - {$err}\n" );
88+
}
89+
exit( 1 );
90+
}
91+
92+
$count = count( $data );
93+
echo "OK: {$count} entries validated.\n";
94+
exit( 0 );

0 commit comments

Comments
 (0)