@wpelevator/wp-release 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # WP Release
2
2
 
3
- Package, checksum, verify and version-check WordPress plugins and themes, and deploy plugins to the WordPress.org plugin SVN repository. Usable as a CLI, as a library, and through the [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip), [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums), [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) and [`wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) GitHub Actions.
3
+ Package, checksum, verify and version-check WordPress plugins and themes, deploy plugins to the WordPress.org plugin SVN repository, and deploy plugins, themes and packages to Git repositories. Usable as a CLI, as a library, and through the [`wp-release-zip`](https://github.com/wpelevator/wp-release-zip), [`wp-release-checksums`](https://github.com/wpelevator/wp-release-checksums), [`wp-release-sign`](https://github.com/wpelevator/wp-release-sign) and [`wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) GitHub Actions.
4
4
 
5
5
  Themes are released by uploading a ZIP on WordPress.org, so `deploy-svn` is for plugins only; use `zip` to build the theme upload.
6
6
 
@@ -32,6 +32,7 @@ npx wp-release --help
32
32
  | `wp-release verify [<source-dir>] --checksums <file> [options]` | Check a plugin or theme directory, or a ZIP, against a checksums manifest. |
33
33
  | `wp-release version-check [<source-dir>] [options]` | Check that the `Version` header, readme `Stable tag`, PHP constants and git tag agree. |
34
34
  | `wp-release deploy-svn [<source-dir>] [options]` | Deploy a plugin to the WordPress.org plugin SVN repository. |
35
+ | `wp-release deploy-git [<source-dir>] --git-url <url> [options]` | Deploy the files of a plugin, theme or package to a Git repository as a release commit. |
35
36
  | `wp-release changeset-add [options]` | Record the release impact of a pull request. |
36
37
  | `wp-release changeset-status [options]` | List pending releases and check changed packages for changesets. |
37
38
  | `wp-release changeset-version [options]` | Apply version bumps and changelog entries. |
