vouchington-tooling 0.46.0 → 0.47.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.
@@ -12,9 +12,9 @@ export type PullRequestProvenance = {
12
12
  };
13
13
  /**
14
14
  * The pull request body: the Summary, Impact, Related issues and Details sections followed by the
15
- * provenance lines. With `noSourceIssue` the Related issues section holds `noSourceStatement`
16
- * (default {@link defaultNoSourceStatement}) instead of `- None`. Pure; the caller supplies the
17
- * device and worktree facts.
15
+ * provenance lines. With `noSourceIssue` the Related issues section ends with `noSourceStatement`
16
+ * (default {@link defaultNoSourceStatement}), after any `Refs` lines, instead of `- None`. Pure;
17
+ * the caller supplies the device and worktree facts.
18
18
  */
19
19
  export declare function renderPullRequestBody(input: PullRequestInput, provenance: PullRequestProvenance, noSourceStatement?: string): string;
20
20
  /**
@@ -1,7 +1,9 @@
1
1
  import { basename } from 'node:path';
2
+ import { formatReferenceKey } from "../gh-cli/closing-refs.mjs";
2
3
  function relatedLine(issue) {
3
4
  const verb = issue.relation === 'closes' ? 'Closes' : 'Refs';
4
- return `- ${verb} #${issue.number} — ${issue.reason}`;
5
+ const [owner, repo] = issue.repository?.split('/') ?? [];
6
+ return `- ${verb} ${formatReferenceKey({ number: issue.number, owner, repo })} — ${issue.reason}`;
5
7
  }
6
8
  /** The default no-source statement; a policy `renderNoSourceIssue` export may replace it. */
