@wpelevator/wp-release 0.4.1 → 0.6.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.
Files changed (44) hide show
  1. package/README.md +136 -6
  2. package/dist/changeset-config.d.ts +189 -0
  3. package/dist/changeset-config.js +273 -0
  4. package/dist/changeset-release.d.ts +74 -0
  5. package/dist/changeset-release.js +829 -0
  6. package/dist/changesets.d.ts +72 -0
  7. package/dist/changesets.js +103 -0
  8. package/dist/cli.d.ts +12 -5
  9. package/dist/cli.js +78 -18
  10. package/dist/commands/changeset-add.d.ts +37 -0
  11. package/dist/commands/changeset-add.js +35 -0
  12. package/dist/commands/changeset-args.d.ts +114 -0
  13. package/dist/commands/changeset-args.js +139 -0
  14. package/dist/commands/changeset-status.d.ts +43 -0
  15. package/dist/commands/changeset-status.js +65 -0
  16. package/dist/commands/changeset-tag.d.ts +25 -0
  17. package/dist/commands/changeset-tag.js +27 -0
  18. package/dist/commands/changeset-version.d.ts +38 -0
  19. package/dist/commands/changeset-version.js +39 -0
  20. package/dist/commands/checksums.js +2 -0
  21. package/dist/commands/command.d.ts +2 -0
  22. package/dist/commands/sign.js +2 -0
  23. package/dist/commands/verify.js +61 -12
  24. package/dist/commands/version-check.js +5 -2
  25. package/dist/commands/zip.js +7 -7
  26. package/dist/errors.d.ts +18 -0
  27. package/dist/errors.js +8 -0
  28. package/dist/github-actions-writer.d.ts +52 -0
  29. package/dist/github-actions-writer.js +94 -0
  30. package/dist/index.d.ts +15 -4
  31. package/dist/index.js +8 -3
  32. package/dist/logger.d.ts +88 -16
  33. package/dist/logger.js +73 -25
  34. package/dist/package-meta.d.ts +26 -0
  35. package/dist/package-meta.js +65 -10
  36. package/dist/report.d.ts +22 -0
  37. package/dist/report.js +35 -0
  38. package/dist/summary.d.ts +141 -0
  39. package/dist/summary.js +287 -0
  40. package/dist/svn-deploy.d.ts +2 -1
  41. package/dist/svn-deploy.js +8 -6
  42. package/dist/version-check.d.ts +39 -1
  43. package/dist/version-check.js +59 -5
  44. package/package.json +1 -1
