Development
Development
The source is TypeScript in src/, with tests next to the files they test (*.test.ts). The published package contains only the compiled dist/ with JavaScript and .d.ts files; tests and translation fixtures stay in the repository.
npm run build --workspace @wpelevator/wp-release
npm test --workspace @wpelevator/wp-release
npm run lint --workspace @wpelevator/wp-releasebuildcompilessrc/todist/withtsc, leaving out the tests.testruns the.tstests directly withnode --test, using Node.js type stripping, so it needs Node.js 22.18 or later but no build. The SVN integration tests insvn-deploy.integration.test.tscreate repositories withsvnadminand deploy to theirfile://URLs; they're skipped whensvnorsvnadminisn't installed.lintruns ESLint and atsctype check.
The i18n tests in @wpelevator/wp-gettext validate gettext round trips, script catalogs and generated PHP data without launching PHP, WP-CLI or WordPress, covering contexts, plural forms, comments, flags, Unicode, escaping and PHP-looking input as data.
The wpelevator-source export condition points at src/index.ts. The wp-release tests and monorepo CLI scripts use it to work with the source instead of a dist/ build. Action builds use the declared dependency's default export, which resolves to dist/ for the workspace copy and the published package. Build wp-release before building the actions.
execute( 'deploy-svn', args, { cwd, logger, svnRunner } ) runs a single-plugin deployment or a configured plan or deployment. The optional svnRunner replaces the external command runner in both modes; without it, the command uses its normal runner.
Architecture
wp-release has two layers, so that it fits any repository layout:
- Primitives work on the directory, file or values they're given and know nothing about the repository:
build <dir>,zip <dir>,checksums,sign,verify,version-check,deploy-git <dir>,deploy-svn <dir>,i18n-pot <dir>,i18n-audit <dir>and thei18n-mo,i18n-jsonandi18n-phpcompilers. - The config layer reads
wp-release.config.jsfor everything about the repository: where packages are (rootand the package globs), how their versions and tags are named (version,tagFormat), which files they ship (files), how their translations are extracted (i18n), where they deploy (deploy) and the gitremote. The changeset commands (with a single-project fallback when no config exists),deploy-gitanddeploy-svn --release-ref,build --packageandzip --package, andi18n-potandi18n-auditwith--packageor--allbelong to it, and the global--configand--tag-formatoptions select or adjust the config.zipandchecksumsalso read caller configuration for integrity settings even without--package; verification reads it only when explicitly selected. Only--release-refreads which package and version a ref releases. A primitive with a config mode, likedeploy-git --release-refbesidedeploy-git <dir>orzip --packagebesidezip <dir>, only takes the package directory, its files and its targets from the config there, and otherwise works the same.
Configured repositories use the layout and naming conventions their config sets. Config-free changeset releases use the root-only detection and defaults described above.
The GitHub Actions are adapters to these commands: they use the same flag names and execute() entry point, with secrets supplied through environment variables. The SVN action injects a command runner that checks or installs SVN only when a deployment runs its first SVN command; config planning and deployment behavior remain in wp-release.
The package uses ES modules as its unit of encapsulation, like most Node.js CLI tools: a module exports a few functions and keeps its helpers private, so there's no class to wrap them in. A class is used only when it holds state across steps or binds dependencies, so that callers and tests can replace them:
- Workflows with steps are classes, so that their steps share state and dependencies on the instance:
SvnDeployshares the temporary working copy acrossprepare(),checkout(),sync()andcommit(),GitDeploybinds the options and the git runner to its steps, andChangesetReleasebinds the repository and the runners toadd(),version()andtag(). - External commands run through injectable runners:
createSvnRunner()andcreateGitRunner()return functions that spawn the binary without a shell. The classes take a runner as an option and create the default one when it's missing, so tests pass a fake runner, or one that wraps the real runner and records some commands, instead of putting fake binaries on thePATH. TheSvnandReleaseRepositoryclasses wrap a runner with the commands of their domain. - Domain logic is plain functions that take their dependencies as arguments, like
discoverPackages( repository, config ), so that each one can be tested on its own. - Commands in
src/commands/parse the CLI options and call the library, andcli.tshandles the global options, logging and exit codes. Errors carry their exit code, likeValidationErrorwith2. Signing lives in@wpelevator/signatures, which throws errors with acode;src/signing.tswraps it to readWP_RELEASE_SIGNING_KEYand to report those errors asValidationError. ZIP archives live in@wpelevator/zipin the same way:src/zip.tsandsrc/unzip.tsadapt its entries andzipPathinputs and report itsZipErrorasValidationError, with the--forcehint for an existing archive. File selection and syncing live in@wpelevator/filesetand checksums in@wpelevator/checksums, which throw errors with acodein the same way:src/file-selection.ts,src/sync.tsandsrc/checksums.tswrap them to report those errors asValidationError, to build the manifest metadata from the plugin or theme slug and version, to select the files from the rules before hashing, and to pass the Gitexport-ignorelister to the fileset package.src/integrity.tswraps the manifest codecs of the checksums package in the same way. - Constructors only store their options. Work starts in a method call.
The changeset release modules, from the entry point down:
| Module | Responsibility |
|---|---|
changeset-release.ts | ChangesetRelease, which plans releases and runs add(), getStatus(), version() and tag(), and the function form of each operation. |
single-project.ts | Resolving the config-free root project, its version source and targets, and shared release settings. |
changeset-config.ts | Loading and validating wp-release.config.js, and the version source and target helpers. |
changeset-packages.ts | Discovering the packages of the configured globs, with or without a version, and finding the changed ones. |
changeset-files.ts | Reading, writing and consuming changeset files. |
changeset-dependents.ts | The dependents policy: which packages re-release with their sources, and rewriting their dependency specs. |
changesets.ts | Parsing and formatting changesets, and the release types. |
version-files.ts | Reading and writing versions in manifests, headers, readmes and PHP constants. |
changelog.ts | Changelog entries, and the CHANGELOG.md and readme changelog sections. |
release-tags.ts | Choosing the version and previous tag of a release, and building its release notes. |
tag-format.ts | Formatting tag names from the tag format. |
package-deploy.ts | Resolving the configured deploys of a ref: the released packages, their targets and file selections, for deploy-git and deploy-svn, and the file selection of a package for zip --package. |
package-zip.ts | zipPackage(), the ZIP and checksums manifest of the selected files of a package, for zip and the snapshot builds of a monorepo. |
release-repository.ts | ReleaseRepository, the git commands of a release: tags, commits and changed files. |
file-journal.ts | FileJournal, which restores every file a failed changeset-version run changed. |
versions.ts | Version checks and SemVer precedence. |
The internationalization modules in src/i18n/ adapt the standalone @wpelevator/wp-gettext package, which owns extraction, catalogs, POT, PO, MO, script JSON, .l10n.php and audits:
| Module | Responsibility |
|---|---|
packages.ts | resolveI18nPackages(), the config adapter that selects a directory, one package or all packages and resolves their options and POT paths. |
generate.ts | extractCatalog(), generatePackagePot(), resolvePotPath() and validatePotDestinations(), which read the headers of the main file with package-meta.ts and pass them as meta to the generate functions of @wpelevator/wp-gettext/generate. |
gettext.ts | The other functions of @wpelevator/wp-gettext, with their GettextError reported as a ValidationError with the original error as its cause. |