Skip to content

Commit a0ad0e0

Browse files
authored
ENG-5451: Ship a Pressable DDEV provider add-on (ddev pull/push for local WordPress dev)
Client-side DDEV provider that syncs a Pressable WordPress site's database and uploads to/from a local DDEV project over the site's existing SSH + WP-CLI + rsync access (no Pressable API, no platform changes). - providers/pressable.yaml — auth + pull (db export | gzip; rsync uploads) and push (capture remote URL, wp db import, wp search-replace back for siteurl+home, wp cache flush, rsync uploads; additive files). - config.pressable.yaml — post-import-db hook rewrites prod URL -> local DDEV URL. - install.yaml — add-on manifest (provider + config overlay, no service). - tests/ + .github/workflows/tests.yml — install/structure bats smoke test (stable + HEAD). - .github/workflows/release{,-on-merge}.yml + .github/scripts/release.rb — tag-driven and merge-driven release automation (Ruby version helper). - README.md, .coderabbit.yaml, Apache 2.0 LICENSE. Verified live end-to-end (pull + push, common + split-config) against a real Pressable test site; CodeRabbit + Codex review addressed.
1 parent 4bc1c0d commit a0ad0e0

14 files changed

Lines changed: 1003 additions & 1 deletion

File tree

.coderabbit.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2+
language: en-US
3+
early_access: false
4+
reviews:
5+
profile: chill
6+
request_changes_workflow: false
7+
high_level_summary: true
8+
review_status: true
9+
poem: false
10+
auto_review:
11+
enabled: true
12+
drafts: false
13+
path_instructions:
14+
- path: "providers/pressable.yaml"
15+
instructions: |
16+
This is a DDEV provider definition. Each *_command block is Bash that
17+
runs inside DDEV's web container during `ddev pull` / `ddev push`.
18+
Review for: correct SSH + WP-CLI + rsync usage; serialized-data-safe URL
19+
rewriting via `wp search-replace` (never sed); and push safety (files
20+
push is additive with no --delete, the DB push overwrites the remote and
21+
is staging-only, and the post-import `wp cache flush` is required because
22+
a raw `wp db import` bypasses the hosting platform's object cache). `${SSH}` and
23+
`${EXPORT_ARGS}` are intentionally unquoted so they word-split into a
24+
command plus its arguments — do not flag that as SC2086.
25+
- path: "config.pressable.yaml"
26+
instructions: |
27+
DDEV config overlay merged into the project config. The post-import-db
28+
hook rewrites the production URL to the local DDEV URL after a pull, via
29+
`wp search-replace`. Review the hook's correctness and idempotency.
30+
- path: ".github/scripts/release.rb"
31+
instructions: |
32+
A plain Ruby release helper. There is no Rails and no RuboCop config in
33+
this repo, so review for correctness of the git-tag / semver logic and
34+
shell-out safety, not Rails or RuboCop-style conventions.
35+
tools:
36+
shellcheck:
37+
enabled: true
38+
yamllint:
39+
enabled: true
40+
actionlint:
41+
enabled: true
42+
markdownlint:
43+
enabled: true
44+
languagetool:
45+
enabled: true
46+
gitleaks:
47+
enabled: true
48+
rubocop:
49+
enabled: false
50+
semgrep:
51+
enabled: false
52+
hadolint:
53+
enabled: false
54+
checkov:
55+
enabled: false
56+
chat:
57+
auto_reply: true

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_size = 4
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.md]
12+
trim_trailing_whitespace = false
13+
14+
[*.{bats,sh,yml,yaml}]
15+
indent_size = 2

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Exclude files from releases/tarballs
2+
tests/ export-ignore
3+
.github/ export-ignore
4+
.gitattributes export-ignore
5+
.editorconfig export-ignore

