Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions src/components/CliReference/cli-schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { describe, expect, test } from 'vitest';
import {
type CliCommandNode,
findCommand,
GROUP_BACKLINKS,
GROUP_DESCRIPTIONS,
GROUP_LABELS,
groupDescription,
groupTopLevel,
isFlagLike,
loadCliSchema,
Expand All @@ -20,16 +23,21 @@ describe('CLI reference grouping', () => {
expect(groupNames).toEqual(schemaNames);
});

// Mirrors the API reference's TAG_DESCRIPTIONS discipline: a new top-level
// command must be given outward-facing copy, caught here rather than shipping
// a bare fallback to the storefront grid and the nav.
test('every group has an editorial label and description', () => {
for (const group of groups) {
expect(GROUP_LABELS[group.name], `GROUP_LABELS missing "${group.name}"`).toBeDefined();
expect(
GROUP_DESCRIPTIONS[group.name],
`GROUP_DESCRIPTIONS missing "${group.name}"`
).toBeDefined();
// A new command auto-derives its copy (humanizeCommand + the schema `about`),
// so it needs no hand-written entry. The remaining risk runs the other way: an
// editorial entry orphaned when a command is renamed or dropped in the schema.
// Guard that drift — the blank-copy case is impossible by construction.
test('editorial maps carry no entry for a missing command', () => {
const commands = new Set(schema.command.commands.map((c) => c.name));
const editorial: Array<[string, Record<string, unknown>]> = [
['GROUP_LABELS', GROUP_LABELS],
['GROUP_DESCRIPTIONS', GROUP_DESCRIPTIONS],
['GROUP_BACKLINKS', GROUP_BACKLINKS],
];
for (const [mapName, map] of editorial) {
for (const name of Object.keys(map)) {
expect(commands.has(name), `${mapName} has a stale entry "${name}"`).toBe(true);
}
}
});

Expand All @@ -53,6 +61,46 @@ describe('CLI reference grouping', () => {
});
});

describe('groupDescription fallback', () => {
// Every shipped group has an editorial entry, so the schema alone never
// exercises the lower tiers — drive them with a synthetic command node.
const node = (about: string | null): CliCommandNode => ({
name: 'demo',
path: ['mergify', 'demo'],
about,
longAbout: null,
usage: 'mergify demo',
aliases: [],
subcommandRequired: false,
source: 'test',
args: [],
commands: [],
});

test('prefers an editorial entry over the schema about', () => {
expect(groupDescription('queue', node('inward-facing help'))).toBe(GROUP_DESCRIPTIONS.queue);
});

test('falls back to the schema about for a command with no entry', () => {
expect(groupDescription('newcmd', node('Inspect the thing.'))).toBe('Inspect the thing.');
});

test('treats a whitespace-only editorial entry as absent', () => {
const original = GROUP_DESCRIPTIONS.queue;
GROUP_DESCRIPTIONS.queue = ' ';
try {
expect(groupDescription('queue', node('Inspect the thing.'))).toBe('Inspect the thing.');
} finally {
GROUP_DESCRIPTIONS.queue = original;
}
});

test('falls back to a generated sentence when about is blank or missing', () => {
expect(groupDescription('newcmd', node(' '))).toBe('Commands for mergify newcmd.');
expect(groupDescription('newcmd', node(null))).toBe('Commands for mergify newcmd.');
});
});

describe('flag-like options', () => {
// clap models `stack push --draft` as a zero-arg option with a value name;
// without isFlagLike it would render a bogus <CREATE_AS_DRAFT> placeholder.
Expand Down
37 changes: 31 additions & 6 deletions src/components/CliReference/cli-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ export interface CliGroup {
slug: string;
/** Outward-facing card/page heading. */
label: string;
/** Outward-facing grid/meta description, resolved via `groupDescription`. */
description: string;
/** The top-level node itself (group or lone leaf). */
node: CliCommandNode;
/** Display strings of every runnable leaf, DFS pre-order (`mergify tests show`). */
Expand All @@ -219,8 +221,8 @@ export interface CliGroup {

/**
* Editorial order for the index grid and nav — capability-first, not clap's
* registration order; maintenance (`self-update`) trails. A top-level command
* missing here still renders, appended after these.
* registration order. A top-level command missing here still renders, slotted
* among the capabilities ahead of the maintenance tail.
*/
export const GROUP_ORDER: string[] = [
'queue',
Expand All @@ -233,6 +235,12 @@ export const GROUP_ORDER: string[] = [
'completions',
];

/**
* Maintenance, not capability: de-emphasized in the grid and sorted to the end,
* so a freshly synced command lands among the capabilities, never below these.
*/
export const MAINTENANCE_GROUPS = new Set(['self-update']);

/** Card/page headings — the CLI counterpart of `TAG_LABELS`. */
export const GROUP_LABELS: Record<string, string> = {
queue: 'Merge Queue',
Expand All @@ -247,8 +255,8 @@ export const GROUP_LABELS: Record<string, string> = {

/**
* Outward-facing one-line value props for the index grid — the CLI counterpart
* of `TAG_DESCRIPTIONS`. Written for evaluators, not copied from the schema's
* inward-facing `about` strings.
* of `TAG_DESCRIPTIONS`. Editorial copy for evaluators; a command without an
* entry falls back to its schema `about` via `groupDescription`.
*/
export const GROUP_DESCRIPTIONS: Record<string, string> = {
queue: 'Pause, unpause, and inspect the merge queue from scripts and incident runbooks.',
Expand All @@ -275,6 +283,18 @@ export const GROUP_BACKLINKS: Record<string, { text: string; href: string }> = {
config: { text: 'Configuration file', href: '/configuration/file-format' },
};

/**
* Outward-facing group description: editorial copy, else the schema's `about`,
* else a generated sentence. Always non-blank, so a command renders before it
* has hand-written copy. `||` over trimmed values: a blank editorial entry falls
* through rather than rendering as empty copy.
*/
export function groupDescription(name: string, node: CliCommandNode): string {
const editorial = GROUP_DESCRIPTIONS[name]?.trim();
const about = node.about?.trim();
return editorial || about || `Commands for mergify ${name}.`;
}

/** Outward-facing label, falling back to a title-cased name for new commands. */
export function humanizeCommand(name: string): string {
return (
Expand Down Expand Up @@ -314,9 +334,13 @@ function collectLeafNodes(node: CliCommandNode): CliCommandNode[] {
*/
export function groupTopLevel(root: CliCommandNode): CliGroup[] {
const byName = new Map(root.commands.map((c) => [c.name, c]));
const known = GROUP_ORDER.filter((name) => byName.has(name));
const unknown = root.commands.map((c) => c.name).filter((name) => !GROUP_ORDER.includes(name));
// New commands slot among the capabilities, ahead of the maintenance tail.
const ordered = [
...GROUP_ORDER.filter((name) => byName.has(name)),
...root.commands.map((c) => c.name).filter((name) => !GROUP_ORDER.includes(name)),
...known.filter((name) => !MAINTENANCE_GROUPS.has(name)),
...unknown,
...known.filter((name) => MAINTENANCE_GROUPS.has(name)),
];
return ordered.map((name) => {
const node = byName.get(name) as CliCommandNode;
Expand All @@ -325,6 +349,7 @@ export function groupTopLevel(root: CliCommandNode): CliGroup[] {
name,
slug: slugifyCommand(name),
label: humanizeCommand(name),
description: groupDescription(name, node),
node,
leaves: leafNodes.map((leaf) => leaf.path.join(' ')),
deprecated: leafNodes.some(isDeprecated),
Expand Down
4 changes: 1 addition & 3 deletions src/pages/cli/[group].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import CliCommand from '~/components/CliReference/CliCommand.astro';
import {
GROUP_BACKLINKS,
GROUP_DESCRIPTIONS,
getHeadingsForCommands,
groupTopLevel,
loadCliSchema,
Expand All @@ -29,8 +28,7 @@ const backlink = GROUP_BACKLINKS[group.name];

const content = {
title: group.label,
description:
GROUP_DESCRIPTIONS[group.name] ?? group.node.about ?? `Commands for mergify ${group.name}.`,
description: group.description,
};
---

Expand Down
22 changes: 16 additions & 6 deletions src/pages/cli/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
GROUP_DESCRIPTIONS,
groupTopLevel,
loadCliSchema,
MAINTENANCE_GROUPS,
} from '~/components/CliReference/cli-schema';
import MainLayout from '~/layouts/MainLayout.astro';
import '~/styles/cli-reference.css';
Expand All @@ -12,6 +13,17 @@ import '~/styles/cli-reference.css';
const schema = loadCliSchema();
const groups = groupTopLevel(schema.command);

// Non-blocking nudge: a freshly synced command renders on auto-derived copy.
// Surface that in the build log so editorial copy still gets written, without
// failing the build.
const unedited = groups.filter((group) => !GROUP_DESCRIPTIONS[group.name]?.trim());
if (unedited.length > 0) {
console.warn(
`[cli-reference] ${unedited.length} command group(s) on fallback copy — ` +
`add a GROUP_DESCRIPTIONS entry: ${unedited.map((group) => group.name).join(', ')}`
);
}

const headings = groups.map((group) => ({
depth: 2,
slug: group.slug,
Expand All @@ -23,9 +35,9 @@ const content = {
description: 'Manage Mergify from your terminal and your CI pipelines.',
};

// The maintenance group (self-update) is de-emphasized in the grid so the
// capability cards lead the page — editorial, see GROUP_ORDER.
const isMuted = (name: string) => name === 'self-update';
// Maintenance groups are de-emphasized in the grid so the capability cards lead
// the page — editorial, see MAINTENANCE_GROUPS.
const isMuted = (name: string) => MAINTENANCE_GROUPS.has(name);
---

<MainLayout content={content} headings={headings} showMarkdownActions={false}>
Expand Down Expand Up @@ -62,9 +74,7 @@ const isMuted = (name: string) => name === 'self-update';
{group.leaves.length} command{group.leaves.length !== 1 ? 's' : ''}
</span>
</div>
<p class="cli-group-description">
{GROUP_DESCRIPTIONS[group.name] ?? group.node.about}
</p>
<p class="cli-group-description">{group.description}</p>
</a>
))
}
Expand Down
Loading