@@ -0,0 +1,94 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { getWorkspacePath } from "./summary.js";
3
+ /**
4
+ * Escape the message of a workflow command, so that a multi-line message
5
+ * stays one annotation.
6
+ *
7
+ * @param text Message.
8
+ * @return Escaped message.
9
+ */
10
+ function escapeData(text) {
11
+ return text
12
+ .replaceAll('%', '%25')
13
+ .replaceAll('\r', '%0D')
14
+ .replaceAll('\n', '%0A');
15
+ }
16
+ /**
17
+ * Escape a workflow command property like a file path or title.
18
+ *
19
+ * @param text Property value.
20
+ * @return Escaped value.
21
+ */
22
+ function escapeProperty(text) {
23
+ return escapeData(text).replaceAll(':', '%3A').replaceAll(',', '%2C');
24
+ }
25
+ /**
26
+ * Writer that prints GitHub Actions workflow commands, so that notices,
27
+ * warnings and errors are annotations on the run and on the files they're
28
+ * about, and appends summaries to the job summary.
29
+ */
30
+ export class GitHubActionsWriter {
31
+ stdout;
32
+ stderr;
33
+ workspace;
34
+ summaryPath;
35
+ constructor({ stdout = process.stdout, stderr = process.stderr, workspace = process.env.GITHUB_WORKSPACE || process.cwd(), summaryPath = process.env.GITHUB_STEP_SUMMARY || null, } = {}) {
36
+ this.stdout = stdout;
37
+ this.stderr = stderr;
38
+ this.workspace = workspace;
39
+ this.summaryPath = summaryPath;
40
+ }
41
+ write({ level, message, annotation }) {
42
+ const stream = 'warning' === level || 'error' === level
43
+ ? this.stderr
44
+ : this.stdout;
45
+ stream.write('info' === level
46
+ ? `${message}\n`
47
+ : this.formatCommand(level, message, annotation));
48
+ }
49
+ startGroup(title) {
50
+ this.stdout.write(`::group::${escapeData(title)}\n`);
51
+ }
52
+ endGroup() {
53
+ this.stdout.write('::endgroup::\n');
54
+ }
55
+ summary(build) {
56
+ if (this.summaryPath) {
57
+ // A blank line keeps the Markdown of separate summaries apart.
58
+ appendFileSync(this.summaryPath, `${build({ workspace: this.workspace }).trimEnd()}\n\n`);
59
+ }
60
+ }
61
+ /**
62
+ * Format a workflow command like
63
+ * "::error file=my-plugin.php,line=3,title=Version mismatch::message".
64
+ *
65
+ * @param name Command name.
66
+ * @param message Message.
67
+ * @param annotation File, line and title of the annotation.
68
+ * @return Workflow command with a line break.
69
+ */
70
+ formatCommand(name, message, annotation = {}) {
71
+ const file = this.getAnnotationFile(annotation.file);
72
+ const properties = [
73
+ file ? `file=${escapeProperty(file)}` : '',
74
+ file && annotation.line ? `line=${annotation.line}` : '',
75
+ annotation.title
76
+ ? `title=${escapeProperty(annotation.title)}`
77
+ : '',
78
+ ]
79
+ .filter(Boolean)
80
+ .join(',');
81
+ return `::${name}${properties ? ` ${properties}` : ''}::${escapeData(message)}\n`;
82
+ }
83
+ /**
84
+ * Get the path of an annotation file relative to the workspace. GitHub
85
+ * only links files of the repository, so files outside the workspace,
86
+ * like an extracted ZIP in a temporary directory, get no file.
87
+ *
88
+ * @param file File path, absolute or relative to the working directory.
89
+ * @return Path with forward slashes, or null when it's outside the workspace.
90
+ */
91
+ getAnnotationFile(file) {
92
+ return file ? getWorkspacePath(this.workspace, file) : null;
93
+ }
94
+ }
package/dist/index.d.ts CHANGED
@@ -1,17 +1,22 @@
1
1
  export { run, execute, commands, getHelp, getCommandHelp } from './cli.ts';
2
+ export { logError, logProblems } from './report.ts';
2
3
  export { getDeployInputFromArgs } from './commands/deploy-svn.ts';
3
4
  export { ValidationError, CredentialsError } from './errors.ts';
4
- export { Logger } from './logger.ts';
5
+ export { Logger, TextWriter } from './logger.ts';
6
+ export { GitHubActionsWriter } from './github-actions-writer.ts';
5
7
  export { SVN_URL, isPrerelease, normalizeTag, isTagRef, isValidSlug, isValidVersion, getDeployOptionErrors, getDeployOptionWarnings, resolveDeployOptions, } from './deploy-options.ts';
6
8
  export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules, selectFiles, } from './file-selection.ts';
7
9
  export { FILENAME_FORMAT, formatFilename } from './filename.ts';
8
10
  export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from './checksums.ts';
9
- export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findMainPluginFile, readPackageMeta, } from './package-meta.ts';
11
+ export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findFileHeaderLine, findReadmeHeaderLine, findMainPluginFile, readPackageMeta, } from './package-meta.ts';
10
12
  export { SIGNATURE_EXTENSION, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, getPublicKey, getFileDigest, signFile, writeSignature, verifyFileSignature, } from './signing.ts';
11
13
  export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from './zip.ts';
12
14
  export { isUrl, downloadFile } from './download.ts';
13
15
  export { readZipEntries, getZipEntryErrors, extractZip } from './unzip.ts';
14
- export { GIT_TAG_FORMAT, getGitTagVersion, findConstantValues, findVersionMismatches, } from './version-check.ts';
16
+ export { GIT_TAG_FORMAT, VersionMismatchError, getGitTagVersion, findConstantValues, findConstantDefinitions, findVersionMismatches, getVersionMismatchProblems, } from './version-check.ts';
17
+ export { CHANGESET_BUMPS, parseChangeset, formatChangeset, getChangesetBump, getNextVersion, } from './changesets.ts';
18
+ export { addChangeset, getChangesetReleasePlan, getChangesetStatus, versionChangesets, tagChangesets, } from './changeset-release.ts';
19
+ export { defineConfig, packageJsonVersion, composerJsonVersion, wordpressPluginVersion, wordpressThemeVersion, wordpressStableTag, gitTagVersion, phpConstantVersion, loadChangesetConfig, } from './changeset-config.ts';
15
20
  export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from './svn.ts';