.github/scripts/release.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Cut a release for the ddev-pressable add-on.
5+
#
6+
# DDEV add-ons are versioned by git tags: `ddev add-on get pressable/ddev-pressable`
7+
# installs the latest GitHub release, which .github/workflows/release.yml publishes
8+
# automatically when a `vX.Y.Z` tag is pushed. This helper computes the next
9+
# version from the latest tag, then creates and pushes the annotated tag.
10+
#
11+
# Usage (run from the repo root):
12+
# .github/scripts/release.rb # patch bump (default): 1.2.3 -> 1.2.4
13+
# .github/scripts/release.rb patch # 1.2.3 -> 1.2.4
14+
# .github/scripts/release.rb minor # 1.2.3 -> 1.3.0
15+
# .github/scripts/release.rb major # 1.2.3 -> 2.0.0
16+
# .github/scripts/release.rb 1.5.0 # explicit version
17+
# .github/scripts/release.rb --dry-run minor
18+
19+
require "open3"
20+
21+
DEFAULT_BUMP = "patch"
22+
REMOTE = "origin"
23+
MAIN_BRANCH = "main"
24+
25+
# Run a command, aborting with its output on failure.
26+
def run(*cmd)
27+
out, status = Open3.capture2e(*cmd)
28+
abort "command failed: #{cmd.join(' ')}\n#{out}" unless status.success?
29+
out.strip
30+
end
31+
32+
# Run a command, returning its (stripped) output and ignoring a non-zero exit.
33+
def capture(*cmd)
34+
out, = Open3.capture2e(*cmd)
35+
out.strip
36+
end
37+
38+
dry_run = !ARGV.delete("--dry-run").nil?
39+
arg = ARGV.shift || DEFAULT_BUMP
40+
41+
branch = run("git", "rev-parse", "--abbrev-ref", "HEAD")
42+
abort "Refusing to release from '#{branch}'; switch to '#{MAIN_BRANCH}' first." unless branch == MAIN_BRANCH
43+
44+
abort "Working tree is dirty; commit or stash changes first." unless capture("git", "status", "--porcelain").empty?
45+
46+
run("git", "fetch", "--tags", REMOTE)
47+
local = run("git", "rev-parse", "@")
48+
# `--verify --quiet` prints nothing and exits non-zero when there is no upstream
49+
# (e.g. CI's `git checkout -B main`), so the sync check is skipped rather than
50+
# tripping on the error text. Without it the check aborts whenever no upstream
51+
# tracking is configured.
52+
upstream = capture("git", "rev-parse", "--verify", "--quiet", "@{u}")
53+
abort "Local #{MAIN_BRANCH} is out of sync with #{REMOTE}; pull/push first." unless upstream.empty? || local == upstream
54+
55+
# Pick the highest STRICT semver tag, ignoring any malformed `v*` tags
56+
# (e.g. `v1`, `vfoo`) that would otherwise crash the major.minor.patch split.
57+
latest = capture("git", "tag", "--list", "v*", "--sort=-v:refname")
58+
.lines.map(&:strip)
59+
.find { |t| t.match?(/\Av\d+\.\d+\.\d+\z/) }
60+
current = latest ? latest.sub(/\Av/, "") : "0.0.0"
61+
major, minor, patch = current.split(".").map(&:to_i)
62+
63+
next_version =
64+
case arg
65+
when "major" then "#{major + 1}.0.0"
66+
when "minor" then "#{major}.#{minor + 1}.0"
67+
when "patch" then "#{major}.#{minor}.#{patch + 1}"
68+
when /\Av?\d+\.\d+\.\d+\z/ then arg.sub(/\Av/, "")
69+
else abort "Unknown argument '#{arg}'. Use major | minor | patch, or an explicit X.Y.Z."
70+
end
71+
72+
tag = "v#{next_version}"
73+
abort "Tag #{tag} already exists." unless capture("git", "tag", "--list", tag).empty?
74+
75+
puts "Current version: v#{current}"
76+
puts "Next version: #{tag}"
77+
78+
if dry_run
79+
puts "[dry-run] Would create and push annotated tag #{tag} to #{REMOTE}."
80+
exit 0
81+
end
82+
83+
run("git", "tag", "-a", tag, "-m", "Release #{tag}")
84+
run("git", "push", REMOTE, tag)
85+
86+
puts "Pushed #{tag}. The release workflow will publish the GitHub release."
87+
slug = capture("git", "remote", "get-url", REMOTE)[%r{github\.com[:/](.+?)(?:\.git)?\z}, 1]
88+
puts "Watch: https://github.com/#{slug}/actions" if slug
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: release-on-merge
2+
3+
# Auto-cuts a release after every successful merge to `main`:
4+
# merge -> the `tests` workflow runs on main -> on success this fires ->
5+
# compute the next version -> tag it -> publish the GitHub release.
6+
#
7+
# Bump level (default: patch):
8+
# - add a `release:minor` or `release:major` label to the merged PR, OR
9+
# - put `#minor` / `#major` in the merge/squash commit message.
10+
# Skip a release for a merge:
11+
# - add a `release:skip` label, OR put `[skip release]` in the commit message.
12+
#
13+
# It is gated on `tests` passing (via workflow_run) so a merge never ships a
14+
# broken release. The version logic lives in .github/scripts/release.rb (the same
15+
# helper used for manual releases); this workflow just selects the bump and runs it.
16+
on:
17+
workflow_run:
18+
workflows: ["tests"]
19+
types: [completed]
20+
branches: [main]
21+
22+
permissions:
23+
contents: write
24+
pull-requests: read
25+
26+
jobs:
27+
release:
28+
# Only when tests passed AND they ran for a push to main (not a PR test run).
29+
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' }}
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
ref: ${{ github.event.workflow_run.head_sha }}
35+
fetch-depth: 0
36+
37+
- name: Determine bump level and skip
38+
id: decide
39+
env:
40+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41+
REPO: ${{ github.repository }}
42+
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
43+
run: |
44+
set -euo pipefail
45+
msg="$(git log -1 --format=%B "${HEAD_SHA}")"
46+
labels="$(gh api "repos/${REPO}/commits/${HEAD_SHA}/pulls" --jq '.[].labels[].name' 2>/dev/null || true)"
47+
bump="patch"
48+
skip="false"
49+
if printf '%s' "${msg}" | grep -qiE '\[skip release\]' || printf '%s\n' "${labels}" | grep -qx 'release:skip'; then
50+
skip="true"
51+
elif printf '%s' "${msg}" | grep -qE '(^|[^[:alnum:]])#major([^[:alnum:]]|$)' || printf '%s\n' "${labels}" | grep -qx 'release:major'; then
52+
bump="major"
53+
elif printf '%s' "${msg}" | grep -qE '(^|[^[:alnum:]])#minor([^[:alnum:]]|$)' || printf '%s\n' "${labels}" | grep -qx 'release:minor'; then
54+
bump="minor"
55+
fi
56+
# Idempotency guard: if this commit is already released (a semver tag
57+
# points at it), skip — e.g. a re-run of the `tests` workflow on the
58+
# same HEAD_SHA must not cut a second release.
59+
if git tag --points-at "${HEAD_SHA}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
60+
echo "Commit ${HEAD_SHA} is already released; skipping."
61+
skip="true"
62+
fi
63+
{
64+
echo "bump=${bump}"
65+
echo "skip=${skip}"
66+
} >> "${GITHUB_OUTPUT}"
67+
echo "Selected bump=${bump} skip=${skip}"
68+
69+
- name: Tag and publish release
70+
if: ${{ steps.decide.outputs.skip != 'true' }}
71+
env:
72+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73+
run: |
74+
set -euo pipefail
75+
git config user.name "github-actions[bot]"
76+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
77+
# Put the tested commit on a local `main` so release.rb's branch guard passes.
78+
git checkout -B main
79+
ruby .github/scripts/release.rb "${{ steps.decide.outputs.bump }}"
80+
tag="$(git describe --tags --abbrev=0)"
81+
gh release create "${tag}" --title "${tag}" --generate-notes --verify-tag