@@ -60,11 +61,14 @@ Each command also adds a section to the [job summary](https://docs.github.com/en
60
61
  | Command | Summary |
61
62
  |---|---|
62
63
  | `deploy-svn` | Plugin, linked to its WordPress.org page, version, written SVN paths, the commit linked to its WordPress.org changeset (or the dry run or no-op result), and the SVN changes in a collapsed table. |
64
+ | `deploy-git` | Repository, release commit and the pushed branch and tag. |
63
65
  | `zip` | ZIP, number of files and checksums manifest. |
64
66
  | `checksums` | Manifest and number of files. |
65
67
  | `sign` | Signature files and the public key. |
66
68
  | `verify` | That all files match, or the counts and a table of the changed, missing and extra files. |
67
69
  | `version-check` | The matching version, or a table of the mismatches with their files and lines. |
70
+ | `changeset-version` | A table of the package releases with their version changes and bumps. |
71
+ | `changeset-tag` | Each tag with its previous tag and status, and its release notes in a collapsed section. |
68
72
 
69
73
  A failed command adds its error to the summary, or a table of its problems with their files and lines, listing up to 100 and counting the rest. The Markdown of the summaries isn't part of the library API. File paths are relative to `GITHUB_WORKSPACE`, or the current directory without it. Files outside it, like those of an extracted ZIP, get an annotation without a file. Multi-line messages stay one annotation.
70
74
 
@@ -89,8 +93,8 @@ flowchart TD
89
93
  F -->|Pass| H["Merge the feature pull request to main"]
90
94
  H --> I["changeset-version updates configured version files and changelogs"]
91
95
  I --> J{"Are there package releases to review?"}
92
- J -->|Yes| K["Workflow opens or updates the Version Packages pull request"]
93
- K --> L["Review and merge the Version Packages pull request"]
96
+ J -->|Yes| K["Workflow opens or updates the release pull request"]
97
+ K --> L["Review and merge the release pull request"]
94
98
  L --> M["changeset-tag creates tags for packages changed by that release"]
95
99
  M --> N["GitHub Releases are created from changelog entries"]
96
100
  N --> O["Tag-triggered publish and split workflows run"]
@@ -127,9 +131,27 @@ Add release notes in a feature PR with `npx wp-release changeset-add --package @
127
131
 
128
132
  On the release branch, preview with `changeset-version --dry-run`, then run `changeset-version` to update version targets and changelogs. By default, new Markdown entries go before the first version heading; `changelogInsertAfter` sets a literal insertion marker for `CHANGELOG.md` and, with `readmeChangelog`, the plugin readme. Put the marker in both files; a missing marker is an error. Without a marker, plugin readmes use the `== Changelog ==` heading. Failed updates are rolled back for safe retries.
129
133
 
134
+ Configure `changelogEntry( { package, changeset, commit } )` to format the changelog entry of each changeset, where `commit` is the `{ hash, shortHash, subject }` of the commit that added the changeset file, if it's committed. The formatted entries go into `CHANGELOG.md`, the `changelog` of each release in the `changeset-version` result, and so the release notes; plugin readmes keep the plain summaries. `getGitHubPullRequest( { cwd, commit } )` finds the `{ number, url, author }` of the pull request that merged a commit with the authenticated `gh` CLI, for example to credit it:
135
+
136
+ ```js
137
+ changelogEntry: async ( { changeset, commit } ) => {
138
+ const pullRequest = commit && ( await getGitHubPullRequest( { cwd: import.meta.dirname, commit: commit.hash } ) );
139
+
140
+ return pullRequest ? `${ changeset.summary } (#${ pullRequest.number } by @${ pullRequest.author })` : changeset.summary;
141
+ },
142
+ ```
143
+
144
+ To release packages separately, for example with a release pull request for each, pass `--package <name>`, repeatable, to version only those packages. A changeset that also names other packages keeps only their entries and its summary, so it still applies to their releases with the same bump. Changesets without packages are left for a run without `--package`.
145
+
146
+ The `changeset-version` result lists each release with its `package`, `bump`, `currentVersion`, `nextVersion` and `changelog` entries, a Markdown list like the one added to `CHANGELOG.md`. Use it to name and describe the release pull request in a repository script, or call `versionChangesets()` from the library and format the entries of each release with `formatChangelogEntries( release.changesets )`.
147
+
130
148
  After merging the release PR, `changeset-tag --since <merge-commit>^1 --push` pushes tags only for packages changed by that release. It pushes one tag at a time because GitHub doesn't trigger workflows for a push of more than three tags. Add `--github-release` to create missing GitHub Releases; it requires `--push`. For initial adoption, preview all missing tags with `changeset-tag --dry-run` (without `--since`), then push if needed. To release each package with a separate workflow instead, `changeset-tag --since <merge-commit>^1 --untagged --github-output` lists the releases that don't have a tag yet, without creating any. Tag pushes need Git write credentials; GitHub Releases need authenticated `gh`.
131
149
 
132
- For local development, run the TypeScript source with `npm run wp-release -- <command>` (for example, `npm run wp-release -- changeset-status --since origin/main`). GitHub workflows use the same CLI for versioning and tagging. `SUBTREE_DEPLOY_TOKEN` lets them update the Version Packages PR, push tags, and create GitHub Releases.
150
+ To release a single package, for example from a manual workflow, pass `--package <name>`, repeatable, and with one package `--version <version>` to fail unless the package is at that version. Packages versioned by git tags have no version file, so for them `--version` is the version to tag. Before a tag is created, every `version.to` target must match the version, so a plugin `Version` header that disagrees with its readme `Stable tag` blocks the tag. GitHub Releases of versions with a pre-release part, like `1.0.0-rc.1`, are marked as pre-releases.
151
+
152
+ The release notes are the version's `CHANGELOG.md` section, or without one the commits that changed the package since its previous tag, the highest version below the released one. Configure `releaseNotes( { package, version, tag, previousTag, changelog, commits, notes } )` to format them, for example to add links; `notes` is the default text and `commits` lists `{ hash, shortHash, subject }`, newest first. `changeset-tag --package <name> --dry-run` prints them without creating the tag.
153
+
154
+ For local development, run the TypeScript source with `npm run wp-release -- <command>` (for example, `npm run wp-release -- changeset-status --since origin/main`). GitHub workflows use the same CLI for versioning and tagging. `SUBTREE_DEPLOY_TOKEN` lets them update the release PR, push tags, and create GitHub Releases.
133
155
 
134
156
  ### Naming convention
135
157
 
@@ -143,6 +165,7 @@ For local development, run the TypeScript source with `npm run wp-release -- <co
143
165
  | Verify files against a manifest | `wp-release verify` | |
144
166
  | Check that the versions agree | `wp-release version-check` | |
145
167
  | Deploy to WordPress.org SVN | `wp-release deploy-svn` | [`wpelevator/wp-release-deploy-svn`](https://github.com/wpelevator/wp-release-deploy-svn) |
168
+ | Deploy to a Git repository | `wp-release deploy-git` | |
146
169
 
147
170
  Each concept has one name, written in the syntax of each interface:
148
171
 
@@ -154,11 +177,14 @@ Each concept has one name, written in the syntax of each interface:
154
177
  | Git tag to compare | `--git-tag <tag>` | | `gitTag` | Git tag checked by `version-check`. |
155
178
  | SVN tag to create | `--svn-tag <version>` | `svn-tag` | `svnTag` | Tag written by `deploy-svn`, distinct from a git tag. |
156
179
  | SVN repository | `--svn-url <url>` | `svn-url` | `svnUrl` | Deploy destination. |
180
+ | Git repository | `--git-url <url>` | | `gitUrl` | Destination of `deploy-git`. Credentials come from git, like a token in the URL or an SSH key, and are never printed. |
181
+ | Branch and tag to release to | `--branch <name>`, `--tag <name>` | | `branch`, `tag` | Refs written by `deploy-git` in the Git repository, distinct from the git tag that `version-check` compares. |
182
+ | Source history | `--split-prefix <dir>` | | `splitPrefix` | Directory of a monorepo package whose history, from `git subtree split`, becomes the source history of `deploy-git` releases. |
157
183
  | SVN credentials | `--svn-username <name>`, `SVN_USERNAME` and `SVN_PASSWORD` env vars | `svn-username`, `svn-password` | `svnUsername`, `svnPassword` | Needed only to commit. There's no password flag, since it would show in the process list; the action inputs and library options fall back to the env vars. |
158
184
  | Log format | `--log-format <text\|github>`, `--ci` for `github` | always `github` | `writer: new TextWriter()` or `new GitHubActionsWriter()` | How the log is printed: plain text, or GitHub Actions annotations and job summaries. Each name matches the start of its writer class. |
159
185
  | Assets sync | `--no-assets` to skip | `assets: false` to skip | `assets` | Booleans are positive in the library and the action; `--no-assets` is only the CLI negation, never `noAssets` or a `no-assets` input. |
160
186
 
161
- CLI JSON results use camelCase keys like `zipPath`, and GitHub Action outputs use kebab-case like `zip-path`. Library classes are named after the capability (`SvnDeploy`, `Svn`) without repeating "release".
187
+ CLI JSON results use camelCase keys like `zipPath`, and GitHub Action outputs use kebab-case like `zip-path`. Library classes are named after the capability (`SvnDeploy`, `Svn`, `GitDeploy`) without repeating "release".
162
188
 
163
189
  ### File selection
164
190
 
@@ -347,13 +373,43 @@ Validation:
347
373
  - `--trunk` without `--svn-tag` works but warns, because trunk then differs from the stable tag that WordPress.org serves downloads from.
348
374
  - A version mismatch throws a `VersionMismatchError`, a `ValidationError` with the `mismatches` and a problem for each, with its file and line. See [Logging](#logging).
349
375
 
376
+ ### Deploy to Git
377
+
378
+ `deploy-git` deploys the selected files of a plugin, theme or package to a Git repository, like a distribution repository that ships the built files and `vendor/` for Composer installs or GitHub-hosted updates, or the release repository of a monorepo package. It selects files like `zip`, with `.distignore` and `--exclude`, so built files that git ignores in the source repository, like `build/` or `vendor/`, are released too. The source directory must be inside a Git repository, which provides the source history and stores the release objects before they're pushed.
379
+
380
+ | Flag | Default | Writes |
381
+ |---|---|---|
382
+ | `--branch <name>` | off | Push the release to this branch |
383
+ | `--tag <name>` | off | Create this tag for the release |
384
+
385
+ ```bash
386
+ # Release a build to a distribution repository and tag it
387
+ wp-release deploy-git build --git-url git@github.com:me/my-plugin-dist.git --branch main --tag 1.2.0
388
+
389
+ # Release a monorepo package with its own history
390
+ wp-release deploy-git packages/my-plugin --split-prefix packages/my-plugin --git-url … --branch main
391
+ ```
392
+
393
+ How a deploy works:
394
+
395
+ 1. The selected files are written to the object database of the source repository and become a tree, with a temporary index, so the working tree and the index of the repository are left alone. Executable files stay executable.
396
+ 2. The previous release is fetched: the tip of `--branch`, or for a tag-only deploy the tip of the repository's default branch (its `HEAD`), whatever that branch holds. Release a tag together with `--branch` to choose the branch it builds on.
397
+ 3. When the files are the same as in the previous release, that release is reused. Otherwise a release commit is created with the files, whose parents are the previous release and the source history: the source commit (`--source-ref`, default `HEAD`), or with `--split-prefix` the history of that directory from `git subtree split`. The repository keeps the source history, and `git log --first-parent` lists the releases.
398
+ 4. When the previous release on `--branch` already has newer source history, the branch isn't moved, so running an older build again, like a re-run of an earlier CI job, can't release older files. The source history is outdated when it's an older commit of the source history that the previous release was made from, its last parent. `git subtree split` makes this reliable for `--split-prefix` too, since it creates the same commits for the same history. A revert is a new commit, so it's released, and with `--tag` the tag of an older build is still released, without moving the branch.
399
+ 5. The tag is checked in the repository first. A tag that already has the same files is the release: it's reused and nothing is pushed, so running a tag release again is a no-op. A tag with other files fails with exit code `2` before anything is pushed.
400
+ 6. The release is pushed to the branch, unless it didn't change or is outdated, and to the tag, in one atomic push. Pushes are never forced, so the branch only moves forward and a tag is never replaced, also when it was created in the meantime.
401
+
402
+ To release a tag again, for example after a release with the wrong files, delete the tag in the repository (`git push <url> :refs/tags/<tag>`) and run the deploy again. There's no flag for replacing a tag, since installs, Composer and GitHub Actions pins trust that a published tag doesn't change.
403
+
404
+ `--dry-run` runs everything except the push. `--message` sets the commit message, which defaults to `Release <tag or branch> from <source commit>`. Credentials come from git, like a token in the URL or an SSH key, and credentials in URLs are never printed. The commit needs a git identity (`user.name` and `user.email`).
405
+
350
406
  ### Exit codes
351
407
 
352
408
  | Code | Meaning |
353
409
  |---|---|
354
410
  | `0` | Success, dry run, no changes, or help |
355
- | `1` | Unexpected failure, such as a failed svn command |
356
- | `2` | Invalid options, failed release checks, an existing SVN tag, or files that don't match the checksums |
411
+ | `1` | Unexpected failure, such as a failed svn or git command |
412
+ | `2` | Invalid options, failed release checks, an existing SVN tag, a Git tag that exists with another release, or files that don't match the checksums |
357
413
  | `3` | Missing SVN credentials, or credentials that SVN rejected |
358
414
 
359
415
  ## Library
@@ -377,6 +433,24 @@ const { committed, revision, version, changes } = await new SvnDeploy(
377
433
 
378
434
  `resolveDeployOptions()` applies the defaults and throws a `ValidationError` (with `exitCode` `2`) for invalid combinations. `SvnDeploy` exposes each step (`prepare()`, `checkVersions()`, `checkout()`, `sync()`, `commit()` and `cleanup()`) separately so CI wrappers can run and report them one by one; `run()` runs all of them, or nothing when there's nothing to write. `getPlan()` lists the planned SVN changes. The `Svn` class wraps the svn command line, and takes the command runner as a dependency so that tests can replace it.
379
435
 
436
+ Git deploys:
437
+
438
+ ```js
439
+ import { GitDeploy, resolveGitDeployOptions } from '@wpelevator/wp-release';
440
+
441
+ const { revision, created, pushed } = await new GitDeploy(
442
+ resolveGitDeployOptions( {
443
+ sourceDir: 'packages/my-plugin',
444
+ gitUrl: 'git@github.com:me/my-plugin.git',
445
+ branch: 'main',
446
+ tag: '1.2.0',
447
+ splitPrefix: 'packages/my-plugin',
448
+ } )
449
+ ).run();
450
+ ```
451
+
452
+ `resolveGitDeployOptions()` throws a `ValidationError` without a repository URL or with an invalid branch or tag. `GitDeploy` exposes `createTree()`, `fetchPrevious()`, `createRelease()`, `isOutdated()`, `checkTag()`, `getRefsToPush()` and `push()` separately, like `SvnDeploy`, and takes a git command runner from `createGitRunner()` as a dependency. The result has `outdated` when the branch wasn't moved for older source history, the refs to write in `planned` and the refs that were pushed in `pushed`, which is empty in a dry run. `redactUrls()` hides the credentials in URLs.
453
+
380
454
  ### Logging
381
455
 
382
456
  `Logger` filters messages by level and passes them to a writer, like the transports of other Node.js loggers. The writer decides how messages look:
@@ -87,6 +87,10 @@ export interface ChangesetConfig {
87
87
  changelogInsertAfter?: string;
88
88
  /** Calculate a package's next version instead of using stable SemVer bumps. */
89
89
  nextVersion?: (input: NextVersionInput) => string;
90
+ /** Format the release notes of a tag, for example to add links. */
91
+ releaseNotes?: (input: ReleaseNotesInput) => string | Promise<string>;
92
+ /** Format the CHANGELOG.md entry of a changeset, for example to credit its pull request. */
93
+ changelogEntry?: (input: ChangelogEntryInput) => string | Promise<string>;
90
94
  }
91
95
  /** Inputs supplied to a custom next-version strategy. */
92
96
  export interface NextVersionInput {
@@ -95,6 +99,33 @@ export interface NextVersionInput {
95
99
  currentVersion: string;
96
100
  changesets: Changeset[];
97
101
  }
102
+ /** A commit of a package release. */
103
+ export interface ReleaseCommit {
104
+ hash: string;
105
+ shortHash: string;
106
+ subject: string;
107
+ }
108
+ /** Inputs supplied to a custom release notes formatter. */
109
+ export interface ReleaseNotesInput {
110
+ package: ChangesetPackage;
111
+ version: string;
112
+ tag: string;
113
+ /** Tag of the previous release of the package, without one for the first release. */
114
+ previousTag?: string;
115
+ /** The version's CHANGELOG.md section, or an empty string without one. */
116
+ changelog: string;
117
+ /** Commits that changed the package since the previous release, newest first. */
118
+ commits: ReleaseCommit[];
119
+ /** The default release notes: the changelog section, or the commit list without one. */
120
+ notes: string;
121
+ }
122
+ /** Inputs supplied to a custom changelog entry formatter. */
123
+ export interface ChangelogEntryInput {
124
+ package: ChangesetPackage;
125
+ changeset: Changeset;
126
+ /** Commit that added the changeset file, unless it isn't committed yet. */
127
+ commit?: ReleaseCommit;
128
+ }
98
129
  export type WpReleaseConfig = ChangesetConfig;
99
130
  export interface PackageJsonVersionOptions {
100
131
  file?: string;
@@ -194,6 +194,14 @@ function validateChangesetConfig(config, configFile) {
194
194
  'function' !== typeof candidate.nextVersion) {
195
195
  throw new ValidationError(`wp-release config ${configFile}.nextVersion must be a function.`);
196
196
  }
197
+ if (undefined !== candidate.changelogEntry &&
198
+ 'function' !== typeof candidate.changelogEntry) {
199
+ throw new ValidationError(`wp-release config ${configFile}.changelogEntry must be a function.`);
200
+ }
201
+ if (undefined !== candidate.releaseNotes &&
202
+ 'function' !== typeof candidate.releaseNotes) {
203
+ throw new ValidationError(`wp-release config ${configFile}.releaseNotes must be a function.`);
204
+ }
197
205
  candidate.packages.forEach((group, index) => {
198
206
  if ('object' !== typeof group ||
199
207
  null === group ||
@@ -1,4 +1,4 @@
1
- import type { Changeset, ChangesetReleasePlan, PackageRelease } from './changesets.ts';
1
+ import type { Changeset, ChangesetPackage, ChangesetReleasePlan, PackageRelease, VersionedRelease } from './changesets.ts';
2
2
  export interface ChangesetRepository {
3
3
  cwd: string;
4
4
  configFile?: string;
@@ -6,6 +6,15 @@ export interface ChangesetRepository {
6
6
  export interface ChangesetVersionInput extends ChangesetRepository {
7
7
  dryRun?: boolean;
8
8
  date?: string;
9
+ /** Version only these packages, leaving the other changesets pending. */
10
+ packages?: string[];
11
+ }
12
+ /** Versioned releases, the applied changesets and whether files were changed. */
13
+ export interface VersionChangesetsResult {
14
+ changesets: Changeset[];
15
+ releases: VersionedRelease[];
16
+ date: string;
17
+ changed: boolean;
9
18
  }
10
19
  export interface ChangesetTagInput extends ChangesetRepository {
11
20
  dryRun?: boolean;
@@ -13,12 +22,18 @@ export interface ChangesetTagInput extends ChangesetRepository {
13
22
  push?: boolean;
14
23
  githubRelease?: boolean;
15
24
  since?: string;
25
+ /** Tag only these packages. */
26
+ packages?: string[];
27
+ /** Expected version of the single package in `packages`. */
28
+ version?: string;
16
29
  }
17
30
  export interface ChangesetTagResult {
18
31
  tags: Array<{
19
32
  package: string;
20
33
  version: string;
21
34
  tag: string;
35
+ previousTag?: string;
36
+ prerelease: boolean;
22
37
  exists: boolean;
23
38
  created: boolean;
24
39
  notes: string;
@@ -31,6 +46,14 @@ export interface ChangesetTagResult {
31
46
  * @return A release plan; `undefined` when no config is present.
32
47
  */
33
48
  export declare function getChangesetReleasePlan(input: ChangesetRepository): Promise<ChangesetReleasePlan | undefined>;
49
+ /**
50
+ * Discover the packages of the repository configuration.
51
+ *
52
+ * @param input Repository root and optional explicit config file.
53
+ * @return Packages in the configured order with their current versions.
54
+ * @throws {ValidationError} Without a config, or for an invalid one.
55
+ */
56
+ export declare function getChangesetPackages(input: ChangesetRepository): Promise<ChangesetPackage[]>;
34
57
  /**
35
58
  * Create a Changesets-compatible Markdown file.
36
59
  *
@@ -48,10 +71,7 @@ export declare function addChangeset(input: ChangesetRepository & {
48
71
  * @param input Repository and versioning options.
49
72
  * @return Planned releases and whether files were changed.
50
73
  */
51
- export declare function versionChangesets(input: ChangesetVersionInput): Promise<ChangesetReleasePlan & {
52
- date: string;
53
- changed: boolean;
54
- }>;
74
+ export declare function versionChangesets(input: ChangesetVersionInput): Promise<VersionChangesetsResult>;
55
75
  /**
56
76
  * Create missing release tags, optionally pushing them to the configured remote.
57
77
  *