16
21
  export { syncDirectory, copyFileIfChanged } from './sync.ts';
17
22
  export { ASSET_MIME_TYPES, SvnDeploy } from './svn-deploy.ts';
@@ -21,7 +26,9 @@ export type { SignResult } from './commands/sign.ts';
21
26
  export type { ZipResult } from './commands/zip.ts';
22
27
  export type { Command, CommandContext, CommandOutcome, ParsedValues, } from './commands/command.ts';
23
28
  export type { DeployArgsInput } from './commands/deploy-svn.ts';
24
- export type { LoggerOptions, OutputStream } from './logger.ts';
29
+ export type { Annotation, LogEntry, LogLevel, LogWriter, LoggerOptions, OutputStream, SummaryBuilder, TextWriterOptions, } from './logger.ts';
30
+ export type { GitHubActionsWriterOptions } from './github-actions-writer.ts';
31
+ export type { Problem } from './errors.ts';
25
32
  export type { DeployInput, DeployOptions } from './deploy-options.ts';
26
33
  export type { FileRules, FileSelection, FileSelectionInput, } from './file-selection.ts';
27
34
  export type { FilenameValues } from './filename.ts';
@@ -30,6 +37,10 @@ export type { FindMainPluginFileInput, HeaderValues, PackageMeta, ParseFileHeade
30
37
  export type { CreateZipInput } from './zip.ts';
31
38
  export type { ExtractZipInput, ZipEntryInfo, ZipEntryType } from './unzip.ts';
32
39
  export type { FindVersionMismatchesInput, GitTagInput, VersionCheckResult, VersionMismatch, } from './version-check.ts';
40
+ export type { Changeset, ChangesetBump, ChangesetPackages, ChangesetConfig, ChangesetPackageType, ChangesetPackage, PackageRelease, ChangesetReleasePlan, } from './changesets.ts';
41
+ export type { ChangesetRepository, ChangesetVersionInput, ChangesetTagInput, ChangesetTagResult, } from './changeset-release.ts';
42
+ export type { WpReleaseConfig, ChangesetPackageGroup, PackageVersionConfig, PackageVersionSource, PackageVersionTarget, PackageIdSource, NextVersionInput, PackageJsonVersionOptions, ComposerJsonVersionOptions, WordPressVersionFileOptions, GitTagVersionOptions, PhpConstantVersionOptions, LoadChangesetConfigInput, } from './changeset-config.ts';
33
43
  export type { SvnCheckoutInput, SvnCommandInput, SvnCommandOutput, SvnCommitInput, SvnCopyInput, SvnDepth, SvnOptions, SvnPathsInput, SvnPropsetInput, SvnRunner, SvnStatusEntry, SvnStatusType, SvnUpdateInput, } from './svn.ts';
34
44
  export type { SyncDirectoryInput, SyncResult } from './sync.ts';
45
+ export type { SummaryContext } from './summary.ts';
35
46
  export type { CheckoutPath, DeployResult } from './svn-deploy.ts';
package/dist/index.js CHANGED
@@ -1,17 +1,22 @@
1
1
  export { run, execute, commands, getHelp, getCommandHelp } from "./cli.js";
2
+ export { logError, logProblems } from "./report.js";
2
3
  export { getDeployInputFromArgs } from "./commands/deploy-svn.js";
3
4
  export { ValidationError, CredentialsError } from "./errors.js";
4
- export { Logger } from "./logger.js";
5
+ export { Logger, TextWriter } from "./logger.js";
6
+ export { GitHubActionsWriter } from "./github-actions-writer.js";
5
7
  export { SVN_URL, isPrerelease, normalizeTag, isTagRef, isValidSlug, isValidVersion, getDeployOptionErrors, getDeployOptionWarnings, resolveDeployOptions, } from "./deploy-options.js";
6
8
  export { DISTIGNORE_FILENAME, parseExclude, resolveFileSelection, loadFileRules, selectFiles, } from "./file-selection.js";
7
9
  export { FILENAME_FORMAT, formatFilename } from "./filename.js";
8
10
  export { CHECKSUMS_EXTENSION, CHECKSUM_ALGORITHMS, hashFile, hashFiles, createChecksums, writeChecksums, readChecksums, parseChecksums, compareChecksums, verifyChecksums, } from "./checksums.js";
9
- export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findMainPluginFile, readPackageMeta, } from "./package-meta.js";
11
+ export { HEADER_BYTES, PLUGIN_HEADERS, THEME_HEADERS, README_HEADERS, README_FILENAMES, parseFileHeaders, parseReadmeHeaders, readFileStart, findFileHeaderLine, findReadmeHeaderLine, findMainPluginFile, readPackageMeta, } from "./package-meta.js";
10
12
  export { SIGNATURE_EXTENSION, SIGNING_KEY_ENV, parseSigningKey, parsePublicKey, getPublicKey, getFileDigest, signFile, writeSignature, verifyFileSignature, } from "./signing.js";
11
13
  export { ZIP_MTIME, ZIP_FILE_MODE, createZip } from "./zip.js";
12
14
  export { isUrl, downloadFile } from "./download.js";
13
15
  export { readZipEntries, getZipEntryErrors, extractZip } from "./unzip.js";
14
- export { GIT_TAG_FORMAT, getGitTagVersion, findConstantValues, findVersionMismatches, } from "./version-check.js";
16
+ export { GIT_TAG_FORMAT, VersionMismatchError, getGitTagVersion, findConstantValues, findConstantDefinitions, findVersionMismatches, getVersionMismatchProblems, } from "./version-check.js";
17
+ export { CHANGESET_BUMPS, parseChangeset, formatChangeset, getChangesetBump, getNextVersion, } from "./changesets.js";
18
+ export { addChangeset, getChangesetReleasePlan, getChangesetStatus, versionChangesets, tagChangesets, } from "./changeset-release.js";
19
+ export { defineConfig, packageJsonVersion, composerJsonVersion, wordpressPluginVersion, wordpressThemeVersion, wordpressStableTag, gitTagVersion, phpConstantVersion, loadChangesetConfig, } from "./changeset-config.js";
15
20
  export { Svn, SvnError, createSvnRunner, parseStatus, toSvnPath, } from "./svn.js";
16
21
  export { syncDirectory, copyFileIfChanged } from "./sync.js";
17
22
  export { ASSET_MIME_TYPES, SvnDeploy } from "./svn-deploy.js";
package/dist/logger.d.ts CHANGED
@@ -1,36 +1,108 @@
1
+ import type { SummaryContext } from './summary.ts';
1
2
  /**
2
3
  * Stream that output is written to, like process.stdout.
3
4
  */
4
5
  export interface OutputStream {
5
6
  write: (text: string) => unknown;
6
7
  }
