diff --git a/docs/api/cautious-journey.syncoptions.flags.md b/docs/api/cautious-journey.statelabel.divider.md
similarity index 51%
rename from docs/api/cautious-journey.syncoptions.flags.md
rename to docs/api/cautious-journey.statelabel.divider.md
index 0a34686..3e097fb 100644
--- a/docs/api/cautious-journey.syncoptions.flags.md
+++ b/docs/api/cautious-journey.statelabel.divider.md
@@ -1,11 +1,11 @@
-[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [SyncOptions](./cautious-journey.syncoptions.md) > [flags](./cautious-journey.syncoptions.flags.md)
+[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [StateLabel](./cautious-journey.statelabel.md) > [divider](./cautious-journey.statelabel.divider.md)
-## SyncOptions.flags property
+## StateLabel.divider property
Signature:
```typescript
-flags: Array;
+divider: string;
```
diff --git a/docs/api/cautious-journey.statelabel.md b/docs/api/cautious-journey.statelabel.md
index aa40614..f68a182 100644
--- a/docs/api/cautious-journey.statelabel.md
+++ b/docs/api/cautious-journey.statelabel.md
@@ -17,5 +17,6 @@ export interface StateLabel extends BaseLabel
| Property | Type | Description |
| --- | --- | --- |
+| [divider](./cautious-journey.statelabel.divider.md) | string | |
| [values](./cautious-journey.statelabel.values.md) | Array<[StateValue](./cautious-journey.statevalue.md)> | Values for this state. |
diff --git a/docs/api/cautious-journey.syncoptions.colors.md b/docs/api/cautious-journey.syncoptions.colors.md
deleted file mode 100644
index b812f3c..0000000
--- a/docs/api/cautious-journey.syncoptions.colors.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [SyncOptions](./cautious-journey.syncoptions.md) > [colors](./cautious-journey.syncoptions.colors.md)
-
-## SyncOptions.colors property
-
-Signature:
-
-```typescript
-colors: Array;
-```
diff --git a/docs/api/cautious-journey.syncoptions.md b/docs/api/cautious-journey.syncoptions.md
index 5683f2e..c23ab46 100644
--- a/docs/api/cautious-journey.syncoptions.md
+++ b/docs/api/cautious-journey.syncoptions.md
@@ -14,11 +14,8 @@ export interface SyncOptions
| Property | Type | Description |
| --- | --- | --- |
-| [colors](./cautious-journey.syncoptions.colors.md) | Array<string> | |
-| [flags](./cautious-journey.syncoptions.flags.md) | Array<[FlagLabel](./cautious-journey.flaglabel.md)> | |
| [logger](./cautious-journey.syncoptions.logger.md) | Logger | |
-| [project](./cautious-journey.syncoptions.project.md) | string | |
+| [project](./cautious-journey.syncoptions.project.md) | ProjectConfig | |
| [random](./cautious-journey.syncoptions.random.md) | prng | |
| [remote](./cautious-journey.syncoptions.remote.md) | [Remote](./cautious-journey.remote.md) | |
-| [states](./cautious-journey.syncoptions.states.md) | Array<[StateLabel](./cautious-journey.statelabel.md)> | States from project config. |
diff --git a/docs/api/cautious-journey.syncoptions.project.md b/docs/api/cautious-journey.syncoptions.project.md
index 5ceea83..ed93344 100644
--- a/docs/api/cautious-journey.syncoptions.project.md
+++ b/docs/api/cautious-journey.syncoptions.project.md
@@ -7,5 +7,5 @@
Signature:
```typescript
-project: string;
+project: ProjectConfig;
```
diff --git a/docs/api/cautious-journey.syncoptions.states.md b/docs/api/cautious-journey.syncoptions.states.md
deleted file mode 100644
index eb05e71..0000000
--- a/docs/api/cautious-journey.syncoptions.states.md
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-[Home](./index.md) > [cautious-journey](./cautious-journey.md) > [SyncOptions](./cautious-journey.syncoptions.md) > [states](./cautious-journey.syncoptions.states.md)
-
-## SyncOptions.states property
-
-States from project config.
-
-Signature:
-
-```typescript
-states: Array;
-```
diff --git a/src/config/index.ts b/src/config/index.ts
index 064f2f7..32adb88 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -5,40 +5,51 @@ import { FlagLabel, StateLabel } from '../labels';
import { RemoteOptions } from '../remote';
import * as SCHEMA_DATA from './schema.yml';
+export interface LoggerConfig {
+ level: LogLevel;
+ name: string;
+}
+
+export interface ProjectConfig {
+ /**
+ * Color palette for labels without their own.
+ */
+ colors: Array;
+
+ /**
+ * Leave a comment along with any update, explaining the changes that were made.
+ *
+ * @default `true`
+ */
+ comment: boolean;
+
+ /**
+ * Individual flag labels.
+ */
+ flags: Array;
+
+ /**
+ * Project name or path.
+ */
+ name: string;
+
+ /**
+ * Remote APIs.
+ */
+ remote: RemoteOptions;
+
+ /**
+ * Grouped state labels.
+ */
+ states: Array;
+}
+
/**
* Config data for the app, loaded from CLI or DOM.
*/
export interface ConfigData {
- logger: {
- level: LogLevel;
- name: string;
- };
- projects: Array<{
- /**
- * Color palette for labels without their own.
- */
- colors: Array;
-
- /**
- * Individual flag labels.
- */
- flags: Array;
-
- /**
- * Project name or path.
- */
- name: string;
-
- /**
- * Remote APIs.
- */
- remote: RemoteOptions;
-
- /**
- * Grouped state labels.
- */
- states: Array;
- }>;
+ logger: LoggerConfig;
+ projects: Array;
}
/**
@@ -54,6 +65,7 @@ export function initConfig(): ConfigData {
},
projects: [{
colors: [],
+ comment: true,
flags: [],
name: '',
remote: {
@@ -82,5 +94,11 @@ export function validateConfig(it: unknown): it is ConfigData {
const ajv = new Ajv(SCHEMA_OPTIONS);
ajv.addSchema(SCHEMA_DATA, 'cautious-journey');
- return ajv.validate('cautious-journey#/definitions/config', it) === true;
+ if (ajv.validate('cautious-journey#/definitions/config', it) === true) {
+ return true;
+ } else {
+ /* eslint-disable-next-line */
+ console.error('invalid config', ajv.errors, it);
+ return false;
+ }
}
diff --git a/src/config/schema.yml b/src/config/schema.yml
index cd48ee2..6dd7059 100644
--- a/src/config/schema.yml
+++ b/src/config/schema.yml
@@ -3,12 +3,17 @@ $id: cautious-journey
definitions:
label-ref:
type: object
+ required:
+ - name
properties:
name:
type: string
change-set:
type: object
+ required:
+ - adds
+ - removes
properties:
adds:
type: array
@@ -23,6 +28,9 @@ definitions:
base-label:
type: object
+ required:
+ - name
+ - requires
properties:
color:
type: string
@@ -59,7 +67,13 @@ definitions:
- $ref: "#/definitions/change-set"
- $ref: "#/definitions/base-label"
- type: object
+ required:
+ - divider
+ - values
properties:
+ divider:
+ type: string
+ default: "/"
values:
type: array
items:
@@ -71,6 +85,8 @@ definitions:
- $ref: "#/definitions/change-set"
- $ref: "#/definitions/base-label"
- type: object
+ required:
+ - becomes
properties:
becomes:
type: array
@@ -78,38 +94,60 @@ definitions:
$ref: "#/definitions/state-change"
default: []
+ project:
+ type: object
+ required:
+ - colors
+ - comment
+ - flags
+ - name
+ - remote
+ - states
+ properties:
+ colors:
+ type: array
+ items:
+ type: string
+ comment:
+ type: boolean
+ default: true
+ flags:
+ type: array
+ items:
+ $ref: "#/definitions/flag-label"
+ default: []
+ name:
+ type: string
+ remote:
+ type: object
+ states:
+ type: array
+ items:
+ $ref: "#/definitions/state-label"
+ default: []
+
+ logger:
+ type: object
+ required:
+ - level
+ - name
+ properties:
+ level:
+ type: string
+ name:
+ type: string
+
config:
type: object
+ required:
+ - logger
+ - projects
properties:
logger:
- type: object
- properties:
- level:
- type: string
- name:
- type: string
+ $ref: "#/definitions/logger"
projects:
type: array
items:
- type: object
- properties:
- colors:
- type: array
- items:
- type: string
- flags:
- type: array
- items:
- $ref: "#/definitions/flag-label"
- default: []
- name:
- type: string
- remote:
- type: object
- states:
- type: array
- items:
- $ref: "#/definitions/state-label"
- default: []
-
+ $ref: "#/definitions/project"
+
type: object
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 60d06d0..7d6c919 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,7 +14,7 @@ const STATUS_ERROR = 1;
*/
main(process.argv).then((status) => process.exit(status)).catch((err: Error) => {
// eslint-disable-next-line no-console
- console.error('uncaught error during main:', err);
+ console.error('uncaught error during main:', err.message);
process.exit(STATUS_ERROR);
});
diff --git a/src/labels.ts b/src/labels.ts
index 94ed2ec..35de284 100644
--- a/src/labels.ts
+++ b/src/labels.ts
@@ -71,6 +71,8 @@ export interface StateValue extends BaseLabel {
* Grouped labels: the equivalent of a radio group.
*/
export interface StateLabel extends BaseLabel {
+ divider: string;
+
/**
* Values for this state.
*/
@@ -97,12 +99,12 @@ export function getLabelNames(flags: Array, states: Array
return new Set(labels);
}
-export function splitValueName(name: string): Array {
- return name.split('/');
+export function splitValueName(state: StateLabel, name: string): Array {
+ return name.split(state.divider);
}
export function getValueName(state: StateLabel, value: StateValue): string {
- return `${state.name}/${value.name}`;
+ return `${state.name}${state.divider}${value.name}`;
}
/**
diff --git a/src/main.ts b/src/main.ts
index dd69b8b..26df876 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,4 @@
-import { doesExist, InvalidArgumentError, isNil } from '@apextoaster/js-utils';
+import { doesExist, InvalidArgumentError } from '@apextoaster/js-utils';
import { createSchema } from '@apextoaster/js-yaml-schema';
import { existsSync, readFileSync, realpathSync } from 'fs';
import { DEFAULT_SAFE_SCHEMA, safeLoad } from 'js-yaml';
@@ -7,11 +7,11 @@ import { alea } from 'seedrandom';
import { ConfigData, validateConfig } from './config';
import { Commands, createParser } from './config/args';
+import { dotGraph, graphLabels } from './graph';
import { BunyanLogger } from './logger/bunyan';
import { GithubRemote } from './remote/github';
import { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
import { VERSION_INFO } from './version';
-import { graphLabels, dotGraph } from './graph';
export { FlagLabel, StateLabel } from './labels';
export { Remote, RemoteOptions } from './remote';
@@ -38,7 +38,7 @@ async function loadConfig(path: string): Promise {
const config = safeLoad(rawConfig, { schema });
if (!validateConfig(config)) {
- throw new Error();
+ throw new InvalidArgumentError();
}
return config as ConfigData;
@@ -52,14 +52,16 @@ export async function main(argv: Array): Promise {
const logger = BunyanLogger.create(config.logger);
logger.info({
- args,
- config,
mode,
version: VERSION_INFO,
- }, 'startup environment');
+ }, 'running main');
+ logger.debug({
+ args,
+ config,
+ }, 'runtime data');
for (const project of config.projects) {
- const { colors, flags, name, states } = project;
+ const { name } = project;
if (doesExist(args.project) && !args.project.includes(name)) {
logger.info({ project: name }, 'skipping project');
@@ -77,13 +79,10 @@ export async function main(argv: Array): Promise {
// mode switch
const options: SyncOptions = {
- colors,
- flags,
logger,
- project: name,
+ project,
random,
remote,
- states,
};
switch (mode) {
case Commands.GRAPH:
diff --git a/src/resolve.ts b/src/resolve.ts
index 38481e1..691800d 100644
--- a/src/resolve.ts
+++ b/src/resolve.ts
@@ -133,40 +133,45 @@ export function resolveLabels(options: ResolveInput): ResolveResult {
label: name,
});
}
- } else {
- const combinedValue: BaseLabel = {
- adds: [...state.adds, ...value.adds],
- name,
- priority: defaultUntil(value.priority, state.priority, 0),
- removes: [...state.removes, ...value.removes],
- requires: [...state.requires, ...value.requires],
- };
- if (checkLabelRules(combinedValue)) {
- continue;
- }
+ continue;
+ }
- let removed = false;
- for (const become of value.becomes) {
- if (become.matches.every((l) => activeLabels.has(l.name))) {
- checkLabelRules({
- ...combinedValue,
- adds: become.adds,
- removes: [...become.matches, ...become.removes],
- requires: [],
+ const combinedValue: BaseLabel = {
+ adds: [...state.adds, ...value.adds],
+ name,
+ priority: defaultUntil(value.priority, state.priority, 0),
+ removes: [...state.removes, ...value.removes],
+ requires: [...state.requires, ...value.requires],
+ };
+
+ if (checkLabelRules(combinedValue)) {
+ continue;
+ }
+
+ // TODO: flatten this bit and remove the mutable boolean
+ let removed = false;
+ for (const become of value.becomes) {
+ const matches = become.matches.every((l) => activeLabels.has(l.name));
+
+ if (matches) {
+ checkLabelRules({
+ ...combinedValue,
+ adds: become.adds,
+ removes: [...become.matches, ...become.removes],
+ requires: [],
+ });
+
+ if (activeLabels.delete(name)) {
+ changes.push({
+ cause: name,
+ effect: ChangeVerb.REMOVED,
+ label: name,
});
-
- if (activeLabels.delete(name)) {
- changes.push({
- cause: name,
- effect: ChangeVerb.REMOVED,
- label: name,
- });
- removed = true;
- }
-
- break;
+ removed = true;
}
+
+ break;
}
if (removed) {
diff --git a/src/sync.ts b/src/sync.ts
index b84ab80..159ac99 100644
--- a/src/sync.ts
+++ b/src/sync.ts
@@ -6,25 +6,13 @@ import { FlagLabel, getLabelColor, getLabelNames, getValueName, StateLabel } fro
import { LabelUpdate, Remote } from './remote';
import { resolveLabels } from './resolve';
import { defaultTo, defaultUntil, compareItems } from './utils';
+import { ProjectConfig } from './config';
export interface SyncOptions {
- /**
- */
- colors: Array;
-
- /**
- */
- flags: Array;
-
logger: Logger;
- project: string;
+ project: ProjectConfig;
random: prng;
remote: Remote;
-
- /**
- * States from project config.
- */
- states: Array;
}
/**
@@ -32,34 +20,38 @@ export interface SyncOptions {
* if there are changes and no errors, then updates the issue.
*/
export async function syncIssueLabels(options: SyncOptions): Promise {
- const issues = await options.remote.listIssues({
- project: options.project,
+ const { logger, project, remote } = options;
+ const issues = await remote.listIssues({
+ project: project.name,
});
for (const issue of issues) {
- options.logger.info({ issue }, 'project issue');
+ logger.info({ issue }, 'project issue');
const { changes, errors, labels } = resolveLabels({
- flags: options.flags,
+ flags: project.flags,
labels: issue.labels,
- states: options.states,
+ states: project.states,
});
- options.logger.debug({ changes, errors, issue, labels }, 'resolved labels');
+ logger.debug({ changes, errors, issue, labels }, 'resolved labels');
// TODO: prompt user to update this particular issue
const sameLabels = compareItems(issue.labels, labels) || changes.length === 0;
if (sameLabels === false && errors.length === 0) {
- options.logger.info({ changes, errors, issue, labels }, 'updating issue');
- await options.remote.updateIssue({
+ logger.info({ changes, errors, issue, labels }, 'updating issue');
+ await remote.updateIssue({
...issue,
labels,
});
- await options.remote.createComment({
- ...issue,
- changes,
- errors,
- });
+
+ if (project.comment) {
+ await remote.createComment({
+ ...issue,
+ changes,
+ errors,
+ });
+ }
}
}
@@ -67,19 +59,21 @@ export async function syncIssueLabels(options: SyncOptions): Promise {
}
export async function syncProjectLabels(options: SyncOptions): Promise {
- const labels = await options.remote.listLabels({
- project: options.project,
+ const { logger, project, remote } = options;
+
+ const labels = await remote.listLabels({
+ project: project.name,
});
const present = new Set(labels.map((l) => l.name));
- const desired = getLabelNames(options.flags, options.states);
+ const desired = getLabelNames(project.flags, project.states);
const combined = new Set([...desired, ...present]);
for (const label of combined) {
const exists = present.has(label);
const expected = desired.has(label);
- options.logger.info({
+ logger.info({
exists,
expected,
label,
@@ -90,15 +84,15 @@ export async function syncProjectLabels(options: SyncOptions): Promise
const data = mustExist(labels.find((l) => l.name === label));
await syncSingleLabel(options, data);
} else {
- options.logger.warn({ label }, 'remove label');
- await options.remote.deleteLabel({
+ logger.warn({ label }, 'remove label');
+ await remote.deleteLabel({
name: label,
- project: options.project,
+ project: project.name,
});
}
} else {
if (expected) {
- options.logger.info({ label }, 'create label');
+ logger.info({ label }, 'create label');
await createLabel(options, label);
} else {
// skip
@@ -110,27 +104,29 @@ export async function syncProjectLabels(options: SyncOptions): Promise
}
export async function createLabel(options: SyncOptions, name: string) {
- const flag = options.flags.find((it) => name === it.name);
+ const { project, remote } = options;
+
+ const flag = project.flags.find((it) => name === it.name);
if (doesExist(flag)) {
- await options.remote.createLabel({
- color: getLabelColor(options.colors, options.random, flag),
+ await remote.createLabel({
+ color: getLabelColor(project.colors, options.random, flag),
desc: mustExist(flag.desc),
name,
- project: options.project,
+ project: project.name,
});
return;
}
- const state = options.states.find((it) => name.startsWith(it.name));
+ const state = project.states.find((it) => name.startsWith(it.name));
if (doesExist(state)) {
const value = state.values.find((it) => getValueName(state, it) === name);
if (doesExist(value)) {
- await options.remote.createLabel({
- color: getLabelColor(options.colors, options.random, state, value),
+ await remote.createLabel({
+ color: getLabelColor(project.colors, options.random, state, value),
desc: defaultUntil(value.desc, state.desc, ''),
name: getValueName(state, value),
- project: options.project,
+ project: project.name,
});
return;
@@ -139,6 +135,8 @@ export async function createLabel(options: SyncOptions, name: string) {
}
export async function syncLabelDiff(options: SyncOptions, oldLabel: LabelUpdate, newLabel: LabelUpdate) {
+ const { logger, project } = options;
+
const dirty =
oldLabel.color !== mustExist(newLabel.color) ||
oldLabel.desc !== mustExist(newLabel.desc);
@@ -148,41 +146,40 @@ export async function syncLabelDiff(options: SyncOptions, oldLabel: LabelUpdate,
color: defaultTo(newLabel.color, oldLabel.color),
desc: defaultTo(newLabel.desc, oldLabel.desc),
name: oldLabel.name,
- project: options.project,
+ project: project.name,
};
- options.logger.debug({ body, newLabel, oldLabel }, 'update label');
-
+ logger.debug({ body, newLabel, oldLabel }, 'updating label');
const resp = await options.remote.updateLabel(body);
-
- options.logger.debug({ resp }, 'update resp');
+ logger.debug({ resp }, 'update response');
}
}
export async function syncSingleLabel(options: SyncOptions, label: LabelUpdate): Promise {
- const flag = options.flags.find((it) => label.name === it.name);
+ const { project } = options;
+ const flag = project.flags.find((it) => label.name === it.name);
if (doesExist(flag)) {
- const color = getLabelColor(options.colors, options.random, flag);
+ const color = getLabelColor(project.colors, options.random, flag);
await syncLabelDiff(options, label, {
color,
desc: defaultTo(flag.desc, label.desc),
name: flag.name,
- project: options.project,
+ project: project.name,
});
return;
}
- const state = options.states.find((it) => label.name.startsWith(it.name));
+ const state = project.states.find((it) => label.name.startsWith(it.name));
if (doesExist(state)) {
const value = state.values.find((it) => getValueName(state, it) === label.name);
if (doesExist(value)) {
- const color = mustExist(getLabelColor(options.colors, options.random, state, value));
+ const color = mustExist(getLabelColor(project.colors, options.random, state, value));
await syncLabelDiff(options, label, {
color,
desc: defaultTo(value.desc, label.desc),
name: getValueName(state, value),
- project: options.project,
+ project: project.name,
});
return;
diff --git a/src/utils.ts b/src/utils.ts
index efca542..192b119 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -35,3 +35,11 @@ export function compareItems(a: Array, b: Array): boolean {
return true;
}
+
+interface Collection {
+ has(value: T): boolean;
+}
+
+export function contains(a: Collection, b: Array): boolean {
+ return b.every((it) => a.has(it));
+}
diff --git a/test/TestLabels.ts b/test/TestLabels.ts
index 27cfb72..176b80c 100644
--- a/test/TestLabels.ts
+++ b/test/TestLabels.ts
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { alea } from 'seedrandom';
-import { getLabelColor, getLabelNames, prioritySort } from '../src/labels';
+import { getLabelColor, getLabelNames, prioritySort, StateLabel } from '../src/labels';
describe('label helpers', () => {
describe('label name helper', () => {
@@ -38,8 +38,9 @@ describe('label helpers', () => {
removes: [],
requires: [],
}];
- const states = [{
+ const states: Array = [{
adds: [],
+ divider: '/',
name: 'foo',
priority: 1,
removes: [],
@@ -47,6 +48,7 @@ describe('label helpers', () => {
values,
}, {
adds: [],
+ divider: '/',
name: 'bar',
priority: 1,
removes: [],
@@ -109,6 +111,7 @@ describe('label helpers', () => {
expect(getLabelColor(['test'], alea(), {
adds: [],
color: 'beans',
+ divider: '/',
name: '',
priority: 1,
removes: [],
@@ -129,6 +132,7 @@ describe('label helpers', () => {
expect(getLabelColor(['test'], alea(), {
adds: [],
color: 'beans',
+ divider: '/',
name: '',
priority: 1,
removes: [],
diff --git a/test/sync/TestSyncLabels.ts b/test/sync/TestSyncLabels.ts
index 6a8b9b2..1f0b07c 100644
--- a/test/sync/TestSyncLabels.ts
+++ b/test/sync/TestSyncLabels.ts
@@ -12,30 +12,35 @@ describe('label sync', () => {
const logger = BunyanLogger.create({
name: 'test',
});
- const remote = new GithubRemote({
+ const remoteConfig = {
data: {},
dryrun: true,
logger,
type: '',
- });
+ };
+ const remote = new GithubRemote(remoteConfig);
const updateSpy = spy(remote, 'updateLabel');
await syncSingleLabel({
- colors: [
- 'ff0000',
- ],
- flags: [{
- adds: [],
- name: 'foo',
- priority: 1,
- removes: [],
- requires: [],
- }],
logger,
- project: '',
+ project: {
+ colors: [
+ 'ff0000',
+ ],
+ comment: true,
+ flags: [{
+ adds: [],
+ name: 'foo',
+ priority: 1,
+ removes: [],
+ requires: [],
+ }],
+ name: '',
+ remote: remoteConfig,
+ states: [],
+ },
random: alea(),
remote,
- states: [],
}, {
color: '',
desc: '',