├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── index.php └── wp-content │ ├── mu-plugins │ └── _core-disallow-updates.php │ └── maintenance.php ├── UPGRADE.md ├── .github ├── dependabot.yml └── workflows │ ├── workflow.yml │ ├── shell-script.yml │ ├── integrity.yml │ ├── deploy.yml │ ├── spelling.yml │ ├── reusable-deployment.yml │ └── back-end.yml ├── .editorconfig ├── .gitignore ├── wp-cli.yml ├── LICENSE ├── move-to-subdirectory.sh ├── MIGRATE.md ├── composer.json ├── README.md ├── INSTALL.md └── deploy-receiver.sh /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szepeviktor/composer-managed-wordpress/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szepeviktor/composer-managed-wordpress/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szepeviktor/composer-managed-wordpress/HEAD/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | false, 16 | 'install_themes' => false, 17 | // 'switch_themes' => false, 18 | 'delete_plugins' => false, 19 | 'delete_themes' => false, 20 | 'update_core' => false, 21 | 'update_plugins' => false, 22 | 'update_themes' => false, 23 | 'update_languages' => false, 24 | 'install_languages' => false, 25 | ] 26 | ); 27 | }, 28 | PHP_INT_MAX, 29 | 1 30 | ); 31 | -------------------------------------------------------------------------------- /.github/workflows/integrity.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | 3 | name: "Integrity" 4 | # This workflow prevents earthquakes. 5 | 6 | on: # yamllint disable-line rule:truthy 7 | pull_request: null 8 | push: 9 | branches: 10 | - "master" 11 | 12 | permissions: {} # yamllint disable-line rule:braces 13 | 14 | concurrency: 15 | group: "${{ github.workflow }}-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | call_workflow_integrity: 20 | name: "Integrity" 21 | uses: "szepeviktor/byte-level-care/.github/workflows/reusable-integrity.yml@master" 22 | with: 23 | executables: >- 24 | deploy-receiver.sh 25 | not-printable-ascii-paths: >- 26 | public/ 27 | export-excludes: >- 28 | --exclude="*.*" 29 | --exclude="public" --exclude="public/*" 30 | exported-paths: >- 31 | LICENSE 32 | -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | # Global parameters 2 | path: "public/project" 3 | disabled_commands: 4 | - "core download" 5 | - "core install" 6 | - "core multisite-convert" 7 | - "core multisite-install" 8 | - "core update" 9 | - "db clean" 10 | - "db create" 11 | - "db drop" 12 | - "db reset" 13 | - "i18n make-json" 14 | - "i18n make-pot" 15 | - "plugin delete" 16 | - "plugin install" 17 | - "plugin uninstall" 18 | - "plugin update" 19 | - "scaffold block" 20 | - "scaffold child-theme" 21 | - "scaffold plugin" 22 | - "scaffold plugin-tests" 23 | - "scaffold post-type" 24 | - "scaffold taxonomy" 25 | - "scaffold theme-tests" 26 | - "scaffold underscores" 27 | - "theme delete" 28 | - "theme install" 29 | - "theme update" 30 | skip-plugins: 31 | - "whats-running" 32 | #exec: | 33 | # global $locale; $locale='en_US'; 34 | 35 | # Commands 36 | apache_modules: 37 | - "mod_rewrite" 38 | "plugin verify-checksums": 39 | exclude: "advanced-custom-fields-pro,wordpress-seo-premium" 40 | server: 41 | docroot: "public" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Viktor Szépe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | 3 | name: "Deploy" 4 | run-name: "Deploy to ${{ inputs.target_env }}" 5 | 6 | # Set up GitHub Environments with "Selected branches" limit and environment URL as a variable 7 | # https://github.com/ORG/REPO/settings/environments 8 | 9 | on: # yamllint disable-line rule:truthy 10 | workflow_dispatch: 11 | inputs: 12 | target_env: 13 | description: "Environment to deploy to" 14 | type: "environment" 15 | required: true 16 | db_wipe: 17 | description: "Wipe database? (REBOOT/No)" 18 | type: "string" 19 | required: false 20 | default: "No" 21 | 22 | permissions: {} # yamllint disable-line rule:braces 23 | 24 | concurrency: 25 | group: "${{ github.workflow }}-${{ github.ref }}-deploy-${{ inputs.target_env }}" 26 | cancel-in-progress: false 27 | 28 | jobs: 29 | call_deployment: 30 | name: "Deployment" 31 | uses: "./.github/workflows/reusable-deployment.yml" 32 | with: 33 | target_env: "${{ inputs.target_env }}" 34 | db_wipe: "${{ inputs.db_wipe }}" 35 | secrets: "inherit" 36 | -------------------------------------------------------------------------------- /move-to-subdirectory.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Moving a WordPress installation to a subdirectory. 3 | # 4 | 5 | # shellcheck disable=SC2317,SC2148 6 | exit 0 7 | 8 | SUBDIR="project" 9 | URL="$(wp option get home)" 10 | 11 | # Change "siteurl" 12 | wp option update siteurl "${URL}/${SUBDIR}" 13 | 14 | # Change URL in database 15 | wp search-replace --precise --recurse-objects --all-tables-with-prefix "/wp-includes/" "/${SUBDIR}/wp-includes/" 16 | 17 | # Change constants in wp-config.php 18 | # - WP_CONTENT_DIR 19 | # - WP_CONTENT_URL 20 | # - TINY_CDN_INCLUDES_URL 21 | # - TINY_CDN_CONTENT_URL 22 | editor wp-config.php 23 | 24 | # Move core to subdir 25 | xargs -I % mv -v "./%" "./${SUBDIR}/" <<"EOF" 26 | wp-admin 27 | wp-includes 28 | licenc.txt 29 | license.txt 30 | olvasdel.html 31 | readme.html 32 | wp-activate.php 33 | wp-blog-header.php 34 | wp-comments-post.php 35 | wp-config-sample.php 36 | wp-cron.php 37 | wp-links-opml.php 38 | wp-load.php 39 | wp-login.php 40 | wp-mail.php 41 | wp-settings.php 42 | wp-signup.php 43 | wp-trackback.php 44 | xmlrpc.php 45 | EOF 46 | cp -v ./index.php "./${SUBDIR}/" 47 | 48 | # Modify /index.php 49 | sed -i -e "s#'/wp-blog-header\\.php'#'/${SUBDIR}/wp-blog-header.php'#" ./index.php 50 | 51 | # Move files from parent directory 52 | mv -v ../wp-config.php ./ 53 | mv -v ../waf4wordpress-http-analyzer.php ./ 54 | 55 | # Edit "path:" in wp-cli.yml 56 | editor ../wp-cli.yml 57 | 58 | # Fix Apache VirtualHost configuration 59 | 60 | # Flush cache 61 | wp cache flush 62 | -------------------------------------------------------------------------------- /MIGRATE.md: -------------------------------------------------------------------------------- 1 | # Migration of WordPress installations 2 | 3 | ## Clone Staging to Production 4 | 5 | Please see [Production-website.md](https://github.com/szepeviktor/debian-server-tools/blob/master/webserver/Production-website.md#migration) 6 | 7 | - Remove development `wp_options`, use WP-CLI 8 | - Delete unused Media files from filesystem and database 9 | - See `humanmade/orphan-command` 10 | - Try WP-Sweep plugin 11 | - Optimize database `wp db optimize` 12 | 13 | ### Changing domain name 14 | 15 | ```shell 16 | wp search-replace --precise --recurse-objects --all-tables-with-prefix "OLD" "NEW" 17 | ``` 18 | 19 | Replace items in this order. 20 | 21 | 1. `https://DOMAIN.TLD` no trailing slash 22 | 1. `http://DOMAIN.TLD` no trailing slash 23 | 1. `/home/PATH/TO/SITE` no trailing slash 24 | 1. `EMAIL@ADDRESS.ES` all addresses 25 | 1. `DOMAIN.TLD` now only the domain name 26 | 27 | Flush permalinks and object cache. 28 | 29 | ```shell 30 | wp rewrite flush --hard 31 | wp cache flush 32 | ``` 33 | 34 | And edit constants in `wp-config.php`. 35 | 36 | Web-based search & replace tool. 37 | 38 | ```shell 39 | wget -O srdb.php https://github.com/interconnectit/Search-Replace-DB/raw/master/index.php 40 | wget https://github.com/interconnectit/Search-Replace-DB/raw/master/srdb.class.php 41 | ``` 42 | 43 | ## Moving settings from parent theme to child theme 44 | 45 | ```shell 46 | wp option list --search="theme_mods_*" --fields=option_name 47 | wp option get theme_mods_parent-theme --format=json | wp option update theme_mods_child-theme --format=json 48 | ``` 49 | -------------------------------------------------------------------------------- /.github/workflows/spelling.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | 3 | name: "Spelling" 4 | # I take care of your fat fingers and ESL mistakes. 5 | 6 | on: # yamllint disable-line rule:truthy 7 | pull_request: null 8 | push: 9 | branches: 10 | - "master" 11 | 12 | permissions: {} # yamllint disable-line rule:braces 13 | 14 | concurrency: 15 | group: "${{ github.workflow }}-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | typos_check: 20 | name: "文A Typos" 21 | runs-on: "ubuntu-22.04" 22 | timeout-minutes: 1 23 | steps: 24 | - 25 | name: "Checkout repository" 26 | uses: "actions/checkout@v5.0.1" 27 | - 28 | name: "Search for misspellings" 29 | env: 30 | GH_TOKEN: "${{ github.token }}" 31 | # yamllint disable rule:line-length 32 | run: | 33 | set -o pipefail 34 | mkdir -p "${{ runner.temp }}/typos" 35 | RELEASE_ASSET_URL="$( 36 | gh api /repos/crate-ci/typos/releases/latest \ 37 | --jq '."assets"[] | select(."name" | test("^typos-.+-x86_64-unknown-linux-musl\\.tar\\.gz$")) | ."browser_download_url"' 38 | )" 39 | wget --secure-protocol=TLSv1_3 --max-redirect=1 --retry-on-host-error --retry-connrefused --tries=3 \ 40 | --quiet --output-document=- "${RELEASE_ASSET_URL}" \ 41 | | tar -xz -C "${{ runner.temp }}/typos" ./typos 42 | "${{ runner.temp }}/typos/typos" --version 43 | git grep --files-with-matches --null -I -e '.' \ 44 | | xargs -0 -t -- "${{ runner.temp }}/typos/typos" --format json \ 45 | | jq --raw-output '"::warning file=\(.path),line=\(.line_num),col=\(.byte_offset)::\"\(.typo)\" should be \"" + (.corrections // [] | join("\" or \"") + "\".")' 46 | -------------------------------------------------------------------------------- /.github/workflows/reusable-deployment.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | 3 | name: "Deployment" 4 | 5 | on: # yamllint disable-line rule:truthy 6 | # Reusable 7 | workflow_call: 8 | inputs: 9 | target_env: 10 | type: "string" 11 | required: true 12 | db_wipe: 13 | type: "string" 14 | required: true 15 | secrets: 16 | CD_SSH_USER_AT_HOST: 17 | required: true 18 | CD_SSH_KEY_B64: 19 | required: true 20 | CD_SSH_KNOWN_HOSTS_B64: 21 | required: true 22 | 23 | permissions: {} 24 | 25 | concurrency: 26 | group: "${{ github.workflow }}-${{ github.ref }}-deployment-${{ inputs.target_env }}" 27 | cancel-in-progress: false 28 | 29 | jobs: 30 | deployment: 31 | name: "Deploy from ${{ github.ref_name }} branch to ${{ inputs.target_env }} env" 32 | runs-on: "ubuntu-22.04" 33 | environment: 34 | name: "${{ inputs.target_env }}" 35 | url: "${{ vars.URL }}" 36 | timeout-minutes: 10 37 | steps: 38 | - 39 | name: "Deploy to ${{ inputs.target_env }} environment" 40 | run: | 41 | # Create files and directories 42 | ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519 43 | rm ~/.ssh/id_ed25519.pub 44 | # Our server's public key 45 | echo "${{ secrets.CD_SSH_KNOWN_HOSTS_B64 }}" | base64 --decode >>~/.ssh/known_hosts 46 | # Project user's SSH private key 47 | echo "${{ secrets.CD_SSH_KEY_B64 }}" | base64 --decode >~/.ssh/id_ed25519 48 | # Trigger deployment 49 | echo "${{ github.repository }} ${{ github.ref_name }} ${{ github.sha }} ${{ inputs.db_wipe }}" \ 50 | | ssh -4 -o ConnectionAttempts=3 -o PasswordAuthentication=no -p 22 "${{ secrets.CD_SSH_USER_AT_HOST }}" 51 | - 52 | name: "Clean up secrets" 53 | run: | 54 | rm -f -r ~/.ssh 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "company/composer-managed-wordpress", 3 | "description": "Install WordPress by using Composer packages.", 4 | "license": "MIT", 5 | "require": { 6 | "php": "~8.1.7", 7 | "ext-curl": "*", 8 | "ext-exif": "*", 9 | "ext-gd": "*", 10 | "ext-intl": "*", 11 | "ext-mbstring": "*", 12 | "ext-redis": "*", 13 | "ext-xml": "*", 14 | "ext-zip": "*", 15 | "composer-plugin-api": "^2.2", 16 | "composer/installers": "^2.2", 17 | "johnpbloch/wordpress-core-installer": "^2.0", 18 | "koodimonni/composer-dropin-installer": "^1.4", 19 | "roots/wordpress-no-content": "^6.5", 20 | "szepeviktor/sentencepress": "^0.4", 21 | "szepeviktor/waf4wordpress": "^0.2.0", 22 | "timber/timber": "^2.1", 23 | "wpackagist-plugin/duplicate-post": "^4.5", 24 | "wpackagist-plugin/webp-uploads": "^2.6", 25 | "wpackagist-plugin/wordpress-seo": "^26.3", 26 | "wpackagist-plugin/wp-redis": "^1.4", 27 | "wpengine/advanced-custom-fields-pro": "^6.2" 28 | }, 29 | "require-dev": { 30 | "johnbillion/query-monitor": "^3.17", 31 | "squizlabs/php_codesniffer": "^3.9", 32 | "szepeviktor/phpstan-wordpress": "^2.0" 33 | }, 34 | "repositories": [ 35 | { 36 | "type": "composer", 37 | "url": "https://wpackagist.org", 38 | "only": [ 39 | "wpackagist-plugin/*", 40 | "wpackagist-theme/*" 41 | ] 42 | }, 43 | { 44 | "type": "vcs", 45 | "url": "https://github.com/szepeviktor/starter-plugin.git" 46 | }, 47 | { 48 | "type":"composer", 49 | "url":"https://connect.advancedcustomfields.com" 50 | } 51 | ], 52 | "config": { 53 | "allow-plugins": { 54 | "composer/installers": true, 55 | "johnpbloch/wordpress-core-installer": true, 56 | "koodimonni/composer-dropin-installer": true 57 | }, 58 | "classmap-authoritative": true, 59 | "dropin-installer": "copy", 60 | "preferred-install": { 61 | "*": "dist" 62 | } 63 | }, 64 | "extra": { 65 | "dropin-paths": {}, 66 | "installer-paths": { 67 | "public/wp-content/plugins/{$name}/": [ 68 | "type:wordpress-plugin" 69 | ], 70 | "public/wp-content/themes/{$name}/": [ 71 | "type:wordpress-theme" 72 | ] 73 | }, 74 | "wordpress-install-dir": "public/project" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composer managed WordPress 2 | 3 | You may learn how I install WordPress. 4 | Almost everything will come from Composer packages, the rest is under version control (git). 5 | 6 | Thus the repository of a WordPress installation should barely contain files. 7 | 8 | ## Support my work 9 | 10 | Please consider sponsoring me monthly if you use my packages in an agency. 11 | 12 | [![Sponsor](https://github.com/szepeviktor/.github/raw/master/.github/assets/github-like-sponsor-button.svg)](https://github.com/sponsors/szepeviktor) 13 | 14 | ## Directory structure 15 | 16 | Most of these files are excluded from this repository as they are installed by Composer! 17 | 18 | - `/`: root directory with configuration files and documents 19 | - `vendor/`: dependencies (packages) 20 | - `public/`: webserver's document [root](https://github.com/szepeviktor/RootFiles) with `index.php`, `wp-config.php`, `favicon.ico` 21 | - `public/$PROJECT_NAME/`: WordPress core 22 | - `public/wp-content/`: `wp-content` directory 23 | 24 | ``` 25 | vendor/ 26 | UPGRADE.md 27 | composer.json 28 | composer.lock 29 | wp-cli.yml 30 | public/─┬─index.php (modified) 31 | ├─wp-config.php 32 | ├─PROJECT_NAME/─┬─index.php 33 | │ ├─wp-load.php 34 | │ ├─wp-login.php 35 | │ ├─wp-admin/ 36 | │ └─wp-includes/ 37 | └─wp-content/ 38 | ``` 39 | 40 | ## Package types 41 | 42 | - Themes from WordPress.org's theme directory through wpackagist 43 | - Your [custom theme](https://github.com/szepeviktor/starter-theme) should be developed as a separate package in a repository of its own 44 | - Plugins from WordPress.org's plugin directory through wpackagist 45 | - Your [custom plugins](https://github.com/szepeviktor/starter-plugin) should be developed as separate packages 46 | - Purchased plugins can be installed by `ffraenz/private-composer-installer` 47 | - Must-use plugins and dropins can be installed by `koodimonni/composer-dropin-installer` 48 | 49 | All other files - except `public/wp-config.php` - should be kept under version control. 50 | 51 | ## Usage 52 | 53 | 1. Run [WordPress core](https://github.com/WordPress/wordpress-develop/blob/trunk/.version-support-php.json), 54 | plugins and theme on PHP 8.1 55 | ([as of 2026](https://johnbillion.github.io/wp-stats/php.html)) 56 | 1. Change the directory name "project" in `.gitignore`, `composer.json`, `public/index.php`, `wp-cli.yml` 57 | 1. Customize `composer.json` and create documents 58 | 1. Create `.env` if you have purchased plugins 59 | 1. Add ACF PRO credentials 60 | `composer config --auth http-basic.connect.advancedcustomfields.com "PLUGIN_ACF_PRO_KEY" "https://example.com"` 61 | 1. Add [MU plugins](https://github.com/szepeviktor/wordpress-website-lifecycle/tree/master/mu-plugins) 62 | 1. Set GitHub OAuth token if you develop a private theme or plugins 63 | `composer config github-oauth.github.com "$YOUR_GITHUB_TOKEN"` 64 | 1. Create [`public/wp-config.php`](https://github.com/szepeviktor/wordpress-website-lifecycle/blob/master/wp-config/wp-config.php) 65 | including `WP_CONTENT_DIR` and `WP_CONTENT_URL` pointing to `public/wp-content`, and loading `vendor/autoload.php` 66 | 1. Set `WP_ENVIRONMENT_TYPE` environment variable 67 | (in [PHP-FPM configuration](https://github.com/szepeviktor/debian-server-tools/blob/master/webserver/phpfpm-pools/Skeleton-pool.conf) 68 | or in `public/wp-config.php`) 69 | 1. Issue `composer update --no-dev` 70 | 1. Administer your WordPress installation with [WP-CLI](https://make.wordpress.org/cli/handbook/guides/installing/) 71 | ```bash 72 | wp core install --title="WP" --admin_user="myname" --admin_email="user@example.com" --admin_password="12345" 73 | wp option update home "https://example.com" 74 | wp option update siteurl "https://example.com/project" 75 | ``` 76 | 77 | ## WordPress core installation 78 | 79 | These are possible variations. 80 | 81 | - ⭐ `roots/wordpress-no-content` + `johnpbloch/wordpress-core-installer` 82 | - `johnpbloch/wordpress` 83 | - `repositories.package` with current ZIP file from wordpress.org 84 | - `roots/wordpress` 85 | 86 | Packages provided by Roots point to wordpress.org ZIP files and git repositories. 87 | -------------------------------------------------------------------------------- /public/wp-content/maintenance.php: -------------------------------------------------------------------------------- 1 | In maintenance mode