7
- export interface LoggerOptions {
8
- /** Use GitHub Actions workflow commands. */
9
- ci?: boolean;
10
- /** Print errors only. */
11
- quiet?: boolean;
12
- /** Print debug messages. */
13
- verbose?: boolean;
8
+ /**
9
+ * Log levels, from the most to the least verbose.
10
+ */
11
+ export type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error';
12
+ /**
13
+ * File and title that a message is about, which writers like the GitHub
14
+ * Actions writer show as an annotation on the file.
15
+ */
16
+ export interface Annotation {
17
+ /** File the message is about, absolute or relative to the working directory. */
18
+ file?: string | null;
19
+ /** Line in the file, starting at 1. */
20
+ line?: number | null;
21
+ /** Annotation title. */
22
+ title?: string;
23
+ }
24
+ /**
25
+ * A message passed from the logger to its writer.
26
+ */
27
+ export interface LogEntry {
28
+ level: LogLevel;
29
+ message: string;
30
+ annotation: Annotation;
31
+ }
32
+ /**
33
+ * Builds the Markdown of a summary for the context of a writer.
34
+ */
35
+ export type SummaryBuilder = (context: SummaryContext) => string;
36
+ /**
37
+ * Destination of the log, like a terminal or a CI system. The logger decides
38
+ * which messages to write, and the writer decides how they look.
39
+ */
40
+ export interface LogWriter {
41
+ /** Write a message. */
42
+ write: (entry: LogEntry) => void;
43
+ /** Start a collapsible group of messages. */
44
+ startGroup: (title: string) => void;
45
+ /** End the group started last. */
46
+ endGroup: () => void;
47
+ /** Add to the summary of the run, for writers that have one. */
48
+ summary: (build: SummaryBuilder) => void;
49
+ }
50
+ export interface TextWriterOptions {
14
51
  /** Stream for regular output. */
15
52
  stdout?: OutputStream;
16
53
  /** Stream for warnings and errors. */
17
54
  stderr?: OutputStream;
18
55
  }
19
56
  /**
20
- * Logger that prints plain text locally and GitHub Actions workflow commands in CI.
57
+ * Writer that prints plain text, for terminals and CI systems without
58
+ * special log formats.
59
+ */
60
+ export declare class TextWriter implements LogWriter {
61
+ readonly stdout: OutputStream;
62
+ readonly stderr: OutputStream;
63
+ constructor({ stdout, stderr, }?: TextWriterOptions);
64
+ write({ level, message }: LogEntry): void;
65
+ startGroup(title: string): void;
66
+ endGroup(): void;
67
+ summary(): void;
68
+ }
69
+ export interface LoggerOptions {
70
+ /** Writer for the messages, a TextWriter by default. */
71
+ writer?: LogWriter;
72
+ /** Print errors and warnings only. */
73
+ quiet?: boolean;
74
+ /** Print debug messages. */
75
+ verbose?: boolean;
76
+ }
77
+ /**
78
+ * Logger that filters messages by level and passes them to a writer, which
79
+ * formats them for a terminal or a CI system.
21
80
  */
22
81
  export declare class Logger {
23
- readonly ci: boolean;
82
+ readonly writer: LogWriter;
24
83
  readonly quiet: boolean;
25
84
  readonly verbose: boolean;
26
- readonly stdout: OutputStream;
27
- readonly stderr: OutputStream;
28
- constructor({ ci, quiet, verbose, stdout, stderr, }?: LoggerOptions);
29
- info(message: string): void;
85
+ constructor({ writer, quiet, verbose, }?: LoggerOptions);
86
+ /**
87
+ * Write a message unless its level is filtered out.
88
+ *
89
+ * @param level Log level.
90
+ * @param message Message.
91
+ * @param annotation File, line and title the message is about.
92
+ */
93
+ log(level: LogLevel, message: string, annotation?: Annotation): void;
30
94
  debug(message: string): void;
31
- notice(message: string): void;
32
- warning(message: string): void;
33
- error(message: string): void;
95
+ info(message: string): void;
96
+ notice(message: string, annotation?: Annotation): void;
97
+ warning(message: string, annotation?: Annotation): void;
98
+ error(message: string, annotation?: Annotation): void;
99
+ /**
100
+ * Add to the summary of the run. Only writers with a summary, like the
101
+ * GitHub Actions job summary, call the builder.
102
+ *
103
+ * @param build Builds the summary Markdown.
104
+ */
105
+ summary(build: SummaryBuilder): void;
34
106
  /**
35
107
  * Run a callback inside a collapsible log group.
36
108
  *
package/dist/logger.js CHANGED
@@ -1,39 +1,85 @@
1
1
  /**
2
- * Logger that prints plain text locally and GitHub Actions workflow commands in CI.
2
+ * Writer that prints plain text, for terminals and CI systems without
3
+ * special log formats.
4
+ */
5
+ export class TextWriter {
6
+ stdout;
7
+ stderr;
8
+ constructor({ stdout = process.stdout, stderr = process.stderr, } = {}) {
9
+ this.stdout = stdout;
10
+ this.stderr = stderr;
11
+ }
12
+ write({ level, message }) {
13
+ const prefixes = {
14
+ warning: 'Warning: ',
15
+ error: 'Error: ',
16
+ };
17
+ const stream = 'warning' === level || 'error' === level
18
+ ? this.stderr
19
+ : this.stdout;
20
+ stream.write(`${prefixes[level] ?? ''}${message}\n`);
21
+ }
22
+ startGroup(title) {
23
+ this.stdout.write(`== ${title}\n`);
24
+ }
25
+ endGroup() { }
26
+ summary() { }
27
+ }
28
+ /**
29
+ * Logger that filters messages by level and passes them to a writer, which
30
+ * formats them for a terminal or a CI system.
3
31
  */
4
32
  export class Logger {
5
- ci;
33
+ writer;
6
34
  quiet;
7
35
  verbose;
8
- stdout;
9
- stderr;
10
- constructor({ ci = 'true' === process.env.GITHUB_ACTIONS, quiet = false, verbose = false, stdout = process.stdout, stderr = process.stderr, } = {}) {
11
- this.ci = ci;
36
+ constructor({ writer = new TextWriter(), quiet = false, verbose = false, } = {}) {
37
+ this.writer = writer;
12
38
  this.quiet = quiet;
13
39
  this.verbose = verbose;
14
- this.stdout = stdout;
15
- this.stderr = stderr;
16
40
  }
17
- info(message) {
18
- if (!this.quiet) {
19
- this.stdout.write(`${message}\n`);
41
+ /**
42
+ * Write a message unless its level is filtered out.
43
+ *
44
+ * @param level Log level.
45
+ * @param message Message.
46
+ * @param annotation File, line and title the message is about.
47
+ */
48
+ log(level, message, annotation = {}) {
49
+ const shown = {
50
+ debug: this.verbose && !this.quiet,
51
+ info: !this.quiet,
52
+ notice: !this.quiet,
53
+ warning: true,
54
+ error: true,
55
+ }[level];
56
+ if (shown) {
57
+ this.writer.write({ level, message, annotation });
20
58
  }
21
59
  }
22
60
  debug(message) {
23
- if (this.verbose && !this.quiet) {
24
- this.stdout.write(this.ci ? `::debug::${message}\n` : `${message}\n`);
25
- }
61
+ this.log('debug', message);
26
62
  }
27
- notice(message) {
28
- if (!this.quiet) {
29
- this.stdout.write(this.ci ? `::notice::${message}\n` : `${message}\n`);
30
- }
63
+ info(message) {
64
+ this.log('info', message);
31
65
  }
32
- warning(message) {
33
- this.stderr.write(this.ci ? `::warning::${message}\n` : `Warning: ${message}\n`);
66
+ notice(message, annotation = {}) {
67
+ this.log('notice', message, annotation);
34
68
  }
35
- error(message) {
36
- this.stderr.write(this.ci ? `::error::${message}\n` : `Error: ${message}\n`);
69
+ warning(message, annotation = {}) {
70
+ this.log('warning', message, annotation);
71
+ }
72
+ error(message, annotation = {}) {
73
+ this.log('error', message, annotation);
74
+ }
75
+ /**
76
+ * Add to the summary of the run. Only writers with a summary, like the
77
+ * GitHub Actions job summary, call the builder.
78
+ *
79
+ * @param build Builds the summary Markdown.
80
+ */
81
+ summary(build) {
82
+ this.writer.summary(build);
37
83
  }
38
84
  /**
39
85
  * Run a callback inside a collapsible log group.
@@ -43,13 +89,15 @@ export class Logger {
43
89
  * @return Callback result.
44
90
  */
45
91
  async group(title, callback) {
46
- this.info(this.ci ? `::group::${title}` : `== ${title}`);
92
+ if (!this.quiet) {
93
+ this.writer.startGroup(title);
94
+ }
47
95
  try {
48
96
  return await callback();
49
97
  }
50
98
  finally {
51
- if (this.ci) {
52
- this.info('::endgroup::');
99
+ if (!this.quiet) {
100
+ this.writer.endGroup();
53
101
  }
54
102
  }
55
103
  }
@@ -76,6 +76,23 @@ export interface ReadPackageMetaInput {
76
76
  * @return Non-empty header values.
77
77
  */
78
78
  export declare function parseFileHeaders(input: ParseFileHeadersInput): HeaderValues;
79
+ /**
80
+ * Find the line of a file header like "Version: 1.2.0", matched the same way
81
+ * as parseFileHeaders().
82
+ *
83
+ * @param contents File contents.
84
+ * @param header Header name.
85
+ * @return Line number starting at 1, or null when the header isn't there.
86
+ */
87
+ export declare function findFileHeaderLine(contents: string, header: string): number | null;
88
+ /**
89
+ * Get the line number of a position in a text.
90
+ *
91
+ * @param contents Text.
92
+ * @param index Character position.
93
+ * @return Line number starting at 1.
94
+ */
95
+ export declare function getLineNumber(contents: string, index: number): number;
79
96
  /**
80
97
  * Parse the headers of a readme.txt or readme.md, the "Key: value" lines
81
98
  * between the title and the first section. Markdown emphasis around the
@@ -85,6 +102,15 @@ export declare function parseFileHeaders(input: ParseFileHeadersInput): HeaderVa
85
102
  * @return Non-empty header values keyed by the names in README_HEADERS.
86
103
  */
87
104
  export declare function parseReadmeHeaders(contents: string): HeaderValues;
105
+ /**
106
+ * Find the line of a readme header like "Stable tag: 1.2.0", the line that
107
+ * parseReadmeHeaders() reads the value from.
108
+ *
109
+ * @param contents Readme contents.
110
+ * @param header Header name from README_HEADERS.
111
+ * @return Line number starting at 1, or null when the header isn't there.
112
+ */
113
+ export declare function findReadmeHeaderLine(contents: string, header: string): number | null;
88
114
  /**
89
115
  * Read the start of a file, where WordPress looks for the headers.
90
116
  *
@@ -82,11 +82,36 @@ export function parseFileHeaders(input) {
82
82
  ]);
83
83
  return Object.fromEntries(entries.filter(([, value]) => value));
84
84
  }
85
+ function getFileHeaderPattern(header) {
86
+ return new RegExp(`^(?:[ \\t]*<\\?php)?[ \\t/*#@]*${escapeRegExp(header)}:(.*)$`, 'mi');
87
+ }
85
88
  function matchFileHeader(contents, header) {
86
- const pattern = new RegExp(`^(?:[ \\t]*<\\?php)?[ \\t/*#@]*${escapeRegExp(header)}:(.*)$`, 'mi');
87
- const value = pattern.exec(contents)?.[1] ?? '';
89
+ const value = getFileHeaderPattern(header).exec(contents)?.[1] ?? '';
88
90
  return value.replace(/\s*(?:\*\/|\?>).*/, '').trim();
89
91
  }
92
+ /**
93
+ * Find the line of a file header like "Version: 1.2.0", matched the same way
94
+ * as parseFileHeaders().
95
+ *
96
+ * @param contents File contents.
97
+ * @param header Header name.
98
+ * @return Line number starting at 1, or null when the header isn't there.
99
+ */
100
+ export function findFileHeaderLine(contents, header) {
101
+ const normalized = contents.replace(/\r\n?/g, '\n');
102
+ const match = getFileHeaderPattern(header).exec(normalized);
103
+ return match ? getLineNumber(normalized, match.index) : null;
104
+ }
105
+ /**
106
+ * Get the line number of a position in a text.
107
+ *
108
+ * @param contents Text.
109
+ * @param index Character position.
110
+ * @return Line number starting at 1.
111
+ */
112
+ export function getLineNumber(contents, index) {
113
+ return contents.slice(0, index).split('\n').length;
114
+ }
90
115
  function escapeRegExp(text) {
91
116
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
117
  }
@@ -99,19 +124,49 @@ function escapeRegExp(text) {
99
124
  * @return Non-empty header values keyed by the names in README_HEADERS.
100
125
  */
101
126
  export function parseReadmeHeaders(contents) {
102
- const lines = contents.replace(/\r\n?/g, '\n').split('\n');
103
- const firstSection = lines.findIndex((line, index) => index > 0 && /^\s*(==|##)/.test(line));
104
- const headerLines = lines.slice(0, -1 === firstSection ? lines.length : firstSection);
105
127
  const values = {};
106
- headerLines.forEach((line) => {
107
- const match = /^[\s*_-]*([^:*_]+?)[*_]*\s*:[*_]*\s*(.*?)\s*$/.exec(line);
108
- const header = README_HEADERS.find((name) => name.toLowerCase() === match?.[1]?.toLowerCase());
109
- if (header && match?.[2] && !Object.hasOwn(values, header)) {
110
- values[header] = match[2];
128
+ getReadmeHeaderLines(contents).forEach((line) => {
129
+ const match = matchReadmeHeader(line);
130
+ if (match && !Object.hasOwn(values, match.header)) {
131
+ values[match.header] = match.value;
111
132
  }
112
133
  });
113
134
  return values;
114
135
  }
136
+ /**
137
+ * Find the line of a readme header like "Stable tag: 1.2.0", the line that
138
+ * parseReadmeHeaders() reads the value from.
139
+ *
140
+ * @param contents Readme contents.
141
+ * @param header Header name from README_HEADERS.
142
+ * @return Line number starting at 1, or null when the header isn't there.
143
+ */
144
+ export function findReadmeHeaderLine(contents, header) {
145
+ const index = getReadmeHeaderLines(contents).findIndex((line) => header === matchReadmeHeader(line)?.header);
146
+ return -1 === index ? null : index + 1;
147
+ }
148
+ /**
149
+ * Get the readme lines before the first section, where the headers are.
150
+ *
151
+ * @param contents Readme contents.
152
+ * @return Lines from the first line.
153
+ */
154
+ function getReadmeHeaderLines(contents) {
155
+ const lines = contents.replace(/\r\n?/g, '\n').split('\n');
156
+ const firstSection = lines.findIndex((line, index) => index > 0 && /^\s*(==|##)/.test(line));
157
+ return lines.slice(0, -1 === firstSection ? lines.length : firstSection);
158
+ }
159
+ /**
160
+ * Match a readme header line. Markdown emphasis around the name is ignored.
161
+ *
162
+ * @param line Readme line.
163
+ * @return Header name from README_HEADERS and its value, or null when the line isn't a non-empty header.
164
+ */
165
+ function matchReadmeHeader(line) {
166
+ const match = /^[\s*_-]*([^:*_]+?)[*_]*\s*:[*_]*\s*(.*?)\s*$/.exec(line);
167
+ const header = README_HEADERS.find((name) => name.toLowerCase() === match?.[1]?.toLowerCase());
168
+ return header && match?.[2] ? { header, value: match[2] } : null;
169
+ }
115
170
  /**
116
171
  * Read the start of a file, where WordPress looks for the headers.
117
172
  *
@@ -0,0 +1,22 @@
1
+ import type { Problem } from './errors.ts';
2
+ import type { Logger } from './logger.ts';
3
+ /**
4
+ * Log each problem as an error on its file and line, and list them in the
5
+ * summary of the run.
6
+ *
7
+ * @param logger Logger.
8
+ * @param title What failed, like the command name, for the summary.
9
+ * @param problems Problems.
10
+ * @param description Text above the problems in the summary, if any.
11
+ */
12
+ export declare function logProblems(logger: Logger, title: string, problems: Problem[], description?: string): void;
13
+ /**
14
+ * Log an error thrown while running a command, and add it to the summary of
15
+ * the run. A ValidationError with problems is logged problem by problem, so
16
+ * that each one is reported on its own file and line.
17
+ *
18
+ * @param logger Logger.
19
+ * @param error Thrown error.
20
+ * @param title What failed, like the command name, for the summary.
21
+ */
22
+ export declare function logError(logger: Logger, error: unknown, title?: string): void;
package/dist/report.js ADDED
@@ -0,0 +1,35 @@
1
+ import { ValidationError } from "./errors.js";
2
+ import { getErrorSummary, getProblemsSummary } from "./summary.js";
3
+ /**
4
+ * Log each problem as an error on its file and line, and list them in the
5
+ * summary of the run.
6
+ *
7
+ * @param logger Logger.
8
+ * @param title What failed, like the command name, for the summary.
9
+ * @param problems Problems.
10
+ * @param description Text above the problems in the summary, if any.
11
+ */
12
+ export function logProblems(logger, title, problems, description = '') {
13
+ problems.forEach(({ message, ...annotation }) => logger.error(message, annotation));
14
+ logger.summary((context) => getProblemsSummary(context, title, problems, description));
15
+ }
16
+ /**
17
+ * Log an error thrown while running a command, and add it to the summary of
18
+ * the run. A ValidationError with problems is logged problem by problem, so
19
+ * that each one is reported on its own file and line.
20
+ *
21
+ * @param logger Logger.
22
+ * @param error Thrown error.
23
+ * @param title What failed, like the command name, for the summary.
24
+ */
25
+ export function logError(logger, error, title = 'wp-release') {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ const problems = error instanceof ValidationError ? error.problems : [];
28
+ if (problems.length) {
29
+ logProblems(logger, title, problems);
30
+ }
31
+ else {
32
+ logger.error(message);
33
+ logger.summary(() => getErrorSummary(title, message));
34
+ }
35
+ }