.github/workflows/release.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: release
2+
3+
# Publishes a GitHub release whenever a semver tag is pushed. The release's
4+
# auto-generated source tarball is what `ddev add-on get pressable/ddev-pressable`
5+
# downloads, so the git tag is the add-on's version. Cut a tag with
6+
# `.github/scripts/release.rb`.
7+
on:
8+
push:
9+
tags:
10+
- 'v*'
11+
12+
permissions:
13+
contents: write
14+
15+
jobs:
16+
release:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Create GitHub release
24+
env:
25+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26+
run: |
27+
gh release create "${GITHUB_REF_NAME}" \
28+
--title "${GITHUB_REF_NAME}" \
29+
--generate-notes \
30+
--verify-tag

.github/workflows/tests.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: tests
2+
on:
3+
pull_request:
4+
paths-ignore:
5+
- "**.md"
6+
push:
7+
branches: [ main ]
8+
paths-ignore:
9+
- "**.md"
10+
11+
schedule:
12+
- cron: '25 08 * * *'
13+
14+
workflow_dispatch:
15+
inputs:
16+
debug_enabled:
17+
type: boolean
18+
description: Debug with tmate
19+
required: false
20+
default: false
21+
22+
concurrency:
23+
group: ${{ github.workflow }}-${{ github.ref }}
24+
cancel-in-progress: true
25+
26+
permissions:
27+
contents: read
28+
29+
jobs:
30+
tests:
31+
strategy:
32+
matrix:
33+
ddev_version: [stable, HEAD]
34+
fail-fast: false
35+
36+
runs-on: ubuntu-latest
37+
38+
steps:
39+
- uses: ddev/github-action-add-on-test@v2
40+
with:
41+
ddev_version: ${{ matrix.ddev_version }}
42+
token: ${{ secrets.GITHUB_TOKEN }}
43+
debug_enabled: ${{ github.event.inputs.debug_enabled }}
44+
addon_repository: ${{ env.GITHUB_REPOSITORY }}
45+
addon_ref: ${{ env.GITHUB_REF }}

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# macOS
2+
.DS_Store
3+
4+
# Editors
5+
.idea/
6+
.vscode/
7+
*.swp
8+
9+
# Local bats test scratch (the test harness writes under $HOME/tmp)
10+
tests/tmp/

0 commit comments

Comments
 (0)