Will come back online in seconds.

', 10 | // 'Maintenance', 11 | // ['response' => 503] 12 | // ); 13 | 14 | status_header(503); 15 | header('Content-Type: text/html; charset=utf-8'); 16 | nocache_headers(); 17 | 18 | ?> 19 | 20 | 21 | 22 | 23 | Maintenance 24 | 128 | 129 | 130 |

In maintenance mode

131 |

Will come back online in seconds.

132 | 133 | 134 | -------------------------------------------------------------------------------- /.github/workflows/back-end.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | 3 | name: "Back-end" 4 | 5 | on: # yamllint disable-line rule:truthy 6 | pull_request: null 7 | push: 8 | branches: 9 | - "master" 10 | # Add [skip ci] to commit message to skip CI. 11 | 12 | permissions: {} # yamllint disable-line rule:braces 13 | 14 | concurrency: 15 | group: "${{ github.workflow }}-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | syntax_errors: 20 | name: "1️⃣ Syntax errors" 21 | runs-on: "ubuntu-22.04" 22 | timeout-minutes: 5 23 | steps: 24 | - 25 | name: "Set up PHP" 26 | uses: "shivammathur/setup-php@2.35.4" 27 | with: 28 | php-version: "8.1" 29 | coverage: "none" 30 | tools: "parallel-lint" 31 | - 32 | name: "Checkout repository" 33 | uses: "actions/checkout@v5.0.1" 34 | - 35 | name: "Search for invalid complex curly syntax 🐌" 36 | run: | 37 | ! git grep -e '\${[A-Z_a-z]' -- '*.php' '*.scss' 38 | - 39 | name: "Check source code for syntax errors" 40 | run: "composer exec --no-interaction -- parallel-lint public/" 41 | 42 | static_analysis: 43 | name: "3️⃣ Static Analysis" 44 | needs: 45 | - "syntax_errors" 46 | runs-on: "ubuntu-22.04" 47 | timeout-minutes: 5 48 | steps: 49 | - 50 | name: "Set up PHP" 51 | uses: "shivammathur/setup-php@2.35.4" 52 | with: 53 | php-version: "8.1" 54 | coverage: "none" 55 | - 56 | name: "Checkout repository" 57 | uses: "actions/checkout@v5.0.1" 58 | - 59 | name: "Check JSON files" 60 | run: | 61 | git ls-files --cached -z -- '*.json' \ 62 | | xargs -t --null -L 1 -- \ 63 | php -r 'json_decode(file_get_contents($argv[1]), null, 512, JSON_THROW_ON_ERROR);' 64 | - 65 | name: "Validate Composer configuration" 66 | run: "composer validate --no-interaction --strict" 67 | - 68 | name: "Remove ACF PRO" 69 | run: "composer remove --no-interaction --no-update wpengine/advanced-custom-fields-pro" 70 | - 71 | name: "Install dependencies" 72 | uses: "ramsey/composer-install@3.1.0" 73 | with: 74 | dependency-versions: "highest" 75 | - 76 | name: "Check PSR-4 mapping 🐌" 77 | run: "composer dump-autoload --optimize --strict-psr" 78 | - 79 | name: "Check for security vulnerability advisories" 80 | run: "composer audit" 81 | - 82 | # https://github.com/phpstan/phpstan/issues/9475 83 | name: "Search for $this typos 🐌" 84 | run: | 85 | ! git grep --line-number -e '\$this\s*->\s*\$this\|\$\$this' -- ':!:*/back-end.yml' 86 | - 87 | name: "Perform static analysis" 88 | run: | 89 | composer exec -- phpstan analyze \ 90 | --configuration=vendor/szepeviktor/phpstan-wordpress/extension.neon \ 91 | --level=5 \ 92 | public/index.php public/wp-content/mu-plugins/ 93 | 94 | coding_standards: 95 | name: "4️⃣ Coding Standards" 96 | needs: 97 | - "syntax_errors" 98 | runs-on: "ubuntu-22.04" 99 | timeout-minutes: 5 100 | steps: 101 | - 102 | name: "Set up PHP" 103 | uses: "shivammathur/setup-php@2.35.4" 104 | with: 105 | php-version: "8.1" 106 | coverage: "none" 107 | tools: "phpcs,cs2pr" 108 | - 109 | name: "Checkout repository" 110 | uses: "actions/checkout@v5.0.1" 111 | - 112 | name: "Remove ACF PRO" 113 | run: "composer remove --no-interaction --no-update wpengine/advanced-custom-fields-pro" 114 | - 115 | name: "Install dependencies" 116 | uses: "ramsey/composer-install@3.1.0" 117 | with: 118 | dependency-versions: "highest" 119 | - 120 | name: "Detect coding standards violations" 121 | run: "composer exec -- phpcs -q --report=checkstyle --standard=PSR12 public/wp-content/mu-plugins/ | cs2pr" 122 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Remove default content 2 | 3 | ```bash 4 | wp post delete $(wp post list --name="$(wp eval 'echo sanitize_title(_x("hello-world", "Default post slug"));')" --posts_per_page=1 --format=ids) 5 | wp post delete $(wp post list --post_type=page --name="$(wp eval 'echo __("sample-page");')" --posts_per_page=1 --format=ids) 6 | wp comment delete 1 7 | wp option update blogdescription "Install and manage WordPress with Composer" 8 | wp plugin uninstall akismet 9 | wp plugin uninstall hello-dolly 10 | wp theme delete twentytwentyone 11 | wp theme delete twentytwentytwo 12 | wp theme delete twentytwentythree 13 | ``` 14 | 15 | ## Add custom database indexes 16 | 17 | ```sql 18 | ALTER TABLE `wp_posts` ADD fulltext(`post_title`); 19 | ``` 20 | 21 | ## WP-Cron 22 | 23 | 1. Disable web-based cron as it runs on PHP-FPM 24 | 1. Run WP-Cron from a Linux cron job 25 | https://github.com/szepeviktor/debian-server-tools/blob/master/webserver/wp-install/wp-cron-cli.sh 26 | 1. WordPress has no built-in queues (immediate background jobs) 27 | 28 | ## Settings 29 | 30 | - General Settings 31 | - Writing Settings 32 | - Reading Settings 33 | - Media Settings 34 | - Permalink Settings 35 | - WP Mail From 36 | 37 | ## Continuous delivery (CD) 38 | 39 | 1. Developer starts GitHub Actions workflow 40 | 1. GitHub Actions connects to the server through SSH starting `deploy-receiver.sh` 41 | 1. On the server `deploy-receiver.sh` checks out git repository and updates dependencies with Composer 42 | 43 | :bulb: There are many other steps. Please see and edit [`deploy-receiver.sh`](/deploy-receiver.sh). 44 | 45 | - Install Debian packages 46 | ```bash 47 | apt-get install grepcidr jq libpng-dev php8.1-fpm 48 | ``` 49 | - Install [WP-CLI](https://github.com/szepeviktor/debian-server-tools/blob/master/debian-setup/packages/php-wpcli) 50 | - Install [cachetool](https://github.com/szepeviktor/debian-server-tools/blob/master/debian-setup/packages/php-cachetool) 51 | - Configure cachetool in `~/.cachetool.yml` 52 | ```yaml 53 | adapter: "fastcgi" 54 | fastcgi: "/run/php/php8.1-fpm-$USER.sock" 55 | temp_dir: "/home/$USER/website/tmp" 56 | ``` 57 | - Install `php-parallel-lint/php-parallel-lint` globally (on user level) 58 | ```bash 59 | composer global require --update-no-dev php-parallel-lint/php-parallel-lint 60 | ``` 61 | - Generate an SSH deploy key 62 | ```bash 63 | ssh-keygen -t ed25519 64 | ``` 65 | - Add the public part to GitHub Actions Deploy keys (no write access) 66 | - Clone the repository in `/home/$USER/website/code` 67 | ```bash 68 | git clone https://github.com/ORG-NAME/REPOSITORY-NAME.git . 69 | ``` 70 | - Connect manually 71 | ```bash 72 | git fetch origin 73 | ``` 74 | - Add the public part to user's authorized keys: `~/.ssh/authorized_keys` 75 | ``` 76 | restrict,command="/home/$USER/website/deploy-receiver.sh" ssh-ed25519 AAAA... 77 | ``` 78 | - Set up secrets for each GitHub Environment, add branch limits 79 | by matching git branch names to environments, e.g. staging, production 80 | ```ini 81 | CD_SSH_USER_AT_HOST 82 | CD_SSH_KEY_B64 83 | CD_SSH_KNOWN_HOSTS_B64 84 | ``` 85 | This is how to get values. 86 | ```bash 87 | # CD_SSH_USER_AT_HOST 88 | echo $(id --user --name)@$(hostname --fqdn) 89 | # CD_SSH_KEY_B64 90 | cat ~/.ssh/id_ed25519 | base64 -w 0; echo 91 | # CD_SSH_KNOWN_HOSTS_B64 92 | ssh-keyscan -p $PORT $HOST | base64 -w 0; echo 93 | ``` 94 | - Add environment URL as variable: `URL`. 95 | - Create `/home/$USER/website/deploy-data` on the server. 96 | ```ini 97 | PROJECT_PATH=org-name/repository-name 98 | COMMIT_REF_NAME=production 99 | GIT_WORK_TREE=/home/$USER/website/code 100 | ``` 101 | - Optionally add license keys to `/home/$USER/website/code/.env` 102 | - Optionally set up Composer authentication 103 | ```bash 104 | composer config --global github-oauth.github.com $PERSONAL-ACCESS-TOKEN 105 | ``` 106 | - Start your first deployment! 107 | 108 | ## Installing translations 109 | 110 | - [Pedro's Translation Tools](https://github.com/pedro-mendonca/Translation-Tools) 111 | - [Loco Translate](https://localise.biz/wordpress/plugin) 112 | - [Tiny translation cache](https://github.com/szepeviktor/tiny-cache) 113 | 114 | ### From wordpress.org 115 | 116 | ```bash 117 | wp language plugin install plugin-name hu_HU 118 | ``` 119 | 120 | ### From a git repository 121 | 122 | ```bash 123 | apt-get install gettext 124 | man msgfmt 125 | ``` 126 | 127 | ### Exported from translate.wordpress.org 128 | 129 | ```bash 130 | PLUGIN_SLUG="plugin-name" 131 | TWPORG_URL="https://translate.wordpress.org/projects/wp-plugins/${PLUGIN_SLUG}/stable/hu/default/export-translations/?format=mo" 132 | wget -O wp-content/languages/plugins/${PLUGIN_SLUG}-hu_HU.mo "${TWPORG_URL}" 133 | wp language plugin is-installed "${PLUGIN_SLUG}" hu_HU 134 | ``` 135 | 136 | ## List tag-category collisions 137 | 138 | ```bash 139 | { wp term list post_tag --field=slug; wp term list category --field=slug; } | sort | uniq -d 140 | ``` 141 | -------------------------------------------------------------------------------- /deploy-receiver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Automatic deployment. 4 | # 5 | # VERSION :0.6.1 6 | # DOCS :https://github.com/szepeviktor/composer-managed-wordpress 7 | # DEPENDS :apt-get install grepcidr jq libpng-dev php8.1-fpm 8 | # DEPENDS2 :php-wpcli php-cachetool 9 | # SECRET :CD_SSH_KNOWN_HOSTS_B64 10 | # SECRET :CD_SSH_KEY_B64 11 | # SECRET :CD_SSH_USER_AT_HOST 12 | # CONFIG-VAR :PROJECT_PATH 13 | # CONFIG-VAR :GIT_WORK_TREE 14 | # shellcheck disable=SC2317 15 | 16 | DEPLOY_CONFIG_NAME="deploy-data" 17 | 18 | Check_github_ci_ip() 19 | { 20 | local IP="$1" 21 | 22 | echo "Connecting IP check: ${IP}" 23 | 24 | NETWORKS="$(wget -q -O- https://api.github.com/meta | jq -r '."actions"[]')" 25 | 26 | if ! grepcidr "${NETWORKS}" <<<"${IP}" >/dev/null; then 27 | echo "Unknown IP tried to deploy: https://bgp.he.net/ip/${IP}" \ 28 | | s-nail -s "[deploy] ${FUNCNAME[0]}() error" admin@szepe.net 29 | echo "Unknown IP tried to deploy: ${IP}" 1>&2 30 | exit 1 31 | fi 32 | } 33 | 34 | Check_name() 35 | { 36 | [[ "$1" =~ ^[0-9a-zA-Z_/-]+$ ]] 37 | } 38 | 39 | Check_hash() 40 | { 41 | [[ "$1" =~ ^[0-9a-f]{40}$ ]] 42 | } 43 | 44 | # shellcheck disable=SC2329 45 | Onexit() 46 | { 47 | local -i RET="$1" 48 | local BASH_CMD="$2" 49 | 50 | set +e 51 | 52 | if [ "${RET}" -ne 0 ]; then 53 | echo "COMMAND WITH ERROR: ${BASH_CMD}" 1>&2 54 | fi 55 | 56 | exit "${RET}" 57 | } 58 | 59 | Get_config() 60 | { 61 | # Configuration file containing PROJECT_PATH, GIT_WORK_TREE 62 | DEPLOY_CONFIG_PATH="$(dirname "$0")/${DEPLOY_CONFIG_NAME}" 63 | if [ ! -r "${DEPLOY_CONFIG_PATH}" ]; then 64 | echo "[ERROR] Unconfigured" 1>&2 65 | exit 1 66 | fi 67 | 68 | # Global 69 | # shellcheck disable=SC1090 70 | source "${DEPLOY_CONFIG_PATH}" 71 | } 72 | 73 | Check_config() 74 | { 75 | # Check deploy configuration 76 | if ! Check_name "${PROJECT_PATH}"; then 77 | echo "[ERROR] Project path not configured correctly: (${PROJECT_PATH})" 1>&2 78 | exit 10 79 | fi 80 | 81 | if [ ! -e "${GIT_WORK_TREE}/.git" ]; then 82 | echo "[ERROR] Git work tree is not available: (${GIT_WORK_TREE})" 1>&2 83 | exit 12 84 | fi 85 | } 86 | 87 | Receive_commit() 88 | { 89 | # Globals 90 | read -r CI_PROJECT_PATH CI_COMMIT_REF_NAME CI_COMMIT_SHA CI_DB_WIPE 91 | printf '%-27s"%s#%s@%s"\n' "Received:" "${CI_PROJECT_PATH}" "${CI_COMMIT_REF_NAME}" "${CI_COMMIT_SHA}" 92 | 93 | # Check commit data 94 | if [ "${CI_PROJECT_PATH}" != "${PROJECT_PATH}" ]; then 95 | echo "[ERROR] Invalid repository: (${CI_PROJECT_PATH})" 1>&2 96 | exit 20 97 | fi 98 | printf '%-28s%s#%s\n' "Project path + branch OK:" "${CI_PROJECT_PATH}" "${CI_COMMIT_REF_NAME}" 99 | 100 | if ! Check_hash "${CI_COMMIT_SHA}"; then 101 | echo "[ERROR] Invalid commit hash: (${CI_COMMIT_SHA})" 1>&2 102 | exit 21 103 | fi 104 | printf '%-28s%s\n' "Commit hash OK:" "${CI_COMMIT_SHA}" 105 | 106 | printf '%-28s%s\n' "Database wipe:" "${CI_DB_WIPE}" 107 | } 108 | 109 | Deploy() 110 | { 111 | echo "Starting deployment ..." 112 | 113 | # Locked for singleton execution 114 | { 115 | flock 9 116 | 117 | if ! Check_hash "${CI_COMMIT_SHA}"; then 118 | echo "[ERROR] Invalid commit hash: (${CI_COMMIT_SHA})" 1>&2 119 | exit 30 120 | fi 121 | 122 | # Check write permission 123 | if [ ! -w "${GIT_WORK_TREE}" ]; then 124 | echo "[ERROR] Cannot write to work tree" 1>&2 125 | stat "${GIT_WORK_TREE}" 1>&2 126 | exit 31 127 | fi 128 | 129 | # Check .env file 130 | if [ ! -r "${GIT_WORK_TREE}/.env" ]; then 131 | echo "[ERROR] Cannot read .env file" 1>&2 132 | stat "${GIT_WORK_TREE}/.env" 1>&2 133 | exit 32 134 | fi 135 | 136 | cd "${GIT_WORK_TREE}" 137 | 138 | echo "Remotes:" 139 | git remote -v 140 | echo "Branches:" 141 | git branch -r 142 | git branch 143 | 144 | echo "Fetching origin ..." 145 | git remote show origin >/dev/null 146 | timeout 30 git fetch --prune origin 147 | 148 | ## test "$(git remote get-url origin)" == "${GIT_URL}" 149 | ## test "$(git rev-parse --abbrev-ref HEAD)" == branch-name 150 | 151 | # Down! 152 | if [ -f "$(wp cli param-dump --with-values | jq -r '."path"."current" + "/wp-includes/version.php"')" ]; then 153 | # Check special pages 154 | test "$(wp post list --format=count --post_type=page --name=esztetika)" -eq 1 155 | test "$(wp post list --format=count --post_type=page --name=plasztika)" -eq 1 156 | test "$(wp post list --format=count --post_type=page --name=gyogyaszat)" -eq 1 157 | wp maintenance-mode activate 158 | # Clear caches 159 | wp cache flush 160 | fi 161 | 162 | echo "Checking out work tree..." 163 | git -c advice.detachedHead=false checkout --force "${CI_COMMIT_SHA}" 164 | 165 | # Check Composer configuration - works only with composer.lock file committed 166 | composer validate --no-interaction --strict 167 | #composer validate --no-interaction --strict --no-check-lock 168 | 169 | # Update everything 170 | #timeout 60 composer update --no-interaction --no-progress --no-dev 171 | # Update only the theme 172 | timeout 60 composer update --no-interaction --no-progress --no-dev org-name/repository-name 173 | 174 | # PHP syntax check 175 | composer global exec --no-interaction -- parallel-lint --exclude vendor . 176 | 177 | # Verify WordPress installation 178 | wp core verify-checksums 179 | wp plugin verify-checksums --all --strict 180 | 181 | # Check required core version of plugins 182 | # shellcheck disable=SC2016 183 | wp eval ' 184 | foreach (get_option("active_plugins") as $plugin) { 185 | if ( 186 | version_compare( 187 | get_plugin_data(WP_PLUGIN_DIR."/".$plugin)["RequiresWP"], 188 | get_bloginfo("version"), 189 | ">" 190 | ) 191 | ) { 192 | error_log("Incompatible plugin version:".$plugin); 193 | exit(33); 194 | } 195 | } 196 | ' 197 | 198 | # Update database 199 | wp core update-db 200 | 201 | # Update translations 202 | wp language core update 203 | wp language plugin update --all 204 | 205 | # Check object cache type 206 | test "$(wp cache type)" == Redis 207 | 208 | # Verify critical options 209 | test "$(wp option get users_can_register)" == 0 210 | test "$(wp option get admin_email)" == admin@szepe.net 211 | test "$(wp option get blog_charset)" == UTF-8 212 | 213 | # Custom checks 214 | #test "$(wp eval 'echo perflab_get_module_settings()["images/webp-uploads"]["enabled"];')" == 1 215 | 216 | # Search for ACF Options Page options with default name prefix 217 | test -z "$(wp option list --search="options_*" --field=option_name)" 218 | test -z "$(wp option list --search="_options_*" --field=option_name)" 219 | 220 | # Display theme version 221 | echo -n "Theme package version: " 222 | composer show --no-interaction --format=json org-name/repository-name | jq -r '."versions"[0]' 223 | echo -n "Theme version: " 224 | wp eval 'var_dump(wp_get_theme()->get("Version"));' 225 | echo -n "Theme version constant: " 226 | wp eval 'var_dump(\Company\ThemeName\Theme::VERSION);' 227 | 228 | # Trigger theme setup 229 | #wp eval '$theme = wp_get_theme("our-theme"); do_action("after_switch_theme", $theme->get("Name"), $theme);' 230 | # Fire "deploy" hook 231 | wp eval 'do_action("deploy");' 232 | 233 | # Reset OPcache 234 | cachetool opcache:reset 235 | 236 | # Build theme front-end assets - with theme/ subdirectory 237 | npm --prefix="$(wp eval 'echo dirname(get_template_directory());')" ci --omit=dev 238 | npm --prefix="$(wp eval 'echo dirname(get_template_directory());')" run production 239 | 240 | # Remove references to source maps 241 | grep -rlZ -- '^//# sourceMappingURL=' "$(wp eval 'echo dirname(get_template_directory());')" \ 242 | | xargs -0 -r -L1 -- sed -i -e '/^\/\/# sourceMappingURL=\S\+\.map$/d' 243 | 244 | # UP! 245 | # .maintenance file is removed during WordPress core update 246 | if [ -z "$(find "$(wp cli param-dump --with-values | jq -r '."path"."current" + "/wp-includes/version.php"')" -cmin -10)" ]; then 247 | wp maintenance-mode deactivate 248 | fi 249 | 250 | wp eval 'echo admin_url(), PHP_EOL;' 251 | } 9<"$0" 252 | } 253 | 254 | set -e 255 | 256 | trap 'Onexit "$?" "${BASH_COMMAND}"' EXIT HUP INT QUIT PIPE TERM 257 | 258 | logger -t "Deploy-receiver" "Started from ${SSH_CLIENT%% *}" 259 | Check_github_ci_ip "${SSH_CLIENT%% *}" 260 | 261 | Get_config 262 | Check_config 263 | 264 | Receive_commit 265 | Deploy 266 | 267 | # Send email notification 268 | echo "All is well: https://github.com/${CI_PROJECT_PATH}/commit/${CI_COMMIT_SHA}" \ 269 | | s-nail -s "[${CI_PROJECT_PATH}] Deployment complete" admin@szepe.net 270 | 271 | echo "OK." 272 | exit 0 273 | --------------------------------------------------------------------------------