@davidvornholt/standards 0.10.0 → 0.10.2

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.
@@ -0,0 +1,136 @@
1
+ import {
2
+ inspectIgnore,
3
+ inspectRegistryReferences,
4
+ overlapsUpdateTarget,
5
+ type UpdateBlock,
6
+ type UpdateTarget,
7
+ updateTarget,
8
+ updateTargetDescription,
9
+ } from './dependabot-update';
10
+ import { isRecord } from './github-settings-parse';
11
+ import { parseYaml } from './yaml-parse';
12
+
13
+ export type ParsedDependabot = {
14
+ readonly document: Record<string, unknown>;
15
+ readonly updates: ReadonlyArray<UpdateBlock>;
16
+ readonly registries: Record<string, unknown>;
17
+ };
18
+
19
+ export type ParsedLocal = {
20
+ readonly updates: ReadonlyArray<UpdateBlock>;
21
+ readonly registries: Record<string, unknown>;
22
+ };
23
+
24
+ export type ValidatedUpdate = {
25
+ readonly block: UpdateBlock;
26
+ readonly target: UpdateTarget;
27
+ readonly label: string;
28
+ };
29
+
30
+ const inspectRegistries = (
31
+ registries: unknown,
32
+ label: string,
33
+ problems: Array<string>,
34
+ ): Record<string, unknown> => {
35
+ if (registries === undefined) {
36
+ return {};
37
+ }
38
+ if (!isRecord(registries)) {
39
+ problems.push(`${label} registries must be a mapping`);
40
+ return {};
41
+ }
42
+ for (const [name, registry] of Object.entries(registries)) {
43
+ if (!isRecord(registry)) {
44
+ problems.push(`${label} registries.${name} must be a mapping`);
45
+ }
46
+ }
47
+ return registries;
48
+ };
49
+
50
+ export const parseDependabot = (
51
+ raw: string,
52
+ label: string,
53
+ problems: Array<string>,
54
+ ): ParsedDependabot | null => {
55
+ const { value, problem } = parseYaml(raw, label);
56
+ if (problem !== null) {
57
+ problems.push(problem);
58
+ return null;
59
+ }
60
+ if (!(isRecord(value) && Array.isArray(value.updates))) {
61
+ problems.push(`${label} must define an updates list`);
62
+ return null;
63
+ }
64
+ if (!value.updates.every(isRecord)) {
65
+ problems.push(`${label} updates entries must be mappings`);
66
+ return null;
67
+ }
68
+ return {
69
+ document: value,
70
+ updates: value.updates,
71
+ registries: inspectRegistries(value.registries, label, problems),
72
+ };
73
+ };
74
+
75
+ export const parseLocal = (
76
+ raw: string,
77
+ label: string,
78
+ problems: Array<string>,
79
+ ): ParsedLocal | null => {
80
+ const { value, problem } = parseYaml(raw, label);
81
+ if (problem !== null) {
82
+ problems.push(problem);
83
+ return null;
84
+ }
85
+ if (value === null || value === undefined) {
86
+ return { updates: [], registries: {} };
87
+ }
88
+ if (!isRecord(value)) {
89
+ problems.push(`${label} must contain a YAML mapping`);
90
+ return null;
91
+ }
92
+ const unknownKeys = Object.keys(value).filter(
93
+ (key) => key !== 'updates' && key !== 'registries',
94
+ );
95
+ if (unknownKeys.length > 0) {
96
+ problems.push(
97
+ `${label} may only define "updates" and "registries"; remove: ${unknownKeys.join(', ')}`,
98
+ );
99
+ }
100
+ const updates = value.updates ?? [];
101
+ if (!(Array.isArray(updates) && updates.every(isRecord))) {
102
+ problems.push(`${label} updates must be a list of mappings`);
103
+ return null;
104
+ }
105
+ return {
106
+ updates,
107
+ registries: inspectRegistries(value.registries, label, problems),
108
+ };
109
+ };
110
+
111
+ export const validateUpdates = (
112
+ updates: ReadonlyArray<UpdateBlock>,
113
+ label: string,
114
+ problems: Array<string>,
115
+ ): ReadonlyArray<ValidatedUpdate> => {
116
+ const validated: Array<ValidatedUpdate> = [];
117
+ for (const [index, block] of updates.entries()) {
118
+ const blockLabel = `${label} updates[${index}]`;
119
+ problems.push(...inspectIgnore(block.ignore, blockLabel));
120
+ problems.push(...inspectRegistryReferences(block.registries, blockLabel));
121
+ const target = updateTarget(block, blockLabel, problems);
122
+ if (target !== null) {
123
+ validated.push({ block, target, label: blockLabel });
124
+ }
125
+ }
126
+ for (const [index, candidate] of validated.entries()) {
127
+ for (const earlier of validated.slice(0, index)) {
128
+ if (overlapsUpdateTarget(candidate.target, earlier.target)) {
129
+ problems.push(
130
+ `${candidate.label} overlaps ${earlier.label}: ${updateTargetDescription(candidate.target)}`,
131
+ );
132
+ }
133
+ }
134
+ }
135
+ return validated;
136
+ };
@@ -0,0 +1,188 @@
1
+ // Composes the generated .github/dependabot.yml from the canonical base and
2
+ // the optional repo-owned additive overlay.
3
+
4
+ import {
5
+ parseDependabot,
6
+ parseLocal,
7
+ type ValidatedUpdate,
8
+ validateUpdates,
9
+ } from './dependabot-compose-input';
10
+ import {
11
+ overlapsUpdateTarget,
12
+ sameUpdateTarget,
13
+ type UpdateBlock,
14
+ updateTargetDescription,
15
+ } from './dependabot-update';
16
+ import { emitYamlDocument } from './yaml-emit';
17
+
18
+ export const DEPENDABOT_FILE = '.github/dependabot.yml';
19
+ export const DEPENDABOT_BASE_FILE = '.github/dependabot.base.yml';
20
+ export const DEPENDABOT_LOCAL_FILE = '.github/dependabot.local.yml';
21
+
22
+ const GENERATED_HEADER = `# GENERATED FILE - do not edit. \`bun standards sync\` (or
23
+ # \`bun standards dependabot --write\`) composes ${DEPENDABOT_BASE_FILE}
24
+ # (canonical) with ${DEPENDABOT_LOCAL_FILE} (repo-owned) into this file.
25
+ `;
26
+
27
+ const MERGE_KEYS = new Set([
28
+ 'package-ecosystem',
29
+ 'directory',
30
+ 'directories',
31
+ 'target-branch',
32
+ 'ignore',
33
+ 'registries',
34
+ ]);
35
+
36
+ const mergeMatchingBlock = (
37
+ target: UpdateBlock,
38
+ local: ValidatedUpdate,
39
+ problems: Array<string>,
40
+ ): void => {
41
+ const extraKeys = Object.keys(local.block).filter(
42
+ (key) => !MERGE_KEYS.has(key),
43
+ );
44
+ if (extraKeys.length > 0) {
45
+ problems.push(
46
+ `${local.label} matches a canonical block and may only add ignore or registries entries; remove: ${extraKeys.join(', ')}`,
47
+ );
48
+ return;
49
+ }
50
+ const hasIgnore =
51
+ Array.isArray(local.block.ignore) && local.block.ignore.length > 0;
52
+ const hasRegistries =
53
+ local.block.registries === '*' ||
54
+ (Array.isArray(local.block.registries) &&
55
+ local.block.registries.length > 0);
56
+ if (!(hasIgnore || hasRegistries)) {
57
+ problems.push(
58
+ `${local.label} matches a canonical block and must add a non-empty ignore or registries list`,
59
+ );
60
+ return;
61
+ }
62
+ if (hasIgnore) {
63
+ target.ignore = [
64
+ ...((target.ignore as ReadonlyArray<unknown> | undefined) ?? []),
65
+ ...(local.block.ignore as ReadonlyArray<unknown>),
66
+ ];
67
+ }
68
+ if (hasRegistries) {
69
+ if (target.registries === '*' || local.block.registries === '*') {
70
+ target.registries = '*';
71
+ } else {
72
+ target.registries = [
73
+ ...new Set([
74
+ ...((target.registries as ReadonlyArray<string> | undefined) ?? []),
75
+ ...(local.block.registries as ReadonlyArray<string>),
76
+ ]),
77
+ ];
78
+ }
79
+ }
80
+ };
81
+
82
+ const mergeUpdates = (
83
+ baseUpdates: ReadonlyArray<ValidatedUpdate>,
84
+ localUpdates: ReadonlyArray<ValidatedUpdate>,
85
+ problems: Array<string>,
86
+ ): Array<UpdateBlock> => {
87
+ const updates = baseUpdates.map(({ block }) => ({ ...block }));
88
+ for (const localUpdate of localUpdates) {
89
+ const matchingIndex = baseUpdates.findIndex(({ target }) =>
90
+ sameUpdateTarget(target, localUpdate.target),
91
+ );
92
+ const overlapping = baseUpdates.find(({ target }) =>
93
+ overlapsUpdateTarget(target, localUpdate.target),
94
+ );
95
+ if (matchingIndex >= 0) {
96
+ const target = updates[matchingIndex];
97
+ if (target !== undefined) {
98
+ mergeMatchingBlock(target, localUpdate, problems);
99
+ }
100
+ } else if (overlapping === undefined) {
101
+ updates.push({ ...localUpdate.block });
102
+ } else {
103
+ problems.push(
104
+ `${localUpdate.label} overlaps ${overlapping.label}: ${updateTargetDescription(localUpdate.target)}`,
105
+ );
106
+ }
107
+ }
108
+ return updates;
109
+ };
110
+
111
+ const inspectRegistryReferencesExist = (
112
+ updates: ReadonlyArray<UpdateBlock>,
113
+ registryNames: ReadonlySet<string>,
114
+ problems: Array<string>,
115
+ ): void => {
116
+ for (const [index, update] of updates.entries()) {
117
+ if (Array.isArray(update.registries)) {
118
+ const missing = update.registries.filter(
119
+ (name) => name !== '*' && !registryNames.has(String(name)),
120
+ );
121
+ if (missing.length > 0) {
122
+ problems.push(
123
+ `${DEPENDABOT_FILE} updates[${index}].registries references undefined registries: ${missing.join(', ')}`,
124
+ );
125
+ }
126
+ }
127
+ }
128
+ };
129
+
130
+ export type DependabotCompose = {
131
+ readonly composed: string | null;
132
+ readonly problems: ReadonlyArray<string>;
133
+ };
134
+
135
+ export const composeDependabot = (
136
+ baseRaw: string,
137
+ localRaw: string | null,
138
+ ): DependabotCompose => {
139
+ const problems: Array<string> = [];
140
+ const base = parseDependabot(baseRaw, DEPENDABOT_BASE_FILE, problems);
141
+ const local =
142
+ localRaw === null
143
+ ? { updates: [], registries: {} }
144
+ : parseLocal(localRaw, DEPENDABOT_LOCAL_FILE, problems);
145
+ if (base === null || local === null) {
146
+ return { composed: null, problems };
147
+ }
148
+ const baseUpdates = validateUpdates(
149
+ base.updates,
150
+ DEPENDABOT_BASE_FILE,
151
+ problems,
152
+ );
153
+ const localUpdates = validateUpdates(
154
+ local.updates,
155
+ DEPENDABOT_LOCAL_FILE,
156
+ problems,
157
+ );
158
+ const registryCollisions = Object.keys(local.registries).filter((name) =>
159
+ Object.hasOwn(base.registries, name),
160
+ );
161
+ if (registryCollisions.length > 0) {
162
+ problems.push(
163
+ `${DEPENDABOT_LOCAL_FILE} registries collide with canonical registries: ${registryCollisions.join(', ')}`,
164
+ );
165
+ }
166
+ if (problems.length > 0) {
167
+ return { composed: null, problems };
168
+ }
169
+ const updates = mergeUpdates(baseUpdates, localUpdates, problems);
170
+ const registries = { ...base.registries, ...local.registries };
171
+ inspectRegistryReferencesExist(
172
+ updates,
173
+ new Set(Object.keys(registries)),
174
+ problems,
175
+ );
176
+ if (problems.length > 0) {
177
+ return { composed: null, problems };
178
+ }
179
+ const document = {
180
+ ...base.document,
181
+ ...(Object.keys(registries).length > 0 ? { registries } : {}),
182
+ updates,
183
+ };
184
+ return {
185
+ composed: `${GENERATED_HEADER}${emitYamlDocument(document)}`,
186
+ problems: [],
187
+ };
188
+ };
@@ -0,0 +1,181 @@
1
+ import { DEPENDABOT_FILE } from './dependabot-compose';
2
+ import { updateTarget } from './dependabot-update';
3
+ import { isNonEmptyString, isRecord } from './github-settings-parse';
4
+ import { parseYaml } from './yaml-parse';
5
+
6
+ const DEPENDABOT_BASELINE_ECOSYSTEMS = ['bun', 'github-actions'] as const;
7
+ const DEPENDABOT_SCHEDULE_INTERVALS = new Set([
8
+ 'daily',
9
+ 'weekly',
10
+ 'monthly',
11
+ 'quarterly',
12
+ 'semiannually',
13
+ 'yearly',
14
+ 'cron',
15
+ ]);
16
+
17
+ type DependabotUpdateInspection = {
18
+ readonly problems: ReadonlyArray<string>;
19
+ readonly rootEcosystem: string | null;
20
+ };
21
+
22
+ const inspectDependabotSchedule = (
23
+ schedule: unknown,
24
+ label: string,
25
+ ): ReadonlyArray<string> => {
26
+ if (!(isRecord(schedule) && isNonEmptyString(schedule.interval))) {
27
+ return [`${label} must define schedule.interval`];
28
+ }
29
+ if (!DEPENDABOT_SCHEDULE_INTERVALS.has(schedule.interval)) {
30
+ return [`${label} has an unsupported schedule.interval`];
31
+ }
32
+ if (schedule.interval === 'cron' && !isNonEmptyString(schedule.cronjob)) {
33
+ return [`${label} must define schedule.cronjob for a cron interval`];
34
+ }
35
+ return [];
36
+ };
37
+
38
+ type DependabotGroupInspection = {
39
+ readonly problems: ReadonlyArray<string>;
40
+ readonly scheduledGroups: ReadonlySet<string>;
41
+ };
42
+
43
+ const inspectDependabotGroups = (
44
+ groups: unknown,
45
+ ): DependabotGroupInspection => {
46
+ if (groups === undefined) {
47
+ return { problems: [], scheduledGroups: new Set() };
48
+ }
49
+ if (!isRecord(groups)) {
50
+ return {
51
+ problems: [`${DEPENDABOT_FILE} multi-ecosystem-groups must be a mapping`],
52
+ scheduledGroups: new Set(),
53
+ };
54
+ }
55
+
56
+ const problems: Array<string> = [];
57
+ const scheduledGroups = new Set<string>();
58
+ for (const [name, group] of Object.entries(groups)) {
59
+ const label = `${DEPENDABOT_FILE} multi-ecosystem-groups.${name}`;
60
+ const groupProblems = isRecord(group)
61
+ ? inspectDependabotSchedule(group.schedule, label)
62
+ : [`${label} must be a mapping`];
63
+ problems.push(...groupProblems);
64
+ if (groupProblems.length === 0) {
65
+ scheduledGroups.add(name);
66
+ }
67
+ }
68
+ return { problems, scheduledGroups };
69
+ };
70
+
71
+ const inspectDependabotUpdateSchedule = (
72
+ grouped: {
73
+ readonly name: unknown;
74
+ readonly patterns: unknown;
75
+ readonly schedule: unknown;
76
+ },
77
+ label: string,
78
+ scheduledGroups: ReadonlySet<string>,
79
+ ): ReadonlyArray<string> => {
80
+ if (grouped.name === undefined) {
81
+ return inspectDependabotSchedule(grouped.schedule, label);
82
+ }
83
+ const problems: Array<string> = [];
84
+ if (!isNonEmptyString(grouped.name)) {
85
+ problems.push(`${label}.multi-ecosystem-group must be a non-empty string`);
86
+ } else if (!scheduledGroups.has(grouped.name)) {
87
+ problems.push(`${label} must reference a scheduled multi-ecosystem group`);
88
+ }
89
+ if (
90
+ !(
91
+ Array.isArray(grouped.patterns) &&
92
+ grouped.patterns.length > 0 &&
93
+ grouped.patterns.every(isNonEmptyString)
94
+ )
95
+ ) {
96
+ problems.push(
97
+ `${label} must define non-empty patterns when using a multi-ecosystem group`,
98
+ );
99
+ }
100
+ if (grouped.schedule !== undefined) {
101
+ problems.push(...inspectDependabotSchedule(grouped.schedule, label));
102
+ }
103
+ return problems;
104
+ };
105
+
106
+ const inspectDependabotUpdate = (
107
+ update: unknown,
108
+ index: number,
109
+ scheduledGroups: ReadonlySet<string>,
110
+ ): DependabotUpdateInspection => {
111
+ const label = `${DEPENDABOT_FILE} updates[${index}]`;
112
+ if (!isRecord(update)) {
113
+ return { problems: [`${label} must be a mapping`], rootEcosystem: null };
114
+ }
115
+
116
+ const {
117
+ schedule,
118
+ patterns,
119
+ 'multi-ecosystem-group': multiEcosystemGroup,
120
+ } = update;
121
+ const problems: Array<string> = [];
122
+ const target = updateTarget(update, label, problems);
123
+
124
+ problems.push(
125
+ ...inspectDependabotUpdateSchedule(
126
+ { name: multiEcosystemGroup, patterns, schedule },
127
+ label,
128
+ scheduledGroups,
129
+ ),
130
+ );
131
+
132
+ return {
133
+ problems,
134
+ rootEcosystem: target?.directories.includes('/') ? target.ecosystem : null,
135
+ };
136
+ };
137
+
138
+ export const inspectDependabot = (raw: string): ReadonlyArray<string> => {
139
+ const problems: Array<string> = [];
140
+ const { value: config, problem } = parseYaml(raw, DEPENDABOT_FILE);
141
+ if (problem !== null) {
142
+ return [problem];
143
+ }
144
+
145
+ if (!isRecord(config)) {
146
+ return [`${DEPENDABOT_FILE} must contain a YAML mapping`];
147
+ }
148
+ if (config.version !== 2) {
149
+ problems.push(`${DEPENDABOT_FILE} must use version: 2`);
150
+ }
151
+
152
+ const { updates, 'multi-ecosystem-groups': multiEcosystemGroups } = config;
153
+ if (!Array.isArray(updates)) {
154
+ problems.push(`${DEPENDABOT_FILE} must define an updates list`);
155
+ return problems;
156
+ }
157
+
158
+ const groupInspection = inspectDependabotGroups(multiEcosystemGroups);
159
+ problems.push(...groupInspection.problems);
160
+ const rootEcosystems = new Set<string>();
161
+ for (const [index, update] of updates.entries()) {
162
+ const inspection = inspectDependabotUpdate(
163
+ update,
164
+ index,
165
+ groupInspection.scheduledGroups,
166
+ );
167
+ problems.push(...inspection.problems);
168
+ if (inspection.rootEcosystem !== null) {
169
+ rootEcosystems.add(inspection.rootEcosystem);
170
+ }
171
+ }
172
+
173
+ for (const ecosystem of DEPENDABOT_BASELINE_ECOSYSTEMS) {
174
+ if (!rootEcosystems.has(ecosystem)) {
175
+ problems.push(
176
+ `${DEPENDABOT_FILE} must include a root-directory ${ecosystem} ecosystem`,
177
+ );
178
+ }
179
+ }
180
+ return problems;
181
+ };
@@ -0,0 +1,156 @@
1
+ import { isNonEmptyString, isRecord } from './github-settings-parse';
2
+
3
+ export type UpdateBlock = Record<string, unknown>;
4
+
5
+ export type UpdateTarget = {
6
+ readonly ecosystem: string;
7
+ readonly targetBranch: string | null;
8
+ readonly directories: ReadonlyArray<string>;
9
+ };
10
+
11
+ const listOfNonEmptyStrings = (
12
+ value: unknown,
13
+ ): value is ReadonlyArray<string> =>
14
+ Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
15
+
16
+ const inspectIgnoreEntry = (
17
+ entry: unknown,
18
+ label: string,
19
+ ): ReadonlyArray<string> => {
20
+ if (!isRecord(entry)) {
21
+ return [`${label} must be a mapping`];
22
+ }
23
+ const problems: Array<string> = [];
24
+ if (!isNonEmptyString(entry['dependency-name'])) {
25
+ problems.push(`${label} must define a non-empty dependency-name`);
26
+ }
27
+ for (const key of ['versions', 'update-types'] as const) {
28
+ const value = entry[key];
29
+ if (value !== undefined && !listOfNonEmptyStrings(value)) {
30
+ problems.push(`${label}.${key} must be a non-empty string list`);
31
+ }
32
+ }
33
+ return problems;
34
+ };
35
+
36
+ export const inspectIgnore = (
37
+ ignore: unknown,
38
+ label: string,
39
+ ): ReadonlyArray<string> => {
40
+ if (ignore === undefined) {
41
+ return [];
42
+ }
43
+ if (!Array.isArray(ignore)) {
44
+ return [`${label}.ignore must be a list`];
45
+ }
46
+ const problems: Array<string> = [];
47
+ for (const [index, entry] of ignore.entries()) {
48
+ problems.push(...inspectIgnoreEntry(entry, `${label}.ignore[${index}]`));
49
+ }
50
+ return problems;
51
+ };
52
+
53
+ export const inspectRegistryReferences = (
54
+ registries: unknown,
55
+ label: string,
56
+ ): ReadonlyArray<string> => {
57
+ if (registries === undefined) {
58
+ return [];
59
+ }
60
+ if (registries === '*') {
61
+ return [];
62
+ }
63
+ if (!listOfNonEmptyStrings(registries)) {
64
+ return [`${label}.registries must be "*" or a non-empty string list`];
65
+ }
66
+ return new Set(registries).size === registries.length
67
+ ? []
68
+ : [`${label}.registries must not contain duplicates`];
69
+ };
70
+
71
+ export const updateTarget = (
72
+ block: UpdateBlock,
73
+ label: string,
74
+ problems: Array<string>,
75
+ ): UpdateTarget | null => {
76
+ const ecosystem = block['package-ecosystem'];
77
+ if (!isNonEmptyString(ecosystem)) {
78
+ problems.push(`${label} must define package-ecosystem`);
79
+ }
80
+ const targetBranch = block['target-branch'];
81
+ if (targetBranch !== undefined && !isNonEmptyString(targetBranch)) {
82
+ problems.push(`${label}.target-branch must be a non-empty string`);
83
+ }
84
+
85
+ const { directory, directories } = block;
86
+ const hasDirectory = Object.hasOwn(block, 'directory');
87
+ const hasDirectories = Object.hasOwn(block, 'directories');
88
+ if (hasDirectory === hasDirectories) {
89
+ problems.push(
90
+ `${label} must define exactly one of directory or directories`,
91
+ );
92
+ return null;
93
+ }
94
+ if (hasDirectory && !isNonEmptyString(directory)) {
95
+ problems.push(`${label}.directory must be a non-empty string`);
96
+ return null;
97
+ }
98
+ if (hasDirectories && !listOfNonEmptyStrings(directories)) {
99
+ problems.push(`${label}.directories must be a non-empty string list`);
100
+ return null;
101
+ }
102
+ const rawDirectories: ReadonlyArray<string> = hasDirectory
103
+ ? [directory as string]
104
+ : (directories as ReadonlyArray<string>);
105
+ const normalized = [...new Set(rawDirectories)].sort();
106
+ if (
107
+ !hasDirectory &&
108
+ Array.isArray(directories) &&
109
+ normalized.length !== directories.length
110
+ ) {
111
+ problems.push(`${label}.directories must not contain duplicates`);
112
+ }
113
+ if (
114
+ !isNonEmptyString(ecosystem) ||
115
+ (targetBranch !== undefined && !isNonEmptyString(targetBranch))
116
+ ) {
117
+ return null;
118
+ }
119
+ return {
120
+ ecosystem,
121
+ targetBranch: targetBranch ?? null,
122
+ directories: normalized,
123
+ };
124
+ };
125
+
126
+ export const sameUpdateScope = (
127
+ left: UpdateTarget,
128
+ right: UpdateTarget,
129
+ ): boolean =>
130
+ left.ecosystem === right.ecosystem &&
131
+ left.targetBranch === right.targetBranch;
132
+
133
+ export const sameUpdateTarget = (
134
+ left: UpdateTarget,
135
+ right: UpdateTarget,
136
+ ): boolean =>
137
+ sameUpdateScope(left, right) &&
138
+ left.directories.length === right.directories.length &&
139
+ left.directories.every(
140
+ (directory, index) => directory === right.directories[index],
141
+ );
142
+
143
+ export const overlapsUpdateTarget = (
144
+ left: UpdateTarget,
145
+ right: UpdateTarget,
146
+ ): boolean =>
147
+ sameUpdateScope(left, right) &&
148
+ left.directories.some((directory) => right.directories.includes(directory));
149
+
150
+ export const updateTargetDescription = (target: UpdateTarget): string => {
151
+ const branch =
152
+ target.targetBranch === null
153
+ ? 'the default target branch'
154
+ : `target branch "${target.targetBranch}"`;
155
+ return `"${target.ecosystem}" on ${branch} for ${target.directories.join(', ')}`;
156
+ };
@@ -25,6 +25,9 @@ export type ParsedGithubSettings = {
25
25
  export const isRecord = (value: unknown): value is Record<string, unknown> =>
26
26
  typeof value === 'object' && value !== null && !Array.isArray(value);
27
27
 
28
+ export const isNonEmptyString = (value: unknown): value is string =>
29
+ typeof value === 'string' && value.length > 0;
30
+
28
31
  export const isNamedRuleset = (
29
32
  value: unknown,
30
33
  ): value is Readonly<Record<string, unknown>> & { readonly name: string } =>
@@ -30,6 +30,7 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
30
30
  name: 'check',
31
31
  commands: [
32
32
  `${SOURCE_CLI} structure --profile source`,
33
+ `${SOURCE_CLI} dependabot --check`,
33
34
  `${SOURCE_CLI} github --check`,
34
35
  'turbo run lint check-types test',
35
36
  ],
@@ -39,6 +40,7 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
39
40
  name: 'check:fix',
40
41
  commands: [
41
42
  `${SOURCE_CLI} structure --profile source`,
43
+ `${SOURCE_CLI} dependabot --write`,
42
44
  `${SOURCE_CLI} github --check`,
43
45
  'turbo run lint:fix check-types test',
44
46
  ],