7
9
  function defaultNoSourceStatement(reason) {
@@ -9,18 +11,16 @@ function defaultNoSourceStatement(reason) {
9
11
  }
10
12
  /**
11
13
  * The pull request body: the Summary, Impact, Related issues and Details sections followed by the
12
- * provenance lines. With `noSourceIssue` the Related issues section holds `noSourceStatement`
13
- * (default {@link defaultNoSourceStatement}) instead of `- None`. Pure; the caller supplies the
14
- * device and worktree facts.
14
+ * provenance lines. With `noSourceIssue` the Related issues section ends with `noSourceStatement`
15
+ * (default {@link defaultNoSourceStatement}), after any `Refs` lines, instead of `- None`. Pure;
16
+ * the caller supplies the device and worktree facts.
15
17
  */
16
18
  export function renderPullRequestBody(input, provenance, noSourceStatement) {
17
- const related = input.relatedIssues.length
18
- ? input.relatedIssues.map(relatedLine)
19
- : [
20
- input.noSourceIssue === undefined
21
- ? '- None'
22
- : (noSourceStatement ?? defaultNoSourceStatement(input.noSourceIssue.reason)),
23
- ];
19
+ const related = input.relatedIssues.map(relatedLine);
20
+ if (input.noSourceIssue !== undefined)
21
+ related.push(noSourceStatement ?? defaultNoSourceStatement(input.noSourceIssue.reason));
22
+ if (!related.length)
23
+ related.push('- None');
24
24
  return [
25
25
  '## Summary',
26
26
  '',
@@ -4,13 +4,15 @@ export type RelatedIssue = {
4
4
  number: number;
5
5
  relation: 'closes' | 'refs';
6
6
  reason: string;
7
+ /** `owner/name` of a cross-repository issue (rendered lowercase); absent for this repository. */
8
+ repository?: string;
7
9
  };
8
10
  export type PullRequestInput = {
9
11
  title: string;
10
12
  summary: string;
11
13
  impact: string;
12
14
  relatedIssues: RelatedIssue[];
13
- /** Set instead of relatedIssues when the pull request has no source issue. */
15
+ /** Declares no source issue; relatedIssues beside it must all be `refs`. */
14
16
  noSourceIssue?: {
15
17
  reason: string;
16
18
  };
@@ -1,3 +1,4 @@
1
+ import { hasClosingIssueReference } from "../gh-cli/closing-refs.mjs";
1
2
  import { optionalString, rejectUnknown, requireObject, requiredChoice, requiredString, requiredStringArray, } from "./args.mjs";
2
3
  import { trustedMode } from "./pr-mode.mjs";
3
4
  const PR_MODES = ['interactive', 'autonomous'];
@@ -9,17 +10,30 @@ export function positiveInteger(value, label) {
9
10
  throw new Error(`${label} must be a positive integer`);
10
11
  return value;
11
12
  }
13
+ // The owner is a GitHub login: alphanumerics and single hyphens, at most 39 characters.
14
+ const REPOSITORY_RE = /^(?=[^/]{1,39}\/)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\/[\w.-]+$/;
15
+ function readRepository(issue, label) {
16
+ if (issue.repository === undefined)
17
+ return undefined;
18
+ // Not lowercased here: redaction runs on this value first, and rendering lowercases it.
19
+ const value = singleLine(issue, 'repository');
20
+ if (!REPOSITORY_RE.test(value))
21
+ throw new Error(`${label} must be a single owner/name`);
22
+ return value;
23
+ }
12
24
  function readRelatedIssues(args) {
13
25
  const value = args.relatedIssues;
14
26
  if (!Array.isArray(value))
15
27
  throw new Error('relatedIssues is required and must be an array');
16
28
  return value.map((item, index) => {
17
29
  const issue = requireObject(item);
18
- rejectUnknown(issue, ['number', 'relation', 'reason']);
30
+ rejectUnknown(issue, ['number', 'relation', 'reason', 'repository']);
31
+ const repository = readRepository(issue, `relatedIssues[${index}].repository`);
19
32
  return {
20
33
  number: positiveInteger(issue.number, `relatedIssues[${index}].number`),
21
34
  relation: requiredChoice(issue, 'relation', RELATIONS),
22
35
  reason: requiredString(issue, 'reason'),
36
+ ...(repository === undefined ? {} : { repository }),
23
37
  };
24
38
  });
25
39
  }
@@ -28,8 +42,12 @@ function readNoSourceIssue(args, related) {
28
42
  return undefined;
29
43
  const value = requireObject(args.noSourceIssue);
30
44
  rejectUnknown(value, ['reason']);
31
- if (related.length)
32
- throw new Error('noSourceIssue and a non-empty relatedIssues are mutually exclusive');
45
+ if (related.some((issue) => issue.relation === 'closes'))
46
+ throw new Error('noSourceIssue cannot be combined with a relatedIssues entry that closes');
47
+ related.forEach((issue, index) => {
48
+ if (hasClosingIssueReference(issue.reason))
49
+ throw new Error(`relatedIssues[${index}].reason must not contain a closing reference beside noSourceIssue`);
50
+ });
33
51
  return { reason: singleLine(value, 'reason') };
34
52
  }
35
53
  function readDetails(args) {
@@ -12,14 +12,20 @@ const FIELDS = {
12
12
  impact: text('Who or what is affected, as markdown.'),
13
13
  relatedIssues: {
14
14
  type: 'array',
15
- description: 'Issues this pull request closes or refs. Empty when it has none; with no source issue ' +
16
- 'at all, give noSourceIssue as well (it is rejected beside a non-empty list).',
15
+ description: 'Issues this pull request closes or refs. Empty when it has none; with no source issue, ' +
16
+ 'give noSourceIssue as well (beside a non-empty list every entry must be refs).',
17
17
  items: {
18
18
  type: 'object',
19
19
  properties: {
20
20
  number: { type: 'integer', minimum: 1 },
21
21
  relation: { enum: ['closes', 'refs'] },
22
22
  reason: text('Why the issue is related.'),
23
+ repository: {
24
+ type: 'string',
25
+ pattern: '^(?=[^/]{1,39}/)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*/[\\w.-]+$',
26
+ description: 'Optional owner/name (lowercased) of the issue repository, rendered as ' +
27
+ 'owner/name#N; omit for an issue in this repository. Allowed for closes and refs.',
28
+ },
23
29
  },
24
30
  required: ['number', 'relation', 'reason'],
25
31
  additionalProperties: false,
@@ -27,9 +33,10 @@ const FIELDS = {
27
33
  },
28
34
  noSourceIssue: {
29
35
  type: 'object',
30
- description: 'Declares a pull request with no source issue, mutually exclusive with a non-empty ' +
31
- 'relatedIssues. Rendered inside Related issues (the policy may supply the wording) and ' +
32
- 'passed to the policy, which may allow it only in interactive mode.',
36
+ description: 'Declares a pull request with no source issue; rejected beside a closes relatedIssues ' +
37
+ 'entry, allowed beside refs-only ones. Rendered inside Related issues after any Refs ' +
38
+ 'lines (the policy may supply the wording) and passed to the policy, which may allow it ' +
39
+ 'only in interactive mode.',
33
40
  properties: { reason: text('Single-line reason there is no source issue.') },
34
41
  required: ['reason'],
35
42
  additionalProperties: false,
@@ -286,8 +286,10 @@ shape is an error, never a pass.
286
286
  retrospective tools. All three take `sessionId` (required) and `worktree` (optional), need a server
287
287
  launched inside a repository worktree (they run the checkout policy), and share this structured
288
288
  input: `title` (single line, at most 256 characters), `summary`, `impact`, `relatedIssues`
289
- (`[{ number, relation: "closes" | "refs", reason }]`, may be empty), optional `noSourceIssue`
290
- (`{ reason }`, see below), `details` (string array), `base` (default `main`), optional `agent`, optional
289
+ (`[{ number, relation: "closes" | "refs", reason, repository? }]`, may be empty; the optional
290
+ `repository` is a single `owner/name` and renders the line as lowercase `owner/name#N`), optional
291
+ `noSourceIssue` (`{ reason }`, see below), `details` (string array), `base` (default `main`),
292
+ optional `agent`, optional
291
293
  `model`, optional `mode` (`interactive` | `autonomous`) and `workspaceSetup`. `pr_update` also
292
294
  requires `pr`; `pr_validate` accepts an optional `pr`.
293
295
 
@@ -319,9 +321,11 @@ which may keep sending it.
319
321
 
320
322
  ### Pull requests with no source issue
321
323
 
322
- Pass `noSourceIssue: { reason }` (a single line) with an empty `relatedIssues`; a non-empty
323
- `relatedIssues` beside it is rejected. The Related issues section then holds the generic statement
324
- `- No source issue — <reason>` instead of `- None`. A policy may replace the statement by
324
+ Pass `noSourceIssue: { reason }` (a single line) with an empty `relatedIssues`, or with a
325
+ `relatedIssues` whose every entry is `refs`; a `closes` entry beside it is rejected. The Related
326
+ issues section then holds the generic statement `- No source issue — <reason>` instead of `- None`,
327
+ preceded by the `- Refs … — <reason>` lines when there are any (the statement is always the last
328
+ line). A policy may replace the statement by
325
329
  exporting `renderNoSourceIssue({ reason })`, returning a non-empty string (30 s). The field also
326
330
  reaches `validatePullRequest` (with the trusted `mode`, so the policy can apply an interactive-only
327
331
  rule) and `pullRequestDefaults`.
@@ -339,8 +343,9 @@ rule) and `pullRequestDefaults`.
339
343
 
340
344
  ## Related issues
341
345
 
342
- - Closes #N — <reason> (or "Refs #N"; "- None" when empty;
343
- "- No source issue — <reason>" for noSourceIssue)
346
+ - Closes #N — <reason> (or "Refs #N"; owner/name#N with `repository`; "- None" when empty;
347
+ "- No source issue — <reason>" for noSourceIssue, rendered
348
+ after any Refs lines)
344
349
 
345
350
  ## Details
346
351
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {