Skip to content

Commit 3b1d38d

Browse files
authored
Merge pull request #18 from ernilambar/refine-commands
Refine commands
2 parents 11601e0 + 478d81b commit 3b1d38d

File tree

10 files changed

+365
-117
lines changed

10 files changed

+365
-117
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,41 @@ Run from your project root (the directory that contains `package.json` with your
1818
npx wp-deployer
1919
```
2020

21+
Show CLI help or version:
22+
23+
```sh
24+
npx wp-deployer --help
25+
npx wp-deployer --version
26+
```
27+
2128
Or add a script to `package.json` and run it:
2229

2330
```sh
2431
npm run wpdeploy
2532
```
2633

34+
## SVN authentication
35+
36+
wp-deployer is intended for **deploying from your own computer** to WordPress.org SVN. It does **not** store or read an SVN password.
37+
38+
* **`username`** in `wpDeployer` is passed to `svn` as `--username` only.
39+
* **Passwords** are handled entirely by your **Subversion client** (prompt in the terminal and/or the **OS keychain** / SVN credential cache, depending on your setup).
40+
* The first time you deploy (or after credentials expire), SVN may **ask for your password**. Later runs often **do not prompt**, because SVN reuses **cached** credentials—that is normal and secure on a personal machine.
41+
* Follow [WordPress.org’s current documentation](https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/) for account access.
42+
43+
On a **shared** computer, review SVN’s credential storage and clear saved auth when you are done if needed.
44+
2745
## Settings
2846

2947
* **slug** : Plugin or theme slug; Default: `name` value in `package.json`
3048
* **username** : WordPress repository username; This is required.
31-
* **repoType**: Repo type; `plugin` or `theme`; Default: `plugin`
49+
* **repoType**: Repo type; `plugin` or `theme`; Default: `plugin`.
3250
* **buildDir**: The directory where your theme or plugin exists as you want it on the repo. Default: `dist`
3351
* **deployTrunk**: Whether to deploy to trunk. This could be set to false to only commit the assets directory. Applies for `plugin` only; Default: `true`
3452
* **deployTag**: Whether to create a tag for this version from trunk. Set to `false` to skip tagging. Applies for `plugin` only; Default: `true`
3553
* **deployAssets**: Whether to deploy assets. Applies for `plugin` only; Default: `false`
3654
* **assetsDir**: The directory where your plugins assets are kept; Default: `.wordpress-org`
55+
* **tmpDir**: Parent directory for the SVN working copy (`slug` is appended). Default: system temp directory from `os.tmpdir()` (not hard-coded `/tmp`).
3756
* **earlierVersion**: Last released version. Applies for `theme` only; This is required if `repoType` is `theme`.
3857

3958
## Example

index.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
* Exit codes: 0 success, 1 config/validation/preflight, 2 deploy failure, 130 SIGINT.
1111
*/
1212

13+
import path from 'node:path'
14+
import { fileURLToPath } from 'node:url'
1315
import fs from 'fs-extra'
1416
import chalk from 'chalk'
17+
import minimist from 'minimist'
1518
import { exec as execCb } from 'child_process'
1619
import { promisify } from 'util'
1720
import { resolveSettings } from './lib/config.js'
@@ -22,7 +25,41 @@ import { EXIT_SUCCESS, EXIT_CONFIG, EXIT_RUNTIME, EXIT_SIGINT } from './lib/exit
2225

2326
const exec = promisify(execCb)
2427

25-
const pkg = fs.readJsonSync('./package.json')
28+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
29+
30+
const argv = minimist(process.argv.slice(2), {
31+
boolean: ['help', 'version'],
32+
alias: { h: 'help', v: 'version' }
33+
})
34+
35+
function printHelp () {
36+
console.log(`Usage: wp-deployer [options]
37+
38+
Deploy a WordPress plugin or theme to WordPress.org SVN using wpDeployer in package.json.
39+
40+
Options:
41+
--help, -h Show this message
42+
--version, -v Print wp-deployer version
43+
`)
44+
}
45+
46+
function printVersion () {
47+
const selfPkg = fs.readJsonSync(path.join(__dirname, 'package.json'))
48+
console.log(selfPkg.version)
49+
}
50+
51+
if (argv.help) {
52+
printHelp()
53+
process.exit(EXIT_SUCCESS)
54+
}
55+
56+
if (argv.version) {
57+
printVersion()
58+
process.exit(EXIT_SUCCESS)
59+
}
60+
61+
const pkgPath = path.join(process.cwd(), 'package.json')
62+
const pkg = fs.readJsonSync(pkgPath)
2663

2764
const awk = process.platform === 'win32' ? 'gawk' : 'awk'
2865
const noRunIfEmpty = process.platform !== 'darwin' ? '--no-run-if-empty ' : ''

lib/config.js

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
/**
2-
* Config resolution: defaults + wpDeployer merge, url/svnPath/buildDir normalization.
2+
* Config resolution: defaults + wpDeployer merge, svnPath/buildDir normalization,
3+
* and WordPress.org SVN base URL (derived from repoType + slug).
34
* Returns { settings, error }. error is set for validation failures (caller should exit).
45
*/
56

6-
import merge from 'just-merge'
7+
import path from 'node:path'
8+
import os from 'node:os'
79
import { validateSettingsSchema } from './config-schema.js'
810
import { validateSvnSafeVersion, validateSvnUrl } from './validators/index.js'
911
import { hasPathUnsafeChars } from './validators/shared.js'
1012

13+
function wordpressOrgSvnBaseUrl (repoType, slug) {
14+
if (repoType === 'theme') {
15+
return `https://themes.svn.wordpress.org/${slug}/`
16+
}
17+
return `https://plugins.svn.wordpress.org/${slug}/`
18+
}
19+
1120
export function getDefaults (pkg) {
1221
return {
13-
url: '',
1422
slug: `${pkg.name}`,
1523
// Seeded from pkg.name; resolveSettings aligns with slug when wpDeployer omits mainFile
1624
mainFile: `${pkg.name}.php`,
@@ -22,7 +30,7 @@ export function getDefaults (pkg) {
2230
deployTag: true,
2331
deployAssets: false,
2432
assetsDir: '.wordpress-org',
25-
tmpDir: '/tmp/',
33+
tmpDir: os.tmpdir(),
2634
earlierVersion: '',
2735
newVersion: pkg.version
2836
}
@@ -36,7 +44,7 @@ export function getDefaults (pkg) {
3644
export function resolveSettings (pkg) {
3745
const defaults = getDefaults(pkg)
3846
const wpDeployer = Object.prototype.hasOwnProperty.call(pkg, 'wpDeployer') ? pkg.wpDeployer : {}
39-
let settings = merge(defaults, wpDeployer)
47+
let settings = { ...defaults, ...wpDeployer }
4048

4149
const slug = settings.slug
4250
if (typeof slug !== 'string' || slug.length === 0) {
@@ -60,20 +68,15 @@ export function resolveSettings (pkg) {
6068
settings.mainFile = `${settings.slug}.php`
6169
}
6270

63-
settings = merge(settings, {
64-
svnPath: settings.tmpDir.replace(/\/$|$/, '/') + settings.slug
65-
})
71+
settings = {
72+
...settings,
73+
svnPath: path.join(settings.tmpDir, settings.slug)
74+
}
6675

6776
settings.buildDir = settings.buildDir.replace(/\/$|$/, '/')
6877

69-
// Default WordPress.org SVN URLs follow `slug` (merged default: pkg.name), not pkg.name directly.
70-
if (!settings.url) {
71-
if (settings.repoType === 'plugin') {
72-
settings.url = `https://plugins.svn.wordpress.org/${settings.slug}/`
73-
} else if (settings.repoType === 'theme') {
74-
settings.url = `https://themes.svn.wordpress.org/${settings.slug}/`
75-
}
76-
}
78+
// Not configurable: only wordpress.org SVN is supported; ignore legacy wpDeployer.url.
79+
delete settings.url
7780

7881
const schemaResult = validateSettingsSchema(settings)
7982
if (!schemaResult.ok) {
@@ -84,6 +87,8 @@ export function resolveSettings (pkg) {
8487
}
8588
}
8689

90+
settings.url = wordpressOrgSvnBaseUrl(settings.repoType, settings.slug)
91+
8792
const urlResult = validateSvnUrl(settings.url)
8893
if (!urlResult.ok) {
8994
return {

lib/schemas/config.schema.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"description": "Schema for wp-deployer runtime configuration.",
66
"type": "object",
77
"required": [
8-
"url",
98
"slug",
109
"mainFile",
1110
"username",
@@ -22,7 +21,6 @@
2221
"svnPath"
2322
],
2423
"properties": {
25-
"url": { "type": "string" },
2624
"slug": { "type": "string", "minLength": 1 },
2725
"mainFile": { "type": "string", "minLength": 1 },
2826
"username": { "type": "string", "minLength": 1 },

lib/steps.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* Errors propagate — the runner should abort the pipeline on first failure.
66
*/
77

8+
import path from 'node:path'
9+
810
function copyDirectory (fs, srcDir, destDir) {
911
if (srcDir.slice(-1) !== '/') {
1012
srcDir = `${srcDir}/`
@@ -16,15 +18,15 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
1618
const clearTrunk = (s) => {
1719
return async function (s) {
1820
console.log('Clearing trunk.')
19-
await exec(`rm -fr ${s.svnPath}/trunk/*`)
21+
await exec(`rm -fr ${path.join(s.svnPath, 'trunk')}/*`)
2022
return s
2123
}
2224
}
2325

2426
const clearAssets = (s) => {
2527
return async function (s) {
2628
console.log('Clearing assets.')
27-
await exec(`rm -fr ${s.svnPath}/assets/*`)
29+
await exec(`rm -fr ${path.join(s.svnPath, 'assets')}/*`)
2830
return s
2931
}
3032
}
@@ -33,7 +35,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
3335
return async function (s) {
3436
console.log(`Checking out ${s.url}${dir}/...`)
3537
const checkoutUrl = `${s.url}${dir}/`
36-
const targetPath = `${s.svnPath}/${dir}`
38+
const targetPath = path.join(s.svnPath, dir)
3739
await exec(`svn co --force-interactive --username="${s.username}" ${checkoutUrl} ${targetPath}`, { maxBuffer: s.maxBuffer })
3840
console.log('Check out complete.')
3941
return s
@@ -43,7 +45,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
4345
const copyBuild = (s) => {
4446
return async function (s) {
4547
console.log(`Copying build directory: ${s.buildDir}`)
46-
copyDirectory(fs, s.buildDir, `${s.svnPath}/trunk/`)
48+
copyDirectory(fs, s.buildDir, path.join(s.svnPath, 'trunk') + path.sep)
4749
return s
4850
}
4951
}
@@ -53,7 +55,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
5355
console.log('Adding files in trunk')
5456
let cmd = 'svn resolve --accept working -R . && svn status |' + awk + " '/^[?]/{print $2}' | xargs " + noRunIfEmpty + 'svn add;'
5557
cmd += 'svn status | ' + awk + " '/^[!]/{print $2}' | xargs " + noRunIfEmpty + 'svn delete;'
56-
await exec(cmd, { cwd: `${s.svnPath}/trunk` })
58+
await exec(cmd, { cwd: path.join(s.svnPath, 'trunk') })
5759
return s
5860
}
5961
}
@@ -62,7 +64,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
6264
return async function (s) {
6365
const commitMsg = `Committing ${s.newVersion} to trunk`
6466
const cmd = `svn commit --force-interactive --username="${s.username}" -m "${commitMsg}"`
65-
await exec(cmd, { cwd: `${s.svnPath}/trunk` })
67+
await exec(cmd, { cwd: path.join(s.svnPath, 'trunk') })
6668
return s
6769
}
6870
}
@@ -80,7 +82,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
8082
const copyAssets = (s) => {
8183
return async function (s) {
8284
console.log('Copying assets')
83-
copyDirectory(fs, s.assetsDir, `${s.svnPath}/assets/`)
85+
copyDirectory(fs, s.assetsDir, path.join(s.svnPath, 'assets') + path.sep)
8486
return s
8587
}
8688
}
@@ -90,7 +92,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
9092
console.log('Adding assets')
9193
let cmd = 'svn resolve --accept working -R . && svn status |' + awk + " '/^[?]/{print $2}' | xargs " + noRunIfEmpty + 'svn add;'
9294
cmd += 'svn status | ' + awk + " '/^[!]/{print $2}' | xargs " + noRunIfEmpty + 'svn delete;'
93-
await exec(cmd, { cwd: `${s.svnPath}/assets` })
95+
await exec(cmd, { cwd: path.join(s.svnPath, 'assets') })
9496
return s
9597
}
9698
}
@@ -99,7 +101,7 @@ export function createPluginSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
99101
return async function (s) {
100102
const commitMsg = 'Committing assets'
101103
const cmd = `svn commit --force-interactive --username="${s.username}" -m "${commitMsg}"`
102-
await exec(cmd, { cwd: `${s.svnPath}/assets` })
104+
await exec(cmd, { cwd: path.join(s.svnPath, 'assets') })
103105
return s
104106
}
105107
}
@@ -125,7 +127,7 @@ export function createThemeSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
125127
const prepareThemeTmp = (s) => {
126128
return async function (s) {
127129
console.log('Preparing temporary folder.')
128-
await exec(`rm -fr ${s.svnPath}/`)
130+
await exec(`rm -fr ${s.svnPath}`)
129131
return s
130132
}
131133
}
@@ -134,7 +136,7 @@ export function createThemeSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
134136
return async function (s) {
135137
console.log(`Checking out ${s.url}...`)
136138
const checkoutUrl = `${s.url}/`
137-
const targetPath = `${s.svnPath}/`
139+
const targetPath = s.svnPath
138140
await exec(`svn co --force-interactive --username="${s.username}" ${checkoutUrl} ${targetPath}`, { maxBuffer: s.maxBuffer })
139141
console.log('Check out complete.')
140142
return s
@@ -153,15 +155,15 @@ export function createThemeSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
153155
const clearTheme = (s) => {
154156
return async function (s) {
155157
console.log('Clearing theme.')
156-
await exec(`rm -fr ${s.svnPath}/${s.newVersion}/*`)
158+
await exec(`rm -fr ${path.join(s.svnPath, s.newVersion)}/*`)
157159
return s
158160
}
159161
}
160162

161163
const copyTheme = (s) => {
162164
return async function (s) {
163165
console.log(`Copying build directory: ${s.buildDir}`)
164-
copyDirectory(fs, s.buildDir, `${s.svnPath}/${s.newVersion}/`)
166+
copyDirectory(fs, s.buildDir, path.join(s.svnPath, s.newVersion) + path.sep)
165167
return s
166168
}
167169
}
@@ -171,7 +173,7 @@ export function createThemeSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
171173
console.log('Adding theme files')
172174
let cmd = 'svn resolve --accept working -R . && svn status |' + awk + " '/^[?]/{print $2}' | xargs " + noRunIfEmpty + 'svn add;'
173175
cmd += 'svn status | ' + awk + " '/^[!]/{print $2}' | xargs " + noRunIfEmpty + 'svn delete;'
174-
await exec(cmd, { cwd: `${s.svnPath}/${s.newVersion}` })
176+
await exec(cmd, { cwd: path.join(s.svnPath, s.newVersion) })
175177
return s
176178
}
177179
}
@@ -181,7 +183,7 @@ export function createThemeSteps (settings, { exec, fs, awk, noRunIfEmpty }) {
181183
const commitMsg = `Committing theme ${s.newVersion}`
182184
console.log(commitMsg)
183185
const cmd = `svn commit --force-interactive --username="${s.username}" -m "${commitMsg}"`
184-
await exec(cmd, { cwd: `${s.svnPath}/${s.newVersion}` })
186+
await exec(cmd, { cwd: path.join(s.svnPath, s.newVersion) })
185187
return s
186188
}
187189
}

package-lock.json

Lines changed: 3 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wp-deployer",
3-
"version": "1.0.3",
3+
"version": "2.0.0",
44
"description": "Deploy WordPress plugin and theme to the WordPress.org repo.",
55
"main": "index.js",
66
"type": "module",
@@ -33,7 +33,7 @@
3333
"ajv": "^8.18.0",
3434
"chalk": "^5.6.2",
3535
"fs-extra": "^11.3.4",
36-
"just-merge": "^3.2.0"
36+
"minimist": "^1.2.8"
3737
},
3838
"devDependencies": {
3939
"standard": "^17.1.2"

0 commit comments

Comments
 (0)