@wpelevator/wp-release 0.6.0 → 0.6.1

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
@@ -37,7 +37,7 @@ npx wp-release --help
37
37
  | `wp-release changeset-version [options]` | Apply version bumps and changelog entries. |
38
38
  | `wp-release changeset-tag [options]` | Create missing package release tags. |
39
39
 
40
- Run `npx wp-release <command> --help` for the options of each command. Every command reads the plugin or theme directory from the first argument or `--source-dir`, defaulting to the current directory, and prints its result as JSON with `--json`.
40
+ Run `npx wp-release <command> --help` for the options of each command. Every command reads the plugin or theme directory from the first argument or `--source-dir`, defaulting to the current directory, and prints its result as JSON with `--json`. In GitHub Actions, `--github-output` sets the result as JSON to the `result` output of the step instead, for later steps and jobs to read with `fromJSON( steps.<id>.outputs.result )`.
41
41
 
42
42
  ### GitHub Actions annotations and job summaries
43
43
 
@@ -127,7 +127,7 @@ Add release notes in a feature PR with `npx wp-release changeset-add --package @
127
127
 
128
128
  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
129
 
130
- After merging the release PR, `changeset-tag --since <merge-commit>^1 --push` pushes tags only for packages changed by that release. 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. Tag pushes need Git write credentials; GitHub Releases need authenticated `gh`.
130
+ 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
131
 
132
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.
133
133
 
@@ -9,6 +9,7 @@ export interface ChangesetVersionInput extends ChangesetRepository {
9
9
  }
10
10
  export interface ChangesetTagInput extends ChangesetRepository {
11
11
  dryRun?: boolean;
12
+ untagged?: boolean;
12
13
  push?: boolean;
13
14
  githubRelease?: boolean;
14
15
  since?: string;
@@ -18,6 +19,7 @@ export interface ChangesetTagResult {
18
19
  package: string;
19
20
  version: string;
20
21
  tag: string;
22
+ exists: boolean;
21
23
  created: boolean;
22
24
  notes: string;
23
25
  }>;
@@ -157,6 +157,9 @@ export async function tagChangesets(input) {
157
157
  if (input.githubRelease && !input.push) {
158
158
  throw new ValidationError('GitHub Releases require pushing the tags.');
159
159
  }
160
+ if (input.untagged && input.push) {
161
+ throw new ValidationError('Listing untagged releases cannot be combined with pushing tags.');
162
+ }
160
163
  const context = await getReleaseContext(input);
161
164
  if (!context) {
162
165
  throw new ValidationError('No wp-release config found. Add wp-release.config.js or pass --config <path>.');
@@ -165,6 +168,7 @@ export async function tagChangesets(input) {
165
168
  ? (await changedPackagesSince(input.cwd, context.packages, input.since)).packages
166
169
  : context.packages;
167
170
  const results = [];
171
+ const dryRun = input.dryRun || input.untagged;
168
172
  const tagFormat = context.config.tagFormat ?? '{path}/{version}';
169
173
  for (const pkg of packages) {
170
174
  const version = 'git-tag' === pkg.versionConfig.from.kind
@@ -173,33 +177,35 @@ export async function tagChangesets(input) {
173
177
  const tag = formatTag(tagFormat, pkg, version);
174
178
  const exists = await gitTagExists(input.cwd, tag);
175
179
  const notes = await readReleaseNotes(pkg.directory, version);
176
- if (!exists && !input.dryRun) {
180
+ if (!exists && !dryRun) {
177
181
  await git(input.cwd, ['tag', tag]);
178
182
  }
179
183
  results.push({
180
184
  package: pkg.name,
181
185
  version,
182
186
  tag,
183
- created: !exists && !input.dryRun,
187
+ exists,
188
+ created: !exists && !dryRun,
184
189
  notes,
185
190
  });
186
191
  }
187
- if (input.push && !input.dryRun) {
188
- const created = results.filter((result) => result.created);
189
- if (created.length) {
190
- await git(input.cwd, [
191
- 'push',
192
- 'origin',
193
- ...created.map((item) => item.tag),
194
- ]);
192
+ // Push one tag at a time because GitHub doesn't create push events for
193
+ // tags when more than three are pushed at once.
194
+ if (input.push && !dryRun) {
195
+ for (const item of results.filter((result) => result.created)) {
196
+ await git(input.cwd, ['push', 'origin', item.tag]);
195
197
  }
196
198
  }
197
- if (input.githubRelease && !input.dryRun) {
199
+ if (input.githubRelease && !dryRun) {
198
200
  for (const release of results) {
199
201
  await createGitHubRelease(input.cwd, release);
200
202
  }
201
203
  }
202
- return { tags: results };
204
+ return {
205
+ tags: input.untagged
206
+ ? results.filter((result) => !result.exists)
207
+ : results,
208
+ };
203
209
  }
204
210
  async function createGitHubRelease(cwd, release) {
205
211
  try {
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { appendFileSync } from 'node:fs';
1
2
  import { parseArgs } from 'node:util';
2
3
  import zip from "./commands/zip.js";
3
4
  import checksums from "./commands/checksums.js";
@@ -36,6 +37,7 @@ const globalArgs = {
36
37
  'log-format': { type: 'string' },
37
38
  ci: { type: 'boolean' },
38
39
  json: { type: 'boolean' },
40
+ 'github-output': { type: 'boolean' },
39
41
  config: { type: 'string' },
40
42
  };
41
43
  const globalOptions = `Global options:
@@ -47,6 +49,8 @@ const globalOptions = `Global options:
47
49
  when GITHUB_ACTIONS=true, otherwise text)
48
50
  --ci Same as --log-format github
49
51
  --json Print the result as JSON
52
+ --github-output Set the result as JSON to the "result" output of
53
+ the GitHub Actions step
50
54
  --config <path> Path to a JavaScript release config file`;
51
55
  /**
52
56
  * Get the top-level help text listing all commands.
@@ -143,6 +147,21 @@ function createLogWriter(values, streams) {
143
147
  }
144
148
  return create(streams);
145
149
  }
150
+ /**
151
+ * Get the GitHub Actions step output file for --github-output, checked before
152
+ * the command runs.
153
+ *
154
+ * @param values Parsed option values.
155
+ * @return Output file path, or undefined without --github-output.
156
+ * @throws {ValidationError} For --github-output without GITHUB_OUTPUT.
157
+ */
158
+ function getGitHubOutputFile(values) {
159
+ const file = process.env.GITHUB_OUTPUT;
160
+ if (getBoolean(values, 'github-output') && !file) {
161
+ throw new ValidationError('--github-output requires the GITHUB_OUTPUT environment variable of GitHub Actions.');
162
+ }
163
+ return getBoolean(values, 'github-output') ? file : undefined;
164
+ }
146
165
  /**
147
166
  * Parse the arguments of a command and run it.
148
167
  *
@@ -169,6 +188,7 @@ async function runCommand(command, args, io) {
169
188
  if ('' === configFile?.trim()) {
170
189
  throw new ValidationError('--config <path> must not be empty.');
171
190
  }
191
+ const githubOutputFile = getGitHubOutputFile(values);
172
192
  logger = new Logger({
173
193
  writer: createLogWriter(values, { stdout, stderr }),
174
194
  quiet: getBoolean(values, 'quiet') || getBoolean(values, 'json'),
@@ -184,6 +204,10 @@ async function runCommand(command, args, io) {
184
204
  if (getBoolean(values, 'json')) {
185
205
  stdout.write(JSON.stringify(result, null, '\t') + '\n');
186
206
  }
207
+ if (githubOutputFile) {
208
+ // Compact JSON has no line breaks, so it fits a single output line.
209
+ appendFileSync(githubOutputFile, `result=${JSON.stringify(result)}\n`);
210
+ }
187
211
  return exitCode;
188
212
  }
189
213
  catch (error) {
@@ -91,6 +91,9 @@ export declare const changesetTagArgs: {
91
91
  readonly 'dry-run': {
92
92
  readonly type: "boolean";
93
93
  };
94
+ readonly untagged: {
95
+ readonly type: "boolean";
96
+ };
94
97
  readonly 'github-release': {
95
98
  readonly type: "boolean";
96
99
  };
@@ -102,6 +105,7 @@ export declare const changesetTagArgs: {
102
105
  export interface ChangesetTagArgs {
103
106
  push: boolean;
104
107
  dryRun: boolean;
108
+ untagged: boolean;
105
109
  githubRelease: boolean;
106
110
  since?: string;
107
111
  }
@@ -113,6 +113,7 @@ export function parseChangesetVersionArgs(values) {
113
113
  export const changesetTagArgs = {
114
114
  push: { type: 'boolean' },
115
115
  'dry-run': { type: 'boolean' },
116
+ untagged: { type: 'boolean' },
116
117
  'github-release': { type: 'boolean' },
117
118
  since: { type: 'string' },
118
119
  };
@@ -126,6 +127,7 @@ export function parseChangesetTagArgs(values) {
126
127
  const args = {
127
128
  push: getBoolean(values, 'push'),
128
129
  dryRun: getBoolean(values, 'dry-run'),
130
+ untagged: getBoolean(values, 'untagged'),
129
131
  githubRelease: getBoolean(values, 'github-release'),
130
132
  since: getString(values, 'since'),
131
133
  };
@@ -135,5 +137,8 @@ export function parseChangesetTagArgs(values) {
135
137
  if (args.githubRelease && !args.push) {
136
138
  throw new ValidationError('--github-release requires --push.');
137
139
  }
140
+ if (args.untagged && args.push) {
141
+ throw new ValidationError('--untagged cannot be combined with --push.');
142
+ }
138
143
  return args;
139
144
  }
@@ -10,6 +10,9 @@ declare const _default: {
10
10
  readonly 'dry-run': {
11
11
  readonly type: "boolean";
12
12
  };
13
+ readonly untagged: {
14
+ readonly type: "boolean";
15
+ };
13
16
  readonly 'github-release': {
14
17
  readonly type: "boolean";
15
18
  };
@@ -2,17 +2,20 @@ import { tagChangesets } from "../changeset-release.js";
2
2
  import { parseChangesetTagArgs, changesetTagArgs } from "./changeset-args.js";
3
3
  export default {
4
4
  name: 'changeset-tag',
5
- usage: 'changeset-tag [--dry-run] [--since <ref>] [--push] [--github-release]',
5
+ usage: 'changeset-tag [--dry-run] [--untagged] [--since <ref>] [--push] [--github-release]',
6
6
  summary: 'Create missing release tags for configured package versions.',
7
7
  help: `Options:
8
8
  --dry-run List missing tags without creating them
9
+ --untagged List only releases without a tag, without creating tags
9
10
  --since <ref> Tag packages changed since this git ref
10
11
  --push Push newly created tags to origin
11
12
  --github-release Create a GitHub Release for each tag
12
13
 
13
14
  Tags are created only when absent, and GitHub Releases are created only when
14
15
  missing, so rerunning after a partial failure is safe. --github-release also
15
- requires --push.`,
16
+ requires --push. With --json or --github-output, --untagged lists the releases
17
+ that still need a tag, for example to release each of them with a separate
18
+ workflow.`,
16
19
  options: changesetTagArgs,
17
20
  async run({ values, cwd, configFile, logger }) {
18
21
  const args = parseChangesetTagArgs(values);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpelevator/wp-release",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Package, checksum, sign, verify and version-check WordPress plugins and themes, and deploy plugins to WordPress.org SVN",
5
5
  "license": "MIT",
6
6
  "repository": {